1
votes

I'm having an issue with how .NET Core is handling JSON Web Token (JWT) authentication in my production environment. I am testing using Postman. If I make a call to my API with a valid token it works fine and returns the expected response from the API endpoint. However, if the token is expired I'm not getting the expected HTTP 401. In fact I'm not getting any response, it just fails quietly. It seems to be something about my production configuration that isn't working. If I test this in my local development environment I don't have any problems and expired tokens get a 401 not authorized HTTP response like expected.

My web app uses both cookies authentication for those signing in via the web site and JWT authentication for those connecting to the API. Here is the configuration for JWT authentication. This is in the Startup.cs. This is the JWT configuration part of the services.Authentication. The cookies authentication is set-up first and is the default authentication scheme.

.AddJwtBearer(options =>{
options.RequireHttpsMetadata = !_hostingEnvironment.IsDevelopment();

options.TokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuer = true,
    ValidateAudience = true,
    ValidateLifetime = true,
    ValidateIssuerSigningKey = true,

    ValidIssuer = Settings.Api.JwtBearer.TokenValidation.ValidIssuer,
    ValidAudience = Settings.Api.JwtBearer.TokenValidation.ValidAudience,
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Settings.Api.JwtBearer.TokenValidation.IssuerSigningKey))
};

options.Events = new JwtBearerEvents()
{
    OnAuthenticationFailed = context =>
    {
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        context.Response.ContentType = "application/json; charset=utf-8";
        var message = _hostingEnvironment.IsDevelopment() ? context.Exception.ToString() : "An error occurred processing your authentication.";
        var result = JsonConvert.SerializeObject(new { message });
        return context.Response.WriteAsync(result);
    }
};});

And here is the other relevant configuration in Startup.cs:

public void Configure(IApplicationBuilder app, IHostingEnvironment env){
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error/Exception");

    app.UseStatusCodePagesWithReExecute("/Error/{0}");

    app.UseHsts();

    app.UseHttpsRedirection();
}

app.UseStaticFiles();

app.UseCors(Constants.Security.Cors.PolicyName);

app.UseAuthentication();

app.UseMvc(ConfigureRoutes);}

` I've played with the Configure set up a lot and tested in production, but I always end up getting no response when token authentication fails. Since it works fine in my local development environment it must mean there is something wrong with my production configuration, but I'm stumped and out of ideas. For some more information here is an example of the error from the error logs:

info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[1] Failed to validate the token. Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed. The token is expired. ValidTo: '[PII is hidden]', Current time: '[PII is hidden]'. at Microsoft.IdentityModel.Tokens.Validators.ValidateLifetime(Nullable1 notBefore, Nullable1 expires, SecurityToken securityToken, TokenValidationParameters validationParameters) at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateLifetime(Nullable1 notBefore, Nullable1 expires, 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.d__6.MoveNext() info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[7] Bearer was not authenticated. Failure message: IDX10223: Lifetime validation failed. The token is expired. ValidTo: '[PII is hidden]', Current time: '[PII is hidden]'. info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] Authorization failed. info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3] Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'. info: Microsoft.AspNetCore.Mvc.ChallengeResult[1] Executing ChallengeResult with authentication schemes (Bearer). info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] Executed action Web.Controllers.ApiController.Users (Web) in 109.4625ms info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1] Executed endpoint 'Web.Controllers.ApiController.Users (Web)' fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1] An unhandled exception has occurred while executing the request. System.InvalidOperationException: StatusCode cannot be set because the response has already started. at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value) at Microsoft.AspNetCore.Http.Internal.DefaultHttpResponse.set_StatusCode(Int32 value) at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.d__7.MoveNext()

Thanks in advance for any ideas about how to fix this.

1
Here is your error: fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1] An unhandled exception has occurred while executing the request. System.InvalidOperationException: StatusCode cannot be set because the response has already started.Anton Toshik
I only had this occur when overriding the status code of a response, in other words when putting the response through middle ware and that middle ware changed the status code of the response you get this error. Hopefully this will help you find your issue.Anton Toshik
Yes that appears to be the issue. However, I'm still left with the issue of how to return a custom error message with the 401.Scribbler

1 Answers

5
votes

So I've found a partial solution that at least unblocks me so I can continue with my API development. Attempting to alter the context.Response in the JwtBearerEvents OnAuthenticationFailed does not work in my production environment (although it work fine on my local machine). I tried a lot of variations, but in the end the only thing that worked was to delete this entire block of code from Startup.cs:

options.Events = new JwtBearerEvents()
{
    OnAuthenticationFailed = context =>
    {
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        context.Response.ContentType = "application/json; charset=utf-8";
        var message = _hostingEnvironment.IsDevelopment() ? context.Exception.ToString() : "An error occurred processing your authentication.";
        var result = JsonConvert.SerializeObject(new { message });
        return context.Response.WriteAsync(result);
    }
};

Once I did that I started getting 401 codes for failed token validation when testing in Postman which is a good thing and the expected behavior. However, in the body of the response the default error page HTML was getting returned as well.

To deal with that I had to modify the Configure method in Startup.cs so that UseStatusCodePagesWithReExecute ignores the API:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error/Exception");

        app.UseWhen(context => !context.Request.Path.Value.StartsWith("/api"), builder =>
        {
            builder.UseStatusCodePagesWithReExecute("/Error/{0}");
        });

        app.UseHsts();

        app.UseHttpsRedirection();
    }

    app.UseStaticFiles();

    app.UseCors(Constants.Security.Cors.PolicyName);

    app.UseAuthentication();

    app.UseMvc(ConfigureRoutes);
}

This works, but I don't love it. Feels like a hack. Now I get the 401 for failed token authentication and nothing in the body. Like I mentioned before this unblocks me, but I would still like to return a custom error message with the Response.

So, my next question would be if I can't use the JwtBearerEvents OnAuthenticationFailed to add a message, then where would be the right place to add the error message?