I have implemented an api which supports jwt token authorization with the following code:
services.AddAuthentication(bearerScheme)
.AddJwtBearer(bearerScheme, config =>
{
config.Authority = authority;
config.Audience = resourceId;
I have also implemented a custom attribute which provides further permission based validation that comes along with its own AuthorizationPolicyProvider and AuthorizationHandler. Sending a valid token works with no issues. The default JwtSecurityTokenHandler fires and validates the token before my customer handler fires and uses the token to validate the user permissions. Sending an expired token however doesn't work as I'd expect. What seems to happen is that JwtSecurityTokenHandler fires and rejects the token with the following error - as expected
IDX10223: Lifetime validation failed. The token is expired.
What I don't understand is why my AuthorizationHandler is then fired which then uses the invalid access token to contact another api and fails because the token is invalid. Ideally, I'd like to either:
- Have the jwt validation stop and return a 401 error because the token failed, or
- Somehow be able to get the result of the jwt validation in my handler and throw the error myself
Does anybody know how I might do this? I'd quite like to understand why an attribute on a controller method is also being handled when authentication has failed as it seems odd to me to continue after failing authentication?
CodePudding user response:
Short answer
Most likely, the config on .AddJwtBearer()
somehow have final setting on (JwtBearerOptions).TokenValidationParameters.ValidateLifetime
was set to false
.
An other possibility was system clock on server have some problem (which rarely happen).
To make it fail right at Authentication
step, try either
.AddJwtBearer(options =>
{
//... Some other config
options.TokenValidationParameters.ValidateLifetime = true;
//... In case options.TokenValidationParameters.ValidateLifetime still got override somehow, do this
options.TokenValidationParameters.LifetimeValidator = (nbf, exp, _, _) => nbf < DateTime.UtcNow && exp > DateTime.UtcNow;
}
)
Under the hood
We got to start at AuthenticationMiddleware. Schemes.GetRequestHandlerSchemesAsync()
won't take back anything to iterate (furthur investigate at IAuthenticationRequestHandler
vs IAuthenticationHandler
if there was some curious).
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
would return the default authentication scheme, which we all know. Then it call context.AuthenticateAsync()
, which is an extension here, make use of AuthenticationService. Now AuthenticationHandler
will be served as classic. Which will do nothing than transform a jwt token to ClaimPrincipal
object. If it doesn't fail fast at this process step, it would flow down to AuthorizationMiddleware
, which would definitely evaluate our policy (aka, AuthorizationHandler
code block would fired no matter what).
So, everything we got to do now is to make the transform process from jwt back to ClaimPrincipal
object to be fail. That way, AuthorizationHandler
would not fire, which can be accomplished up Short answer section.
CodePudding user response:
In ASP.NET Core, successful authentication isn't by default a requirement for authorization, because they serve different purposes:
Authentication
: restore user's identitiesAuthorization
: decide whether to grant access
In the request pipeline (if this is where the authorization is triggered), authorization middleware attempts to restore the user's identities using the given authentication schemes on the authorization policy (if any). If none of the authentication schemes succeeded, the authentication result is simply NO RESULT. In this case, the authorization policies will be evaluated against an unauthenticated user.
If you're enforcing an authenticated user, you will need to explicitly specify it when constructing the policy by using RequireAuthenticatedUser
:
new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
You can also check the User.Identity.IsAuthenticated
to determine if the user is authenticated in your authorization handling code and only make a call to your API for authenticated scenarios.
You also have the choice to either ignore unauthenticated users and let other authorization handlers to decide if the "anonymous user" should have access to resource based on other factors, or fail it loudly by returning a failed authorization result from your code.
If you're interested in the source code, have a look at PolicyEvaluator
.