Good morning. I am having an issue with Authorize in controllers and using Roles and / or Policy set up. It is always returning a 403 forbidden, with the following in the logs:
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
The request address matched a server endpoint: Token.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
The token request was successfully extracted: {
"grant_type": "password",
"username": "Administrator#MRM2Inc.com",
"password": "[redacted]"
}.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
The token request was successfully validated.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
The response was successfully returned as a JSON document: {
"access_token": "[redacted]",
"token_type": "Bearer",
"expires_in": 3600
}.
info: OpenIddict.Validation.OpenIddictValidationDispatcher[0]
The response was successfully returned as a challenge response: {
"error": "insufficient_access",
"error_description": "The user represented by the token is not allowed to perform the requested action.",
"error_uri": "https://documentation.openiddict.com/errors/ID2095"
}.
info: OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler[13]
AuthenticationScheme: OpenIddict.Validation.AspNetCore was forbidden.
If I remove the Roles = or Policy = from the Authorize Tag it works. I have the project setup as follows:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<IdentDbContext>(options =>
{
options.UseSqlServer(
Configuration.GetConnectionString("IdentityDB"));
options.UseOpenIddict();
});
// Add the Identity Services we are going to be using the Application Users and the Application Roles
services.AddIdentity<ApplicationUsers, ApplicationRoles>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.SignIn.RequireConfirmedAccount = true;
config.User.RequireUniqueEmail = true;
config.Lockout.MaxFailedAccessAttempts = 3;
}).AddEntityFrameworkStores<IdentDbContext>()
.AddUserStore<ApplicationUserStore>()
.AddRoleStore<ApplicationRoleStore>()
.AddRoleManager<ApplicationRoleManager>()
.AddUserManager<ApplicationUserManager>()
.AddErrorDescriber<ApplicationIdentityErrorDescriber>()
.AddDefaultTokenProviders()
.AddDefaultUI();
services.AddDataLibrary();
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
// Add in the email
var emailConfig = Configuration.GetSection("EmailConfiguration").Get<EmailConfiguration>();
services.AddSingleton(emailConfig);
services.AddEmailLibrary();
services.AddAuthorization(option =>
{
option.AddPolicy("SiteAdmin", policy => policy.RequireClaim("Site Administrator"));
});
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default entities.
options.UseEntityFrameworkCore()
.UseDbContext<IdentDbContext>()
/*.ReplaceDefaultEntities<Guid>()*/;
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the token endpoint. What other endpoints?
options.SetLogoutEndpointUris("/api/LogoutPost")
.SetTokenEndpointUris("/Token");
// Enable the client credentials flow. Which flow do I need?
options.AllowPasswordFlow();
options.AcceptAnonymousClients();
options.DisableAccessTokenEncryption();
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the ASP.NET Core options.
options.UseAspNetCore()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough();
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(swagger =>
{
swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer'[space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\""
});
swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
swagger.OperationFilter<SwaggerDefaultValues>();
swagger.OperationFilter<AuthenticationRequirementOperationFilter>();
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
swagger.IncludeXmlComments(xmlPath);
});
services.AddApiVersioning();
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVVV";
options.DefaultApiVersion = ApiVersion.Parse("0.6.alpha");
options.AssumeDefaultVersionWhenUnspecified = true;
});
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.DisplayOperationId();
var versionDescription = provider.ApiVersionDescriptions;
foreach (var description in provider.ApiVersionDescriptions.OrderByDescending(_ => _.ApiVersion))
{
c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"MRM2 Identity API {description.GroupName}");
}
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
AuthorizationController.cs
/// <summary>
/// Controls the Authorization aspects of the API
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
[ApiVersion("0.8.alpha")]
[Produces(MediaTypeNames.Application.Json)]
public class AuthorizationController : ControllerBase
{
private readonly IConfiguration _configuration;
private readonly IdentDbContext _context;
private readonly ApplicationUserManager _userManager;
private readonly ApplicationRoleManager _roleManager;
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly IOpenIddictScopeManager _scopeManager;
private readonly SignInManager<ApplicationUsers> _signInManager;
private HttpClient _client;
public AuthorizationController(IConfiguration configuration, IdentDbContext context, ApplicationUserManager userManager,
ApplicationRoleManager roleManager, IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager, SignInManager<ApplicationUsers> signInManager)
{
_configuration = configuration;
_context = context;
_userManager = userManager;
_roleManager = roleManager;
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_signInManager = signInManager;
}
[HttpPost("/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
ClaimsPrincipal claimsPrincipal;
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
var roleList = await _userManager.GetRolesListAsync(user);
var databaseList = await _userManager.GetDatabasesAsync(user);
string symKey = _configuration["Jwt:Symmetrical:Key"];
string jwtSub = _configuration["Jwt:Subject"];
string issuer = _configuration["Jwt:Issuer"];
string audience = _configuration["Jwt:Audience"];
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, jwtSub, issuer),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), issuer),
new Claim(ClaimTypes.Name, user.UserName, issuer)
};
foreach (var role in roleList)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name));
}
foreach (var database in databaseList)
{
claims.Add(new Claim(type: "DatabaseName", database));
}
var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
identity.AddClaim(OpenIddictConstants.Claims.Name, user.UserName, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Subject, jwtSub, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Audience, audience, OpenIddictConstants.Destinations.AccessToken);
foreach (var cl in claims)
{
identity.AddClaim(cl.Type, cl.Value);
}
claimsPrincipal = new ClaimsPrincipal(identity);
// Set the list of scopes granted to the client application.
claimsPrincipal.SetScopes(new[]
{
Scopes.OpenId,
Scopes.Email,
Scopes.Profile,
Scopes.Roles
}.Intersect(request.GetScopes()));
foreach (var claim in claimsPrincipal.Claims)
{
claim.SetDestinations(GetDestinations(claim, claimsPrincipal));
}
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/device code/refresh token.
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
// Retrieve the user profile corresponding to the authorization code/refresh token.
// Note: if you want to automatically invalidate the authorization code/refresh token
// when the user password/roles change, use the following line instead:
// var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
var user = await _userManager.GetUserAsync(principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}
RolesController.cs
/// <summary>
/// Controls the actions for roles within the API
/// </summary>
/// <response code="401">If the user did not login correctly or
/// does not have the correct permissions</response>
[Route("api/[controller]")]
[ApiController]
[ApiVersion("0.8.alpha")]
[Produces(MediaTypeNames.Application.Json)]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme, Roles = "Site Administrator")] //If I change this to Policy = "SiteAdmin" still does not work. If I remove the Roles completely it works.
public class RolesController : ControllerBase
{
private readonly ApplicationRoleManager _roleManager;
private readonly ILogger<RolesController> _logger;
private readonly IApplicationDatabaseData _databaseData;
public RolesController(ApplicationRoleManager roleManager, ILogger<RolesController> logger, IApplicationDatabaseData databaseData)
{
_roleManager = roleManager;
_logger = logger;
_databaseData = databaseData;
}
/// <summary>
/// Gets a List of all the Roles
/// </summary>
/// <returns>A list of ApplicationRoles</returns>
/// <response code="200">Returns the list</response>
/// <response code="404">If the list is empty</response>
[HttpGet("ListRoles", Name = nameof(ListRolesAsync))]
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme/*, Roles = "Site Administrator"*/)] //Currently commented out until Roles work.
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IList<ApplicationRoles>>> ListRolesAsync()
{
var roles = await _roleManager.GetAllRoles();
if (!roles.Any())
{
return NotFound();
}
else
{
var output = roles;
return Ok(output);
}
}
}
What I noticed that when stepping through this in debugging except for the Claims.Name when going through GetDestinations all claims hit the default switch case. So I am not sure where in the startup or authorizationcontoller I went wrong. But I am pretty sure my issue is within there.
What have I missed to get my Roles and / or Policy to work correctly within the Controllers?
Update to the AuthorizationController allowed this to work. New section of the AuthorizationContoller for the Exchange method is as follows (still a work in progress, but now a working work in progress):
[HttpPost("/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
ClaimsPrincipal claimsPrincipal;
if (request.IsClientCredentialsGrantType())
{
var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
identity.AddClaim(OpenIddictConstants.Claims.Subject, request.ClientId ?? throw new InvalidOperationException());
identity.AddClaim("some-claim", "some-value", OpenIddictConstants.Destinations.AccessToken);
claimsPrincipal = new ClaimsPrincipal(identity);
claimsPrincipal.SetScopes(request.GetScopes());
}
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
var roleList = await _userManager.GetRolesListAsync(user);
var databaseList = await _userManager.GetDatabasesAsync(user);
string symKey = _configuration["Jwt:Symmetrical:Key"];
string jwtSub = _configuration["Jwt:Subject"] + " " + user.Id;
string issuer = _configuration["Jwt:Issuer"];
string audience = _configuration["Jwt:Audience"];
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, jwtSub, issuer),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), issuer),
new Claim(ClaimTypes.Name, user.UserName, issuer)
};
foreach (var role in roleList)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name, ClaimValueTypes.String, issuer));
}
foreach (var database in databaseList)
{
claims.Add(new Claim(type: "DatabaseName", database, ClaimValueTypes.String, issuer));
}
var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
identity.AddClaim(OpenIddictConstants.Claims.Name, user.UserName, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Subject, jwtSub, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Audience, audience, OpenIddictConstants.Destinations.AccessToken);
foreach (var cl in claims)
{
if (cl.Type == ClaimTypes.Role)
{
identity.AddClaim(OpenIddictConstants.Claims.Role, cl.Value, OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken);
}
identity.AddClaim(cl.Type, cl.Value, OpenIddictConstants.Destinations.IdentityToken);
}
claimsPrincipal = new ClaimsPrincipal(identity);
// Set the list of scopes granted to the client application.
claimsPrincipal.SetScopes(new[]
{
Scopes.OpenId,
Scopes.Email,
Scopes.Profile,
Scopes.Roles
}.Intersect(request.GetScopes()));
foreach (var claim in claimsPrincipal.Claims)
{
claim.SetDestinations(GetDestinations(claim, claimsPrincipal));
}
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/device code/refresh token.
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
// Retrieve the user profile corresponding to the authorization code/refresh token.
// Note: if you want to automatically invalidate the authorization code/refresh token
// when the user password/roles change, use the following line instead:
// var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
var user = await _userManager.GetUserAsync(principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
Related
I am using ASPNET core 5.0 for both front-end and back-end API. It worked perfectly on the local machine, but I deploy both the front-end and API application it always gives me audience validation failure. here is the code I am using.
"Jwt": {
"Issuer": "RestaurantPortal",
"Audience": "http://mansoor0786-001-site1.ctempurl.com/",
"Key": "ASAscethtCVdAQAAAAEAACcQAAAAEDhnGasldjaslkjdleEnGunGWR4Z79AvrtgIjYXhcWZx4OqpvWbsdsdsdSafcV/ZuPw25KbhKWhg1SIXXU2Ad7maaGAk******"
},
I have kept this in appSettings of both front end and API applications. Here is API startup code
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build();
});
services.AddAuthentication()
.AddCookie()
.AddJwtBearer(config =>
{
config.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = JwtConfiguration.JWTIssuer,
ValidAudience = JwtConfiguration.JWTAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey)),
ClockSkew = TimeSpan.Zero
};
});
Here is the validation I am doing on API end when user wants to login.
public bool ValidateToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = JwtConfiguration.JWTIssuer,
ValidAudience = JwtConfiguration.JWTAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey)),
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
}
catch (Exception)
{
return false;
}
return true;
}
Locally it works fine but when deploy these both applications it gives me an error and when I try to login it doesn't allow me to login into system. Here are the URL for both API and front-end application. This where I generate token
public string GenerateAccessToken(IEnumerable<Claim> claims)
{
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokeOptions = new JwtSecurityToken(
issuer: JwtConfiguration.JWTIssuer,
audience: JwtConfiguration.JWTAudience,
claims: claims,
expires: DateTime.Now.AddHours(24),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
return tokenString;
}
In this the configuration gets information from appSettings.json
public static class JwtConfiguration
{
public static readonly string JWTIssuer = Utils._config["Jwt:Issuer"];
public static readonly string JWTAudience = Utils._config["Jwt:Audience"];
public static readonly string JWTKey = Utils._config["Jwt:Key"];
}
This is my response from when I log in the user
if (apiResponseModel != null && apiResponseModel.Data != null && apiResponseModel.Data.Status == 1)
{
var claims = new List<Claim>
{
new Claim(AuthKeys.AccessToken, apiResponseModel.Data.AccessToken),
new Claim(AuthKeys.RefreshToken, apiResponseModel.Data.RefreshToken)
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(30),
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
if (apiResponseModel.Data.RoleName == UserRole.Roles.Customer.GetEnumDescription())
{
return RedirectToAction("index", "Home");
}
return RedirectToAction("index", "dashboard");
}
After that it redirected to dashboard index page where I wrote base controller and added attribute on top of basecontroller which does the following.
[ServiceFilter(typeof(JWT_Authentication))]
public class BaseController : Controller
{
public readonly IOptions<AppSettingDTO> _appSetting;
protected readonly IUserProfileInfo _userService;
public readonly IHttpContextAccessor _httpContextAccessor;
protected readonly IHttpNetClientService _apiService;
public BaseController(IOptions<AppSettingDTO> AppSetting, IHttpNetClientService HttpService, IUserProfileInfo UserInfo, IHttpContextAccessor HttpContext)
{
_appSetting = AppSetting;
_apiService = HttpService;
_userService = UserInfo;
_httpContextAccessor = HttpContext;
}
}
Here is my JWT_Authentication
public class JWT_Authentication : ActionFilterAttribute
{
private readonly IHttpContextAccessor _httpContextAccessor;
protected readonly IUserProfileInfo _userService;
public JWT_Authentication(IHttpContextAccessor HttpContext, IUserProfileInfo UserInfo)
{
_httpContextAccessor = HttpContext;
_userService = UserInfo;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
string actionName = context.RouteData.Values["Action"].ToString().ToLower();
string controllerName = context.RouteData.Values["Controller"].ToString().ToLower();
if (
controllerName != "account" && actionName != "logout")
{
string accessTokens = _userService.GetToken(_httpContextAccessor);
if (!_userService.ValidateToken(accessTokens))
{
}
else
{
return;
}
context.Result = new RedirectToRouteResult(new RouteValueDictionary(){
{ "action", "LogOut" },
{ "controller", "Account" }
});
return;
}
}
}
API
http://mansoor00786-001-site1.gtempurl.com/
Front-End
http://mansoor0786-001-site1.ctempurl.com/
I am calling login API from the front-end application which is also in asp net core 5.0 but it doesn't log me into the dashboard because of validation failure and that is because of the audience.
Well, as far as i saw it, here is some points I spot
There won't ever be an exception was throw when calling ValidateToken
Cause we put it on try catch block, so where does it throw audience validation failure ? It cannot be during deployment cause catch block doesn't have logging support anywhere, therefore, it might just be your assumption. And behavior on production state should be always redirect to Login page after logout.
The way MVC project handle Jwt Token was cumbersome
As we handmade Jwt Token and validate them ourself, such thing as validation failure with the same setting (Issuer, audience,...) should not exists. If that was fine on the client, have faith and logging those setting out from production state.
And for current approach, We can validate Jwt token and restrict them from access our resource fine, but HttpContext.User object still be null, therefore Authorization process became mostly, unusable.
Instead, how about consider to write our own Authentication scheme ?
What might be the problem here ?
public class JWT_Authentication : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
//... Some upper process
if (!_userService.ValidateToken(accessTokens))
{
// Doing something if the jwt invalid ?
}
else
{
return;
}
//... Some below process
}
}
If my block code idea was right, take a look at string accessTokens = _userService.GetToken(_httpContextAccessor);, log it out, as there might be a null here, due to you passing down a IHttpContextAccessor, which was singleton, not a HttpContext which scope for each request (localhost would be fine, cause we have only one client).
I'm using ASP.NET Core 3.1, and I have multiple external login providers configured:
services.AddAuthentication()
.AddDeviantArt(d => {
d.Scope.Add("feed");
d.ClientId = Configuration["Authentication:DeviantArt:ClientId"];
d.ClientSecret = Configuration["Authentication:DeviantArt:ClientSecret"];
d.SaveTokens = true;
})
.AddTwitter(t => {
t.ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"];
t.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"];
t.SaveTokens = true;
});
I'd like to create a custom AuthenticationProvider that, instead of redirecting to another website, asks for that website's "API key" and treats that as the access token. (The website in question does not support any version of OAuth.)
Before actually hooking it up, I want to test that I can get a custom AuthenticationProvider working at all, so I found one that implements HTTP Basic authentication:
public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> {
public CustomAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (!Request.Headers.ContainsKey("Authorization")) {
return AuthenticateResult.NoResult();
}
if (!AuthenticationHeaderValue.TryParse(Request.Headers["Authorization"], out AuthenticationHeaderValue headerValue)) {
return AuthenticateResult.NoResult();
}
if (!"Basic".Equals(headerValue.Scheme, StringComparison.OrdinalIgnoreCase)) {
return AuthenticateResult.NoResult();
}
byte[] headerValueBytes = Convert.FromBase64String(headerValue.Parameter);
string userAndPassword = Encoding.UTF8.GetString(headerValueBytes);
string[] parts = userAndPassword.Split(':');
if (parts.Length != 2) {
return AuthenticateResult.Fail("Invalid Basic authentication header");
}
string user = parts[0];
string password = parts[1];
bool isValidUser = true;
if (!isValidUser) {
return AuthenticateResult.Fail("Invalid username or password");
}
var claims = new[] { new Claim(ClaimTypes.Name, user) };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties) {
Response.Headers["WWW-Authenticate"] = $"Basic realm=\"Custom realm name here\", charset=\"UTF-8\"";
await base.HandleChallengeAsync(properties);
}
}
I added this to Startup.cs:
.AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>("Test", "Testing", o => { })
The problem is that the HandleAuthenticateAsync method is never called. The other solutions I find to this problem generally say you need to make it the "default" authentication scheme, but I don't want this to interfere with how the other external login providers are set up.
Is there a way I can get this working without changing the default authentication scheme or adding additional attributes to my controllers or actions?
You need add a default Scheme when you add AddAuthentication
like services.AddAuthentication("Test")
I need to implement multi-tenant REST API on asp.net core and use the Jwt Web token for authentication.
Asp.net core docs suggest using the following code in Startup.cs ConfigureServices method:
services.AddAuthentication().AddJwtBearer("Bearer", options =>
{
options.Audience = "MyAudience";
options.Authority = "https://myauhorityserver.com";
}
The issue is that my REST API application is multi-tenant. The tenant is discovered from the URL, e.g.
https://apple.myapi.com,
https://samsung.myapi.com,
https://google.myapi.com
So each of such URLs will eventually point to the same IP, but based on the first word in the URL the app discovers tenant on using the appropriate DB connection.
Each such tenant has its own Authority URL. We use Keycloak as an identity management server, so each tenant on it has its own REALM.
So the Authority URL per tenant is something like that:
https://mykeycloack.com/auth/realms/11111111,
https://mykeycloack.com/auth/realms/22222222,
https://mykeycloack.com/auth/realms/33333333
The API application should be able to add and remove tenants dynamically, without restarting the application so, setting all the tenants in the application startup is not a good idea.
I was trying to add more schemas with more calls to AddJwtBearer, however, all the calls go to the schema "Bearer", according to options.Events.OnAuthenticationFailed event. It's not clear how to make other schemas to handle calls with Bearer token in HTTP header. Even though if it's somehow possible with a help of custom middle-ware, as I mention before providing tenant-specific configuration for Bearer token authentication in the app startup is not a solution as new tenants need to be added dynamically.
Additional info:
According to the fiddler, the Authority URL is finally combined with
/.well-known/openid-configuration
and called when the first request comes to the API endpoint marked with
[Authorize]
If the to configuration fails, the API call fails too. If the call to configuration is successful, the application is not calling it again on the next API requests.
I have found a solution and made it a Nuget Package for reusability you can take a look.
Pretty much I replace the JwtBearerTokenHandler by a DynamicBearerTokenHandler, it gives the flexibility, to resolve OpenIdConnectOptions at runtime.
I've also made another package that cares care of that for you, the only thing you need is to resolve the authority for the ongoing request, you receive the HttpContext.
Here is the package:
https://github.com/PoweredSoft/DynamicJwtBearer
How to use it.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddMemoryCache();
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddDynamicJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.ValidateAudience = false;
})
.AddDynamicAuthorityJwtBearerResolver<ResolveAuthorityService>();
services.AddControllers();
}
}
The service
internal class ResolveAuthorityService : IDynamicJwtBearerAuthorityResolver
{
private readonly IConfiguration configuration;
public ResolveAuthorityService(IConfiguration configuration)
{
this.configuration = configuration;
}
public TimeSpan ExpirationOfConfiguration => TimeSpan.FromHours(1);
public Task<string> ResolveAuthority(HttpContext httpContext)
{
var realm = httpContext.Request.Headers["X-Tenant"].FirstOrDefault() ?? configuration["KeyCloak:MasterRealm"];
var authority = $"{configuration["KeyCloak:Endpoint"]}/realms/{realm}";
return Task.FromResult(authority);
}
}
Whats different from the original one
// before
if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await
Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
// after
var currentConfiguration = await this.dynamicJwtBearerHanderConfigurationResolver.ResolveCurrentOpenIdConfiguration(Context);
How its replaced
public static AuthenticationBuilder AddDynamicJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action<JwtBearerOptions> action = null)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
if (action != null)
return builder.AddScheme<JwtBearerOptions, DynamicJwtBearerHandler>(authenticationScheme, null, action);
return builder.AddScheme<JwtBearerOptions, DynamicJwtBearerHandler>(authenticationScheme, null, _ => { });
}
Source of the Handler
public class DynamicJwtBearerHandler : JwtBearerHandler
{
private readonly IDynamicJwtBearerHanderConfigurationResolver dynamicJwtBearerHanderConfigurationResolver;
public DynamicJwtBearerHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IDynamicJwtBearerHanderConfigurationResolver dynamicJwtBearerHanderConfigurationResolver) : base(options, logger, encoder, clock)
{
this.dynamicJwtBearerHanderConfigurationResolver = dynamicJwtBearerHanderConfigurationResolver;
}
/// <summary>
/// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using <see cref="TokenValidationParameters"/> set in the options.
/// </summary>
/// <returns></returns>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string token = null;
try
{
// Give application opportunity to find from a different location, adjust, or reject token
var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);
// event can set the token
await Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.Result != null)
{
return messageReceivedContext.Result;
}
// If application retrieved token from somewhere else, use that.
token = messageReceivedContext.Token;
if (string.IsNullOrEmpty(token))
{
string authorization = Request.Headers[HeaderNames.Authorization];
// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
return AuthenticateResult.NoResult();
}
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("Bearer ".Length).Trim();
}
// If no token found, no further work possible
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
}
var currentConfiguration = await this.dynamicJwtBearerHanderConfigurationResolver.ResolveCurrentOpenIdConfiguration(Context);
var validationParameters = Options.TokenValidationParameters.Clone();
if (currentConfiguration != null)
{
var issuers = new[] { currentConfiguration.Issuer };
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;
validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(currentConfiguration.SigningKeys)
?? currentConfiguration.SigningKeys;
}
List<Exception> validationFailures = null;
SecurityToken validatedToken;
foreach (var validator in Options.SecurityTokenValidators)
{
if (validator.CanReadToken(token))
{
ClaimsPrincipal principal;
try
{
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
}
catch (Exception ex)
{
Logger.TokenValidationFailed(ex);
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
&& ex is SecurityTokenSignatureKeyNotFoundException)
{
Options.ConfigurationManager.RequestRefresh();
}
if (validationFailures == null)
{
validationFailures = new List<Exception>(1);
}
validationFailures.Add(ex);
continue;
}
Logger.TokenValidationSucceeded();
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
{
Principal = principal,
SecurityToken = validatedToken
};
await Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.Result != null)
{
return tokenValidatedContext.Result;
}
if (Options.SaveToken)
{
tokenValidatedContext.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = "access_token", Value = token }
});
}
tokenValidatedContext.Success();
return tokenValidatedContext.Result;
}
}
if (validationFailures != null)
{
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
return AuthenticateResult.Fail(authenticationFailedContext.Exception);
}
return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
}
catch (Exception ex)
{
Logger.ErrorProcessingMessage(ex);
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = ex
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
throw;
}
}
}
If you're still trying to figure this out you can try Finbuckle.
https://www.finbuckle.com/MultiTenant/Docs/Authentication
I wanted to implement forms authentication with membership in my asp.net MVC Core application.
We had forms authentication setup in our previous application as below and wanted to use the same in .net core.
[HttpPost]
public ActionResult Login(LoginModel model, string returnUrl)
{
if (!this.ModelState.IsValid)
{
return this.View(model);
}
//Authenticate
if (!Membership.ValidateUser(model.UserName, model.Password))
{
this.ModelState.AddModelError(string.Empty, "The user name or
password provided is incorrect.");
return this.View(model);
}
else
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
return this.RedirectToAction("Index", "Home");
}
return this.View(model);
}
In my config:
<membership defaultProvider="ADMembership">
<providers>
<add name="ADMembership"
type="System.Web.Security.ActiveDirectoryMembershipProvider"
connectionStringName="ADConnectionString"
attributeMapUsername="sAMAccountName" />
</providers>
</membership>
So we are using active directory here in membership.
Is this still applicable in .net core.
If not what else is available in .net core for forms authentication and AD.
Would appreciate inputs.
Yes you can do that in Core MVC application. You enable form authentication and use LDAP as user store at the back-end.
Here is how I set things up, to give you start:
Startup.cs
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
// Read LDAP settings from appsettings
services.Configure<LdapConfig>(this.Configuration.GetSection("ldap"));
// Define an interface for authentication service,
// We used Novell.Directory.Ldap as implementation.
services.AddScoped<IAuthenticationService, LdapAuthenticationService>();
// Global filter is enabled to protect the whole site
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
...
});
// Form authentication and cookies settings
var cookiesConfig = this.Configuration.GetSection("cookies").Get<CookiesConfig>();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = cookiesConfig.CookieName;
options.LoginPath = cookiesConfig.LoginPath;
options.LogoutPath = cookiesConfig.LogoutPath;
options.AccessDeniedPath = cookiesConfig.AccessDeniedPath;
options.ReturnUrlParameter = cookiesConfig.ReturnUrlParameter;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Redirects all HTTP requests to HTTPS
if (env.IsProduction())
{
app.UseRewriter(new RewriteOptions()
.AddRedirectToHttpsPermanent());
}
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
app.UseStaticFiles();
app.UseStatusCodePagesWithReExecute("/error", "?code={0}");
app.UseAuthentication();
app.UseMvc(routes =>
{
...
});
}
}
appsettings.json
{
"connectionStrings": {
"appDbConnection": xxx
},
"ldap": {
"url": "xxx.loc",
"bindDn": "CN=Users,DC=xxx,DC=loc",
"username": "xxx",
"password": "xxx",
"searchBase": "DC=xxx,DC=loc",
"searchFilter": "(&(objectClass=user)(objectClass=person)(sAMAccountName={0}))"
},
"cookies": {
"cookieName": "xxx",
"loginPath": "/account/login",
"logoutPath": "/account/logout",
"accessDeniedPath": "/account/accessDenied",
"returnUrlParameter": "returnUrl"
}
}
IAuthenticationService.cs
namespace DL.SO.Services.Core
{
public interface IAuthenticationService
{
IAppUser Login(string username, string password);
}
}
LdapAuthenticationService.cs
Ldap implementation of authentication service, using Novell.Directory.Ldap library to talk to active directory. You can Nuget that library.
using Microsoft.Extensions.Options;
using Novell.Directory.Ldap;
...
using DL.SO.Services.Core;
namespace DL.SO.Services.Security.Ldap
{
public class LdapAuthenticationService : IAuthenticationService
{
private const string MemberOfAttribute = "memberOf";
private const string DisplayNameAttribute = "displayName";
private const string SAMAccountNameAttribute = "sAMAccountName";
private const string MailAttribute = "mail";
private readonly LdapConfig _config;
private readonly LdapConnection _connection;
public LdapAuthenticationService(IOptions<LdapConfig> configAccessor)
{
// Config from appsettings, injected through the pipeline
_config = configAccessor.Value;
_connection = new LdapConnection();
}
public IAppUser Login(string username, string password)
{
_connection.Connect(_config.Url, LdapConnection.DEFAULT_PORT);
_connection.Bind(_config.Username, _config.Password);
var searchFilter = String.Format(_config.SearchFilter, username);
var result = _connection.Search(_config.SearchBase, LdapConnection.SCOPE_SUB, searchFilter,
new[] { MemberOfAttribute, DisplayNameAttribute, SAMAccountNameAttribute, MailAttribute }, false);
try
{
var user = result.next();
if (user != null)
{
_connection.Bind(user.DN, password);
if (_connection.Bound)
{
var accountNameAttr = user.getAttribute(SAMAccountNameAttribute);
if (accountNameAttr == null)
{
throw new Exception("Your account is missing the account name.");
}
var displayNameAttr = user.getAttribute(DisplayNameAttribute);
if (displayNameAttr == null)
{
throw new Exception("Your account is missing the display name.");
}
var emailAttr = user.getAttribute(MailAttribute);
if (emailAttr == null)
{
throw new Exception("Your account is missing an email.");
}
var memberAttr = user.getAttribute(MemberOfAttribute);
if (memberAttr == null)
{
throw new Exception("Your account is missing roles.");
}
return new AppUser
{
DisplayName = displayNameAttr.StringValue,
Username = accountNameAttr.StringValue,
Email = emailAttr.StringValue,
Roles = memberAttr.StringValueArray
.Select(x => GetGroup(x))
.Where(x => x != null)
.Distinct()
.ToArray()
};
}
}
}
finally
{
_connection.Disconnect();
}
return null;
}
}
}
AccountController.cs
Then finally after the user is verified, you need to construct the principal from the user claims for sign in process, which would generate the cookie behind the scene.
public class AccountController : Controller
{
private readonly IAuthenticationService _authService;
public AccountController(IAuthenticationService authService)
{
_authService = authService;
}
...
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (ModelState.Valid)
{
try
{
var user = _authService.Login(model.Username, model.Password);
if (user != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Username),
new Claim(CustomClaimTypes.DisplayName, user.DisplayName),
new Claim(ClaimTypes.Email, user.Email)
}
// Roles
foreach (var role in user.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
// Construct Principal
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, _authService.GetType().Name));
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
principal,
new AuthenticationProperties
{
IsPersistent = model.RememberMe
}
);
return Redirect(Url.IsLocalUrl(model.ReturnUrl)
? model.ReturnUrl
: "/");
}
ModelState.AddModelError("", #"Your username or password is incorrect.");
}
catch(Exception ex)
{
ModelState.AddModelError("", ex.Message);
}
}
return View(model);
}
}
Would this post help you integrate with AD for Authentication and Authorization?
MVC Core How to force / set global authorization for all actions?
The idea is add authentication within ConfigureServices method in Startup.cs file:
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireRole([Your AD security group name in here without domain name]) // This line adds authorization to users in the AD group only
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
In Asp.Net Core the Authentication is controlled through project properties.
Open the solution. Right click on the Project and Click Properties.
Click the Debug tab. Check the Enable Windows Authentication checkbox. Ensure Anonymous Authentication is disabled.
Here is Microsoft's document, https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth
Cheers!
I have set up my Resource Server (Web Api 2) to validate JWT token for incoming requests. The JWT token is issued by Auth0 and my client pass it to my web api. This all works fine and raises 401 response if Issuer, Audience or Expiry date is not valid. When I add my custom middleware derived from OwinMiddleware it suppresses token validation logic and I get 200 response for invalid requests.
public class Startup
{
public void Configuration(IAppBuilder app)
{
var issuer = "my issuer";
var audience= "my audience";
var clientId= "my client id";
app.UseActiveDirectoryFederationServicesBearerAuthentication(
new ActiveDirectoryFederationServicesBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = audience,
ValidIssuer = issuer,
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) => parameters.IssuerSigningTokens.FirstOrDefault()?.SecurityKeys?.FirstOrDefault()
},
// Setting the MetadataEndpoint so the middleware can download the RS256 certificate
MetadataEndpoint = $"{issuer.TrimEnd('/')}/wsfed/{clientId}/FederationMetadata/2007-06/FederationMetadata.xml"
});
HttpConfiguration config = new HttpConfiguration();
app.Use<HttpUsernameInjector>();
// Web API routes
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
and my custom OwinMiddleWare:
public class HttpUsernameInjector : OwinMiddleware
{
public HttpUsernameInjector(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
const string usernameClaimKey = "my username claim key";
var bearerString = context.Request.Headers["Authorization"];
if (bearerString != null && bearerString.StartsWith("Bearer ", StringComparison.InvariantCultureIgnoreCase))
{
var tokenString = bearerString.Substring(7);
var token = new JwtSecurityToken(tokenString);
var claims = token.Claims.ToList();
var username = claims.FirstOrDefault(x => x.Type == usernameClaimKey);
if (username == null) throw new Exception("Token should have username");
// Add to HttpContext
var genericPrincipal = new GenericPrincipal(new GenericIdentity(username.Value), new string[] { });
IPrincipal principal = genericPrincipal;
context.Request.User = principal;
}
await Next.Invoke(context);
}
}
How should I configure my custom middleware to avoid conflict/suppressing OWIN token authentication logic?
Nothing's wrong with OWINMiddleware but assigning context.Request.User causes problem. GenericIdentity created here has a Readonly IsAuthenticated equal to true and not possible to set to false. When assigning context.Request.User = genericPrincipal; it overrides IsAuthenticated inside context.Request.User with IsAuthenticated from genericPrincipal. Need to check for Authentication result at the beginning of Invoke method and skip the logic if user is not authenticated. So it wouldn't change IsAuthenticated in context.Request.User.
public override async Task Invoke(IOwinContext context)
{
if (context.Authentication.User.Identity.IsAuthenticated)
{
//my username injection logic
}
await Next.Invoke(context);
}