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(Nullable
1 notBefore, Nullable
1 expires, SecurityToken securityToken, TokenValidationParameters validationParameters) at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateLifetime(Nullable1 notBefore, Nullable
1 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.