I am trying to add multiple ways to authenticate users in our app. All but one are working flawlessly. I am able to log in with ASP.NET Core Identity, GitHub, Azure AD, and even API auth, but JWT is giving me a bit of a headache as I always get a 401 response when I pass in an bearer token to the authorization header.
This might have something to do with a custom middleware class that works on this header:
app.UseMiddleware<JwtAuthMiddleware>()
.UseAuthentication()
.UseAuthorization()
The middleware class in question:
public class JwtAuthMiddleware
{
private readonly RequestDelegate _next;
public JwtAuthMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext context)
{
string authHeader = context.Request.Headers["Authorization"];
if (authHeader != null)
{
string jwtEncodedString = authHeader[7..]; // Fetch token without Bearer
JwtSecurityToken token = new(jwtEncodedString: jwtEncodedString);
ClaimsIdentity identity = new(token.Claims, "jwt");
context.User = new ClaimsPrincipal(identity);
}
return _next(context);
}
}
The identity setup is fairly simple.
services
.AddAuthentication()
.AddCookie(/*config*/)
.AddGitHub(/*config*/)
.AddJwtBearer(/*config*/)
.AddAzureAd(/*config*/);
string[] authSchemes = new string[]
{
IdentityConstants.ApplicationScheme,
CookieAuthenticationDefaults.AuthenticationScheme,
GitHubAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme,
"InHeader"
};
AuthorizationPolicy authnPolicy = new AuthorizationPolicyBuilder(authSchemes)
.RequireAuthenticatedUser()
.Build();
services.AddAuthorization(options =>
{
options.FallbackPolicy = authnPolicy;
});
I also set a filter in AddMvc:
.AddMvc(options =>
{
AuthorizationPolicy policy = new AuthorizationPolicyBuilder(authSchemes)
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
JWT works when options.FallbackPolicy
is not set. Otherwise, I get a 401 response. Inspecting the DenyAnonymousAuthorizationRequirement
in debug mode confirms this as there is no principal set with the right claims filled out. So it looks like context.User
is ignored or reset.
Ideally, I would want to get rid of JwtAuthMiddleware
altogether, but I still have to figure out how to combine JWT with cookie authentication in this particular setup. Any thoughts?
@Shazad Hassan made some good points in his answer. The custom middleware was in fact not necessary in the end because the System.IdentityModel.Tokens.XXXX
assemblies were throwing errors that the tokens were invalid. I never really knew the token was invalid because the middleware assumed it was correct (and content-wise, it was), while all this time the context was running for an unauthenticated user, which became apparent when I applied the fallback policy.
So I rewrote the token generation code and now I no longer get this issue, and I can safely remove the custom middleware. So far so good.
While I still have to bone up on authentication schemes and all that, for the moment it doesn't even seem necessary to have multiple calls to AddAuthentication
because this works:
services
.AddAuthentication()
.AddCookie(configureCookieAuthOptions)
.AddGitHub(configureGithub)
.AddAzureAd(config)
.AddJwtBearer(configureJwtBearerOptions)
.AddApiKeyInHeader<ApiKeyProvider>("InHeader", options =>
{
options.Realm = "My realm";
options.KeyName = "X-API-KEY";
});
With this, I am able to log in with every method in the list. Looks promising, but might need to be reviewed by someone who knows the ins and outs of authentication in ASP.NET Core.