In my Asp.net Core based Web API I would like to use jti (JWT ID) claim to prevent replay attacks while working with JWT (JSON Web Tokens).
As suggested I configured JWT Bearer authentication:
services.AddAuthentication(o =>
{
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateTokenReplay = true,
IssuerSigningKeyResolver = (token, securityToken, kid, parameters) =>
{
// Resolving decryption key
},
TokenReplayValidator = (expirationTime, /*string*/ token, validationParameters) =>
{
// I can check expiration time, but there is no way to check jti
},
};
o.SaveToken = true;
});
In replay validator I can access an expiration time, but there is no way to get jti somehow.
I find it a bit confusing. What common might have a replay attack and an expiration time?
Moreover we have another validator:
bool LifetimeValidator(DateTime? notBefore, DateTime? expires,
SecurityToken securityToken,
TokenValidationParameters validationParameters);
Here I can access the SecurityToken data including jti, but I can't believe that this is a right place for jti-based replay attack validation.
So the question is: how could I get SecurityToken data inside TokenReplayValidator?
UPDATE:
After the #orhtej2 comment, I checked the possibility which he suggested. That works indeed:
TokenReplayValidator = (expirationTime, token, validationParameters) =>
{
var handler = o.SecurityTokenValidators.OfType<JwtSecurityTokenHandler>().First();
var securityToken = handler.ReadJwtToken(token);
var jti = securityToken.Id;
// check replay attack
},
Well, it is a solution, but requires additional token parsing (which cannot be good - unnecessary calculations required on the each HTTP call).
I would like to keep the question unanswered until some better solution found.
Related
I'm using the Microsoft.AspNetCore.Authentication.JwtBearer and System.IdentityModel.Tokens.Jwt packages for my .NET Core project.
When configuring the services I'm adding logic to the OnTokenValidated event.
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(jwtBearerOptions =>
{
// ... set TokenValidationParameters ...
jwtBearerOptions.Events = new JwtBearerEvents()
{
OnTokenValidated = (tokenValidatedContext) =>
{
JwtSecurityTokenHandler jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
string tokenWithoutSignature = jwtSecurityTokenHandler.WriteToken(tokenValidatedContext.SecurityToken);
// ... full token from request? ...
}
};
});
Since I know the context only returns me the token without the signature I would like to know how I can
either get the full token with signature
or the signature additionally to add it to the tokenWithoutSignature string
If this is not possible:
I'm generating new tokens this way
public string GenerateAccessToken(IDictionary<string, object> payload)
{
SymmetricSecurityKey symmetricSecurityKey = new SymmetricSecurityKey(Convert.FromBase64String("secret from config"));
SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor
{
Claims = payload,
Expires = DateTime.Now, // value from config
SigningCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256Signature)
};
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
SecurityToken securityToken = tokenHandler.CreateToken(tokenDescriptor);
string token = tokenHandler.WriteToken(securityToken);
return token;
}
Maybe I can either retrieve
the token string without the signature
only the signature
within this method?
If nothing works:
Since I kow a bearer token always contains three parts like
header.payload.signature
I could split the string segments to an array, take the first and second element from the array and create a new string of
firstString + . + secondString
That should give me the token without the signature. Are there any better ideas to cut off the signature from a full token?
Why do I want to achieve this?
This question is based on this one
Security token from TokenValidatedContext from the OnTokenValidated event listener is missing last string segment
I'm working with access and refresh tokens. During validation, I have to compare the token from the request with the token from the database. The token in the database contains the signature too. So I'm facing the same problem as linked above.
That's why I thought about multiple solutions and wrote them down here. If the TokenValidatedContext is not able to return me the signature it seems I have to store the JWT to the database without the signature. And also for this case, I need to separate the signature from the generated JWT.
Without using refresh tokens I only store the maximum session lifetime of a user to the database. The flow is based on this idea
Only store the time of the JWT with the highest lifetime to the database instead of the whole JWT
With using refresh tokens I came up with the following flow. Since you know that the OnTokenValidated handles the validation logic the following logic is additional. I have a database table with
username | access_token | refresh_token | refresh_token_expires_at
and the primary key is a composite key of username + access_token. Refresh tokens are just some random strings generated like so
public string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
and that's why I'm storing an additional expiration date to it. It should be able to expire after some time.
Signing in
Store the generated access and refresh token and its expiration time for a user to the database. Either store the full access token or the access token without signature to the database (depends on the solution of this question).
Hitting a protected endpoint
Check if that access token for that user exists in the database.
Hitting the refresh endpoint
Check if the database refresh token has expired. If not, compare this one with the refresh token from the request. If everything is fine, remove the old access and refresh token from the database and store the new generated access and refresh token to the database.
Signing out
Remove that access and its connected refresh token from the database.
I don't quite understand why you do all this, but if all you need is the original token, you can use one of these:
o.Events = new JwtBearerEvents
{
OnTokenValidated = (context) =>
{
var securityToken = (System.IdentityModel.Tokens.Jwt.JwtSecurityToken)context.SecurityToken;
var token = securityToken.RawData; // "ey...."
var tokenHeader = securityToken.RawHeader; // "ey...."
var tokenPayload = securityToken.RawPayload; // "ey...."
var tokenSignatur = securityToken.RawSignature; // "ey...."
var fullBearerHeader = context.Request.Headers["Authorization"]; // "Bearer ey..."
return Task.CompletedTask;
}
};
You probably want to make the code a bit more safe with regards to type casting etc., but it should give you the token.
Why do you want to manipulate the token? If it is just to Validate the token you can use below code.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
//ValidIssuer = Configuration["Issuer"],
//ValidAudience = Configuration["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Key"]))
};
});
and
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseMvc();
}
We have a security requirement that we must validate the id token we receive from Azure AD B2C. We need to validate these at the minimum
customSecurityLevel, audience, not before and "expiration time", issuer, nonce
Looking asp.net MVC OWIN middleware, I noticed that that OpenIdConnectAuthenicationOptions provides these:
return new OpenIdConnectAuthenticationOptions
{
...
Notifications = new OpenIdConnectAuthenticationNotifications //Specifies events which the OpenIdConnectAuthenticationMiddleware invokes to enable developer control over the authentication process.
{
AuthenticationFailed = this.AuthenticationFailed,
RedirectToIdentityProvider = this.RedirectToIdentityProvider,
AuthorizationCodeReceived = this.OnAuthorizationCodeReceived,
},
TokenValidationParameters = new TokenValidationParameters
{
SaveSigninToken = true, // Important to save the token in boostrapcontext
ValidateAudience = true, // Validate the Audience
ValidateIssuer = true, // Validate the Issuer
ValidateLifetime = true, // Validate the tokens lifetime
ValidIssuer = Issuer, // The issuer to be validated
ValidAudience = ClientId, // The Audience to be validated
},
};
Being quiet new to OWIN, I'm trying to understand below:
Does OWIN middleware magically validates the token we receive from Azure AD B2C or do we need to manually perform validation per this:
https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-webapi-manual-jwt-validation/
At what point in time should token validation should occur i.e. AuthorizationCodeReceived event or on redirect controller/action (page) that's configured on Azure AD B2C redirect URL?
We need to validate more attributes that TokenValidationParameters supports e.g. customSecurityAttribute we send on initial payload. Is there a way to extend this?
How do we parse the token that we receive from Azure AD B2C using OWIN?
Any code sample would be handy.
TO make your question simpler.
The idea behind token is to parse the token and get 3 parts from the token
-Header : contain information about in which algorithm the token haven been encrypted
-Payload : information about the user
-Signature: it's the calculation of encryption of ( Header + Payload) using the Azure certificate or( your identity provider).
Next step the user sends request to your back-end along with JWT.
your back-end will parse the token and get certificate type then will preform HTTP request to your identity provider to get certificate
Next your back-end will construct the certificate option and try to do encryption for ( header + Payload) came from your token the output string must be exactly same Signature you received in the token from your front-end.
If every thing is okay
now your back-end will start validating other attributes like Audience, Issuer
if you configure your token to validate the Audience means your front-end required to provide token contain Audience(Application ID) exactly same your back-end as well as for issuer.
the question now how my back-end knows about certificate?
Azure AD using OpenID connect, More information here
since you configred you tenant in backend the auth packge will make a call to https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration to get the details about your identity provider
and one important link as will is ("jwks_uri": "https://login.microsoftonline.com/common/discovery/keys") where the signature hosted.
You can read and search more how to validate certificate JWT and check this
https://codereview.stackexchange.com/questions/70005/authentication-with-jwt
moving to part 2 of validate more attributes.
Since you are using OpenIdConnect, the package has class called OpenIdConnectEvents that you can trigger events and do what ever you want like this
.AddOpenIdConnect(o =>
{
//Additional config snipped
o.Events = new OpenIdConnectEvents
{
OnTokenValidated = async ctx =>
{
//Get user's immutable object id from claims that came from Azure AD
string oid = ctx.Principal.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier");
//Get EF context
var db = ctx.HttpContext.RequestServices.GetRequiredService<AuthorizationDbContext>();
//Check is user a super admin
bool isSuperAdmin = await db.SuperAdmins.AnyAsync(a => a.ObjectId == oid);
if (isSuperAdmin)
{
//Add claim if they are
var claims = new List<Claim>
{
new Claim(ClaimTypes.Role, "superadmin")
};
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
}
}
};
});
moving to part 3
parsing token in javascript
function parseJwt (token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(window.atob(base64));
};
parsing token in C#
use this libary https://www.jsonwebtoken.io/
try {
string jsonPayload = JWT.JsonWebToken.Decode(token, secretKey);
Console.WriteLine(jsonPayload);
} catch (JWT.SignatureVerificationException) {
Console.WriteLine("Invalid token!");
}
or manual
var jwtHandler = new JwtSecurityTokenHandler();
var jwtInput = txtJwtIn.Text;
//Check if readable token (string is in a JWT format)
var readableToken = jwtHandler.CanReadToken(jwtInput);
if(readableToken != true)
{
txtJwtOut.Text = "The token doesn't seem to be in a proper JWT format.";
}
if(readableToken == true)
{
var token = jwtHandler.ReadJwtToken(jwtInput);
//Extract the headers of the JWT
var headers = token.Header;
var jwtHeader = "{";
foreach(var h in headers)
{
jwtHeader += '"' + h.Key + "\":\"" + h.Value + "\",";
}
jwtHeader += "}";
txtJwtOut.Text = "Header:\r\n" + JToken.Parse(jwtHeader).ToString(Formatting.Indented);
//Extract the payload of the JWT
var claims = token.Claims;
var jwtPayload = "{";
foreach(Claim c in claims)
{
jwtPayload += '"' + c.Type + "\":\"" + c.Value + "\",";
}
jwtPayload += "}";
txtJwtOut.Text += "\r\nPayload:\r\n" + JToken.Parse(jwtPayload).ToString(Formatting.Indented);
}
I hope that answer your qustions
I have an Asp Net Core API without any controller implementation. Client (Auth0 implementation) is passing a JWT token (RS256 alg) that I need to verify if signature is valid or not. I have gone through the Auth0 official documentation that suggest to implement JwtBearer and set the app to UseAuthentication in startup configuration
Microsoft.AspNetCore.Authentication.JwtBearer
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// 1. Add Authentication Services
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = "https://myapi.auth0.com/";
options.Audience = "API/Endpoint";
});
}
As mentioned above, there is no controller in this API, I can't decorate the method with Authorize attrubute so I am left with the option of verifying this signature manually. For this reason, I have been through stack overflow posts where people have mentioned different approaches such as the use of
System.IdentityModel.Tokens.Jwt
while other have opposed to it and suggested to use low level implementation etc. I have tried couple but no success so far.
Let's say following method is the entry point of the API that receives the JWT token. Would please someone tell me what I need to do in order to verify the signature manually
public Task InvokeAsync(HttpContext context)
{
var accessToken = context.Request.Headers["Authorization"];
// Here I wan't to verify the signature?
// This token has RS256 alg
}
Following is the JWT decoded result
You could do something like this:
public Task InvokeAsync(HttpContext context)
{
var accessToken = context.Request.Headers["Authorization"];
var secretKey = "Insert your secret key here";
var validationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true;
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
// Add any other validations: issuer, audience, lifetime, etc
}
var handler = new JwtSecurityTokenHandler();
var principal = handler.ValidateToken(accessToken, validationParameters, out var validToken);
JwtSecurityToken validJwt = validToken as JwtSecurityToken;
if (validJwt == null)
{
throw new ArgumentException("Invalid JWT");
}
if (!validJwt.Header.Alg.Equals(SecurityAlgorithms.RsaSha256Signature, StringComparison.Ordinal))
{
throw new ArgumentException("Algorithm must be RS256");
}
// Add any validations which cannot be included into TokenValidationParameters
// Validation passed, continue with your logic
}
It is based on this article, which explains how to validate jwt tokens received through cookies. Although the objective is different from yours, the way of validating tokens can be applied to your problem.
I'm using ASP.NET Core 2.1 and Auth0.
When I try to retrieve the acces_token to access my own API I use
string accessToken = await HttpContext.GetTokenAsync("access_token");
The strange thing is when I paste the token on https://jwt.io/ it shows that an audience has been added. The thing is that two audiences are not allowed and so the token is not valid. The audience that is added ends with /userinfo
Can someone please explain why there are two audiences in my acces token?
I use the following code in ConfigureServices
// Add authentication services
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("Auth0", options =>
{
// Set the authority to your Auth0 domain
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
// Configure the Auth0 Client ID and Client Secret
options.ClientId = Configuration["Auth0:ClientId"];
options.ClientSecret = Configuration["Auth0:ClientSecret"];
// Set response type to code
options.ResponseType = "code";
// Configure the scope
options.Scope.Clear();
options.Scope.Add("openid");
// Set the callback path, so Auth0 will call back to http://localhost:5000/signin-auth0
// Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard
options.CallbackPath = new PathString("/signin-auth0");
// Configure the Claims Issuer to be Auth0
options.ClaimsIssuer = "Auth0";
// Saves tokens to the AuthenticationProperties
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents
{
// handle the logout redirection
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
// transform to absolute
var request = context.Request;
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
}
logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
}
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
},
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("audience", "MY_OWN_AUDIENCE_URL");
return Task.FromResult(0);
}
};
});
Can someone please explain why there are two audiences in my acces token?
The second audience is the userinfo endpoint. The userinfo endpoint is part of the OpenID Connect protocol; it exposes the end-user's profile information and is present because of the openid scope.
When Auth0 receives an authorization request, it checks the request's audience and scope parameters. If the audience is a custom API, and if the scope includes openid, then the access_token will include two audiences: one for your custom API, the other for the Auth0 userinfo endpoint.
Here is a supporting quote from https://auth0.com/docs/tokens/access-token
When the audience is set to a custom API and the scope parameter includes the openid value, then the generated Access Token will be a JWT valid for both retrieving the user's profile and for accessing the custom API. The aud claim of this JWT will include two values: YOUR_AUTH0_DOMAIN/userinfo and your custom API's unique identifier.
WORKING
I got it working with the next code placed in ConfigureServices in the Startup class. In the List from Configuration I placed the audience from Auth0 userinfo API and my own API.
// Multiple audiences
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudiences = Configuration.GetSection("Auth0:Audiences").Get<List<string>>(),
ValidateLifetime = true
};
Is it possible to support multiple JWT Token issuers in ASP.NET Core 2?
I want to provide an API for external service and I need to use two sources of JWT tokens - Firebase and custom JWT token issuers. In ASP.NET core I can set the JWT authentication for Bearer auth scheme, but only for one Authority:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://securetoken.google.com/my-firebase-project"
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "my-firebase-project"
ValidateAudience = true,
ValidAudience = "my-firebase-project"
ValidateLifetime = true
};
}
I can have multiple issuers and audiences, but I can't set several Authorities.
You can totally achieve what you want:
services
.AddAuthentication()
.AddJwtBearer("Firebase", options =>
{
options.Authority = "https://securetoken.google.com/my-firebase-project"
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "my-firebase-project"
ValidateAudience = true,
ValidAudience = "my-firebase-project"
ValidateLifetime = true
};
})
.AddJwtBearer("Custom", options =>
{
// Configuration for your custom
// JWT tokens here
});
services
.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Firebase", "Custom")
.Build();
});
Let's go through the differences between your code and that one.
AddAuthentication has no parameter
If you set a default authentication scheme, then on every single request the authentication middleware will try to run the authentication handler associated with the default authentication scheme. Since we now have two possible authentication schemes, there's no point in running one of them.
Use another overload of AddJwtBearer
Every single AddXXX method to add an authentication has several overloads:
One where the default authentication scheme associated with the authentication method is used, as you can see here for cookies authentication
One where you pass, in addition to the configuration of the options, the name of the authentication scheme, as on this overload
Now, because you use the same authentication method twice but authentication schemes must be unique, you need to use the second overload.
Update the default policy
Since the requests won't be authenticated automatically anymore, putting [Authorize] attributes on some actions will result in the requests being rejected and an HTTP 401 will be issued.
Since that's not what we want because we want to give the authentication handlers a chance to authenticate the request, we change the default policy of the authorization system by indicating both the Firebase and Custom authentication schemes should be tried to authenticate the request.
That doesn't prevent you from being more restrictive on some actions; the [Authorize] attribute has an AuthenticationSchemes property that allows you to override which authentication schemes are valid.
If you have more complex scenarios, you can make use of policy-based authorization. I find the official documentation is great.
Let's imagine some actions are only available to JWT tokens issued by Firebase and must have a claim with a specific value; you could do it this way:
// Authentication code omitted for brevity
services
.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Firebase", "Custom")
.Build();
options.AddPolicy("FirebaseAdministrators", new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Firebase")
.RequireClaim("role", "admin")
.Build());
});
You could then use [Authorize(Policy = "FirebaseAdministrators")] on some actions.
A final point to note: If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed. Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
This is an extension of Mickaƫl Derriey's answer.
Our app has a custom authorization requirement that we resolve from an internal source. We were using Auth0 but are switching to Microsoft Account authentication using OpenID. Here is the slightly edited code from our ASP.Net Core 2.1 Startup. For future readers, this works as of this writing for the versions specified. The caller uses the id_token from OpenID on incoming requests passed as a Bearer token. Hope it helps someone else trying to do an identity authority conversion as much as this question and answer helped me.
const string Auth0 = nameof(Auth0);
const string MsaOpenId = nameof(MsaOpenId);
string domain = "https://myAuth0App.auth0.com/";
services.AddAuthentication()
.AddJwtBearer(Auth0, options =>
{
options.Authority = domain;
options.Audience = "https://myAuth0Audience.com";
})
.AddJwtBearer(MsaOpenId, options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = "00000000-0000-0000-0000-000000000000",
ValidateIssuer = true,
ValidIssuer = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0",
ValidateIssuerSigningKey = true,
RequireExpirationTime = true,
ValidateLifetime = true,
RequireSignedTokens = true,
ClockSkew = TimeSpan.FromMinutes(10),
};
options.MetadataAddress = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0/.well-known/openid-configuration";
}
);
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes( Auth0, MsaOpenId )
.Build();
var approvedPolicyBuilder = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(Auth0, MsaOpenId)
;
approvedPolicyBuilder.Requirements.Add(new HasApprovedRequirement(domain));
options.AddPolicy("approved", approvedPolicyBuilder.Build());
});
The solution to your question, is available in following blog post https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
Basically the solutions exists of overriding the regular JWTBearer handler with you own generic handler that can check through the JWTBearerConfig if the issuer in the cfg is the same to the isseur in your token.
The blog post suggests to use seperate handlers for each scheme, that doesn't seem to be needed, a generic class JWTAuthenticationHandler that overrides the HandleAuthenticateAsync method seems to suffice!
Code wise you could implement your startup like this:
//Using multiple schemes can cause issues when validating the issuesSigningKey therefore we need to implement seperate handlers for each scheme! => cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
services.AddAuthentication()
//Set the authenticationScheme by using the identityServer helper methods (we are using a Bearer token)
.AddScheme<JwtBearerOptions, JWTAuthenticationHandler>(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
{
//TO DO Get the origin url's from configuration file, instead of setting all url's here
options.Authority = _identityServerSettings.Authority;
options.Audience = _identityServerSettings.Audience;
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
return Task.CompletedTask;
},
//When using multiple JwtBearer schemes we can run into "OnAuthenticationFailed" for instance when logging in via IdentityServer the AuthenticationHandler will still check in these events, this can be ignored...
//Cfr => https://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication
//If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed.Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
//We managed to fix this issue by adding seperate AuthenticationHandlers for each type of bearer token... cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
OnAuthenticationFailed = context =>
{
return Task.CompletedTask;
},
OnMessageReceived = context =>
{
return Task.CompletedTask;
},
OnForbidden = context =>
{
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
}
};
})
//Set the authentication scheme for the AzureAd integration (we are using a bearer token)
.AddScheme<JwtBearerOptions, JWTAuthenticationHandler>("AzureAD", "AzureAD", options =>
{
options.Audience = _azureAdSettings.Audience; //ClientId
options.Authority = _azureAdSettings.Authority; //"https://login.microsoftonline.com/{tenantId}/v2.0/"
options.TokenValidationParameters = new TokenValidationParameters
{
//Set built in claimTypes => Role
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
};
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
return Task.CompletedTask;
},
//When using multiple JwtBearer schemes we can run into "OnAuthenticationFailed" for instance when logging in via IdentityServer the AuthenticationHandler will still check in these events, this can be ignored...
//Cfr => https://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication
//A final point to note: If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed.Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
//We managed to fix this issue by adding seperate AuthenticationHandlers for each type of bearer token... cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
OnAuthenticationFailed = context =>
{
return Task.CompletedTask;
},
OnMessageReceived = context =>
{
return Task.CompletedTask;
},
OnForbidden = context =>
{
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
}
};
});
}
The JWTAuthenticationHandlerClass can look like this
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace WebAPI.Auth
{
public class JWTAuthenticationHandler: JwtBearerHandler
{
public JWTAuthenticationHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{ }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//Fetch OIDC configuration for the IDP we are handling
var authorityConfig = await this.Options.ConfigurationManager.GetConfigurationAsync(this.Context.RequestAborted);
//Determine the issuer from the configuration
var authorityIssuer = authorityConfig.Issuer;
var jwtToken = this.ReadTokenFromHeader();
var jwtHandler = new JwtSecurityTokenHandler();
//Check if we can read the token as a valid JWT, if not let the JwtBearerHandler do it's thing...
if (jwtHandler.CanReadToken(jwtToken))
{
//Read the token and determine if the issuer in config is the same as the one in the token, if this is true we know we want to let the JwtBearerHandler continue, if not we skip and return noResult
//This way the next IDP configuration will pass here until we find a matching issuer and then we know that is the IDP we are dealing with
var token = jwtHandler.ReadJwtToken(jwtToken);
if (string.Equals(token.Issuer, authorityIssuer, StringComparison.OrdinalIgnoreCase))
{
return await base.HandleAuthenticateAsync();
}
else
{
// return NoResult since the issuer in cfg did not match the one in the token, so no need to proceed to tokenValidation
this.Logger.LogDebug($"Skipping jwt token validation because token issuer was {token.Issuer} but the authority issuer is: {authorityIssuer}");
return AuthenticateResult.NoResult();
}
}
return await base.HandleAuthenticateAsync();
}
//Fetch the bearer token from the authorization header on the request!
private string ReadTokenFromHeader()
{
string token = null;
string authorization = Request.Headers["Authorization"];
//If we don't find the authorization header return null
if (string.IsNullOrEmpty(authorization))
{
return null;
}
//get the token from the auth header
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("Bearer ".Length).Trim();
}
return token;
}
}
}
One thing that was missing in Mickael's answer is that scheme needs to be specified in Authorize attribute (If you want to use authorization)
[Authorize(AuthenticationSchemes = "Firebase,Custom", Policy ="FirebaseAdministrators")]
Without AuthenticationSchemes provided, and AddAuthentication() has no parameter, NetCore fails to Authenticate and Request.HttpContext.User.Identity.IsAuthenticated is set to false