I have an ASP.NET Core API which uses JWT Authentication. Simple setup:
....
string authority = $"https://{configuration["Auth:Authority"]}";
string audience = configuration["Auth:Audience"];
return builder.AddJwtBearer(options =>
{
options.Authority = authority;
options.Audience = audience;
options.TokenValidationParameters = new TokenValidationParameters
{
// NameClaimType = "http://schemas.org/email"
// NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"
// NameClaimType = "sub"
};
});
(as you can see commented code, I have been trying several settings)
When I decode JWT (using jwt.io) I see there is claim "sub" in JWT and has string value (internal ID of user in dbo)
{
"nbf": 1592585433,
"exp": 1592585763,
"iss": "https://*************",
"aud": "api_10",
"sub": "142",
"scope": [
"openid",
"api_10"
]
}
The problem is that dotnet switch sub to name claim. This returns no (0) results:
principal.Claims.Where(w => w.Type == "sub")
This returns userId I need "142":
principal.Claims.Where(w => w.Type == ClaimTypes.Name || ClaimTypes.NameIdentifier)
What is going on?! Where is my sub claim gone?
Just add JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); on the API's ConfigureServices method. Its suggested on the other comment as well. I verified it on my sample repo.
This issue is because OIDC StandardClaims are renamed on JWT token handler. By adding JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); you will clear the inbound claim type map on the JWT token handler.
Read more here
Related
I have a github project link where the client sends Authorization header to the server containing the JWT token like below. What I can't understand is how on the server side [Authorize(Role.Admin)]can understand the Role.Admin which is application specific enum value (belonging to the account object - details beow).
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// add auth header with jwt if account is logged in and request is to the api url
const account = this.accountService.accountValue;
const isLoggedIn = account && account.jwtToken;
const isApiUrl = request.url.startsWith(environment.apiUrl);
if (isLoggedIn && isApiUrl) {
request = request.clone({
setHeaders: { Authorization: `Bearer ${account.jwtToken}` }
});
}
return next.handle(request);
}
On the server side I have middleware class containing the code that decripts the token like this:
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var accountId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
// attach account to context on successful jwt validation
context.Items["Account"] = await dataContext.Accounts.FindAsync(accountId);
}
catch
{
// do nothing if jwt validation fails
// account is not attached to context so request won't have access to secure routes
Console.WriteLine("Failed");
}
So on the server side I have controller that has [Authorize(Role.Admin)] annotation like below which I can't understand - how Authorize can understand the Role:Admin - which is application specific - is it via introspection?
[Authorize(Role.Admin)]
[HttpGet("all-dates")]
public ActionResult<ScheduleDateTimeResponse> GetAllDates()
{
var dates = _accountService.GetAllDates();
return Ok(dates);
}
The project has a custom AuthorizeAttribute for which the Role enumeration is in scope. This differs from the standard attribute from the BCL. This attribute implements IAuthorizationFilter which the asp.net middleware understands must be invoked against this controller action.
Probably the application is referring to some kind of authentication service before the call endpoint. You should be able to see it in the console logs.
Check in solution nugets if there is no some kind of company aut service package
Ofc if you are sure that role is not a part of JWT token.
you can check that with: https://jwt.io/
Sorry...but I cannot add just simply comment yet...
I am struggling to find an exact way of validating my OAuth bearer token which is passed when a request is sent to the API am working on which is a Asp.Net core project.
The purpose here is to extract the bearer token and Validate it and if all is fine then continue with the request.
So far my findings have come across the following
JWT bear token authorization which mostly talks about access_token
Asp.Net core security middleware
Custom Authorize attribute which handle this.
I am not really sure how I can achieve my validation? Should I extract the bearer token and then create a custom validating method?
Ideally would like the [Authorize] attribute to handle this.
Suggestions please?
Well finally after more research I finally found that custom AuthorizationHandler is a more suitable solution as suppose to using custom Authorize attributes which is not suggested in Asp.Net Core.
It was simple to setup and I am able to extract my Bearer token from the header for further authorization with OAuth.
Here is a my approach:
public class CustomAuthorizationHandler: IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
var authFilterCtx = (Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext)context.Resource;
string authHeader = authFilterCtx.HttpContext.Request.Headers["Authorization"];
if (authHeader != null && authHeader.Contains("Bearer"))
{
var token = authHeader.Replace("Bearer", "");
// Now token can be used for further authorization
}
throw new NotImplementedException();
}
}
Lastly registering the handler in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IAuthorizationHandler, CustomAuthorizationHandler>();
}
I think to put the following code snippet inside ConfigureServices() should be able to validate your access_token after installing Microsoft.AspNetCore.Authentication.JwtBearer NuGet package:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
//options.SaveToken = true;
options.MetadataAddress = ValidationEndPoint;
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = tokenIssuer,
ValidAudiences = new[] { clientId },
ValidAudience = null
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("MyPolicy", policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
});
remember to put app.UseAuthentication() and app.UseAuthorization() in the Configure() method. And add [authorize] to your controller API.
I have the stock file | new | web | asp.net core web api project template where I selected AzureAD authentication that generated the following Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
and the following appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "mymsdn.onmicrosoft.com",
"TenantId": "<my azuread tenant id>",
"ClientId": "<my azuread web app id>"
}
I'm using postman to acquire token leveraging a public client profile app setup just as I have done for another web api setup that is working as expected with the same azureAd bearer token auth code and settings coordinates.
For some reason this app is trying to validate the wrong token issuer format and i'm at a loss as to how I correct it.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information: AzureADJwtBearer was not authenticated. Failure message: IDX10205: Issuer validation failed. Issuer: 'https://login.microsoftonline.com/<my azuread tenantid>/v2.0'. Did not match: validationParameters.ValidIssuer: 'null' or validationParameters.ValidIssuers: 'https://sts.windows.net/<my azuread tenantid>/'.
Turns out this issue surfaces if you configured azuread app registrations entry for your ClientId to support any organization or consumer, aka microsoft account, signins instead of just your organization or any organization. The fix was to use the AddJwtBearer() block of Startup.ConfigureServices() code shown below instead of the project template provided AddAzureADBearer() block.
public void ConfigureServices(IServiceCollection services)
{
// if azuread app registrations entry for ClientId has "signInAudience": "AzureADMyOrg" or "AzureADMultipleOrgs" where "iss": "https://sts.windows.net/{TenantId}/"
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => //Configuration.Bind("AzureAd", options));
{
Configuration.Bind("AzureAd", options);
Log.LogInformation($"the AddAzureADBearer options have been configured for ClientId = {options.ClientId}");
});
// if azuread app registrations entry for ClientId has "signInAudience": "AzureADandPersonalMicrosoftAccount" where "iss": "https://login.microsoftonline.com/{TenantId}/v2.0"
services.AddAuthentication(options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; })
.AddJwtBearer(options =>
{
var azureadoptions = new AzureADOptions(); Configuration.Bind("AzureAd", azureadoptions);
options.Authority = $"{azureadoptions.Instance}{azureadoptions.TenantId}/v2";
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudience = $"{azureadoptions.ClientId}",
//ValidAudiences = new List<string> { $"{azureadoptions.ClientId}", $"api://{azureadoptions.ClientId}", $"https://myapp.azurewebsites.net/" },
//ValidIssuer = $"https://sts.windows.net/{azureadoptions.TenantId}/" // for "signInAudience": "AzureADMyOrg" or "AzureADMultipleOrgs"
ValidIssuer = $"{azureadoptions.Instance}{azureadoptions.TenantId}/v2.0" // for "signInAudience": "AzureADandPersonalMicrosoftAccount"
//ValidIssuers = new List<string> { $"https://sts.windows.net/{azureadoptions.TenantId}/", $"{azureadoptions.Instance}{azureadoptions.TenantId}/v2.0" }
};
Log.LogInformation($"the AddJwtBearer options have been configured for ClientId = {azureadoptions.ClientId}");
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
I was trying to authenticate existing token from uwp-app in web-api and I had a similar issue:
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information: Failed to validate the token.
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidIssuerException: IDX10205: Issuer validation failed. Issuer: 'https://spolujizda.b2clogin.com/4e8094c9-5058-454c-b201-ef61d7ae6619/v2.0/'. Did not match: validationParameters.ValidIssuer: 'null' or validationParameters.ValidIssuers: 'https://login.microsoftonline.com/4e8094c9-5058-454c-b201-ef61d7ae6619/v2.0/'.
at Microsoft.IdentityModel.Tokens.Validators.ValidateIssuer(String issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateIssuer(String issuer, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information: AzureADB2CJwtBearer was not authenticated. Failure message: IDX10205: Issuer validation failed. Issuer: 'https://spolujizda.b2clogin.com/4e8094c9-5058-454c-b201-ef61d7ae6619/v2.0/'. Did not match: validationParameters.ValidIssuer: 'null' or validationParameters.ValidIssuers: 'https://login.microsoftonline.com/4e8094c9-5058-454c-b201-ef61d7ae6619/v2.0/'.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Route matched with {action = "Get", controller = "Values"}. Executing action Spolujizda.ApiServer.Controllers.ValuesController.Get (Spolujizda.ApiServer)
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes ().
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information: AuthenticationScheme: AzureADB2CJwtBearer was challenged.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action Spolujizda.ApiServer.Controllers.ValuesController.Get (Spolujizda.ApiServer) in 8.105ms
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 8950.4739ms 401 text/plain
But for me the fix was much simpler.
For Web API I had AzureAdB2C.Instance set to https://spolujizda.b2clogin.com/tfp/.
For an UWP app, I had been issuing token via https://login.microsoftonline.com/tfp/
That is why it resulted in this error. Because in the UWP app, the token was issued for login.microsoft... and in the Web API, it was trying to verify issuer as spolujizda.b2clogin...
First I tried to change address for issuing token for uwp-app, but that didn't work.
So secondly I just changed web-api's AzureAdB2C.Instance config to login.microsoft.... and it now works.
In my .net core 1.1 code I'm doing the authentication as follows(which is sending the bearer token to external URL and look for claims in the return token). This code is in Configure method
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = signinAuthority,
RequireHttpsMetadata = signinHTTPS,
ClientId = "skybus",
ClientSecret = "secret",
ResponseType = "code id_token",
Scope = { "api1", "offline_access" },
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true
});
Now I upgraded my code to .net Core 2.0 the both UseCookieAuthentication & UseOpenIdConnectAuthentication are changed. I'm finding it difficult to find what needs to be done in this case
What I changed it to is as follows in ConfigureServices method
services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(o =>
{
o.Authority = signinAuthority;
o.SignInScheme = "Cookies";
o.RequireHttpsMetadata = signinHTTPS;
o.ClientId = "skybus";
o.ClientSecret = "secret";
o.ResponseType = "code id_token";
o.GetClaimsFromUserInfoEndpoint = true;
o.SaveTokens = true;
o.Scope.Add("api1");
o.Scope.Add("offline_access");
});
In Browser I see this URL after the above changes. It should either show me the external login page if user is not logged in or return to home page of my website
http://localhost:5000/Account/Login?ReturnUrl=%2F
I followed this link from microsoft to start my migration. Most of the migration is covered by the this link, but I faced issue where most of my claims are missing.
With the ASP.NET Core 1.x, client would have received the claims: nbf, exp, iss, aud, nonce, iat, c_hash, sid, sub, auth_time, idp, amr.
In Core 2.0 we only get sid, sub and idp. What happened?
Microsoft added a new concept to their OpenID Connect handler called ClaimActions. Claim actions allow modifying how claims from an external provider are mapped (or not) to a claim in your ClaimsPrincipal. Looking at the ctor of the OpenIdConnectOptions, you can see that the handler will now skip the following claims by default:
ClaimActions.DeleteClaim("nonce");
ClaimActions.DeleteClaim("aud");
ClaimActions.DeleteClaim("azp");
ClaimActions.DeleteClaim("acr");
ClaimActions.DeleteClaim("amr");
ClaimActions.DeleteClaim("iss");
ClaimActions.DeleteClaim("iat");
ClaimActions.DeleteClaim("nbf");
ClaimActions.DeleteClaim("exp");
ClaimActions.DeleteClaim("at_hash");
ClaimActions.DeleteClaim("c_hash");
ClaimActions.DeleteClaim("auth_time");
ClaimActions.DeleteClaim("ipaddr");
ClaimActions.DeleteClaim("platf");
ClaimActions.DeleteClaim("ver");
If you want to “un-skip” a claim, you need to delete a specific claim action when setting up the handler. The following is the very intuitive syntax to get the amr claim back:
options.ClaimActions.Remove("amr");
Requesting more claims from the OIDC provider
When you are requesting more scopes, e.g. profile or custom scopes that result in more claims, there is another confusing detail to be aware of.
Depending on the response_type in the OIDC protocol, some claims are transferred via the id_token and some via the userinfo endpoint.
So first of all, you need to enable support for the userinfo endpoint in the handler:
options.GetClaimsFromUserInfoEndpoint = true;
In the end you need to add the following class to import all other custom claims
public class MapAllClaimsAction : ClaimAction
{
public MapAllClaimsAction() : base(string.Empty, string.Empty)
{
}
public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
{
foreach (var claim in identity.Claims)
{
// If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set
var shortClaimTypeName = claim.Properties.ContainsKey(JwtSecurityTokenHandler.ShortClaimTypeProperty) ?
claim.Properties[JwtSecurityTokenHandler.ShortClaimTypeProperty] : string.Empty;
// checking if claim in the identity (generated from id_token) has the same type as a claim retrieved from userinfo endpoint
JToken value;
var isClaimIncluded = userData.TryGetValue(claim.Type, out value) || userData.TryGetValue(shortClaimTypeName, out value);
// if a same claim exists (matching both type and value) both in id_token identity and userinfo response, remove the json entry from the userinfo response
if (isClaimIncluded && claim.Value.Equals(value.ToString(), StringComparison.Ordinal))
{
if (!userData.Remove(claim.Type))
{
userData.Remove(shortClaimTypeName);
}
}
}
// adding remaining unique claims from userinfo endpoint to the identity
foreach (var pair in userData)
{
JToken value;
var claimValue = userData.TryGetValue(pair.Key, out value) ? value.ToString() : null;
identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, issuer));
}
}
}
and then use the above class code to add it to ClaimActions
options.ClaimActions.Add(new MapAllClaimsAction());
I don't know if you are using IdentityServer4 but they have provided some samples on github for ASP.NET Core 2.0.
Hope this helps you.
Sample Project
Did you take the authentication middleware into use?
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
...
I have IdentityServer4 that generates signed JWT tokens.
In my web api I added auth middleware to validate these tokens:
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = env.IsProduction() ? "https://www.example.com/api/" : "http://localhost/api/",
AllowedScopes = { "WebAPI", "firm",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile },
RequireHttpsMetadata = env.IsProduction(),
});
It works perfectly. However, I suspect it doesn't verify signature of jwt token because there is no public key configured to validate token. How to configure token signature validation?
PS: I try to use UseJwtBearerAuthentication instead this way:
var cert = new X509Certificate2("X509.pfx", "mypassword");
var TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidIssuer = env.IsProduction() ? "https://www.example.com/api/" : "http://localhost/api/",
IssuerSigningKey = new X509SecurityKey(cert),
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
Authority = env.IsProduction() ? "https://www.wigwam3d.com/api/" : "http://localhost/api/",
Audience = "WebAPI",
RequireHttpsMetadata = env.IsProduction(),
TokenValidationParameters = TokenValidationParameters
});
It also works (and I hope validates token signature also!) but gives me another bug:
UserManager.GetUserAsync(HttpContext.HttpContext.User)
return null, while using UseIdentityServerAuthentication returns me correct User
I think there is no need to add certificate to you API for validation. .UseIdentityServerAuthentication() middleware calls your IdentiyServer to retrieve public key on startup from https://www.example.com/api/.well-known/openid-configuration. At least that's my understanding how it works.
Finally I done it with JwtBearerAuthentication,
GetUserAsync function failure can be fixed with call to:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
because of this issue: https://github.com/aspnet/Security/issues/1043
Any ideas to configure the same using IdentityServer auth are welcome!