This post was written and submitted by Michael Rousos
In several previous posts, I discussed a customer scenario I ran into recently that required issuing bearer tokens from an ASP.NET Core authentication server and then validating those tokens in a separate ASP.NET Core web service which may not have access to the authentication server. The previous posts covered how to setup an authentication server for issuing bearer tokens in ASP.NET Core using libraries like OpenIddict or IdentityServer4. In this post, I’m going to cover the other end of token use on ASP.NET Core – how to validate JWT tokens and use them to authenticate users.
JWT Authentication
The good news is that authenticating with JWT tokens in ASP.NET Core is straightforward. Middleware exists in the Microsoft.AspNetCore.Authentication.JwtBearer package that does most of the work for us!
To test this out, let’s create a new ASP.NET Core web API project. Unlike the web app in my previous post, you don’t need to add any authentication to this web app when creating the project. No identity or user information is managed by the app directly. Instead, it will get all the user information it needs directly from the JWT token that authenticates a caller.
Once the web API is created, decorate some of its actions (like the default Values controller) with [Authorize]
attributes. This will cause ASP.NET Core to only allow calls to the attributed APIs if the user is authenticated and logged in.
To actually support JWT bearer authentication as a means of proving identity, all that’s needed is a call to the UseJwtBearerAuthentication
extension method (from the Microsoft.AspNetCore.Authentication.JwtBearer
package) in the app’s Startup.Configure
method. Because ASP.NET Core middleware executes in the order it is added in Startup
, it’s important that the UseJwtBearerAuthentication
call comes before UseMvc
.
UseJwtBearerAuthentication
takes a JwtBearerOptions
parameter which specifies how to handle incoming tokens. A typical, simple use of UseJwtBearerAuthentication
might look like this:
app.UseJwtBearerAuthentication(new JwtBearerOptions()
{
Audience = "http://localhost:5001/",
Authority = "http://localhost:5000/",
AutomaticAuthenticate = true
});
The parameters in such a usage are:
- Audience represents the intended recipient of the incoming token or the resource that the token grants access to. If the value specified in this parameter doesn’t match the aud parameter in the token, the token will be rejected because it was meant to be used for accessing a different resource. Note that different security token providers have different behaviors regarding what is used as the ‘aud’ claim (some use the URI of a resource a user wants to access, others use scope names). Be sure to use an audience that makes sense given the tokens you plan to accept.
- Authority is the address of the token-issuing authentication server. The JWT bearer authentication middleware will use this URI to find and retrieve the public key that can be used to validate the token’s signature. It will also confirm that the iss parameter in the token matches this URI.
- AutomaticAuthenticate is a boolean value indicating whether or not the user defined by the token should be automatically logged in or not.
- RequireHttpsMetadata is not used in the code snippet above, but is useful for testing purposes. In real-world deployments, JWT bearer tokens should always be passed only over HTTPS.
The scenario I worked on with a customer recently, though, was a little different than this typical JWT scenario. The customer wanted to be able to validate tokens without access to the issuing server. Instead, they wanted to use a public key that was already present locally to validate incoming tokens. Fortunately, UseJWTBearerAuthentication
supports this use-case. It just requires a few adjustments to the parameters passed in.
- First, the
Authority
property should not be set on theJwtBearerOptions
. If it’s set, the middleware assumes that it can go to that URI to get token validation information. In this scenario, the authority URI may not be available. - A new property (
TokenValidationParameters
) must be set on theJwtBearerOptions
. This object allows the caller to specify more advanced options for how JWT tokens will be validated.
There are a number of interesting properties that can be set in a TokenValidationParameters
object, but the ones that matter for this scenario are shown in this updated version of the previous code snippet:
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidIssuer = "http://localhost:5000/",
IssuerSigningKey = new X509SecurityKey(new X509Certificate2(certLocation)),
};
app.UseJwtBearerAuthentication(new JwtBearerOptions()
{
Audience = "http://localhost:5001/",
AutomaticAuthenticate = true,
TokenValidationParameters = tokenValidationParameters
});
ValidateIssuerSigningKey
and ValdiateIssuer
properties indicate that the token’s signature should be validated and that the key’s property indicating it’s issuer must match an expected value. This is an alternate way to make sure the issuer is validated since we’re not using an Authority
parameter in our JwtBearerOptions
(which would have implicitly checked that the JWT’s issuer matched the authority). Instead, the JWT’s issuer is matched against custom values that are provided by the ValidIssuer
or ValidIssuers
properties of the TokenValidationParameters
object.The IssuerSigningKey
is the public key used for validating incoming JWT tokens. By specifying a key here, the token can be validated without any need for the issuing server. What is needed, instead, is the location of the public key. The certLocation
parameter in the sample above is a string pointing to a .cer certificate file containing the public key corresponding to the private key used by the issuing authentication server. Of course, this certificate could just as easily (and more likely) come from a certificate store instead of a file.
In my previous posts on the topic of issuing authentication tokens with ASP.NET Core, it was necessary to generate a certificate to use for token signing. As part of that process, a .cer file was generated which contained the public (but not private) key of the certificate. That certificate is what needs to be made available to apps (like this sample) that will be consuming the generated tokens.
With UseJwtBearerAuthentication
called in Startup.Configure
, our web app should now respect identities sent as JWT bearer tokens in a request’s Authorization header.
Authorizing with Custom Values from JWT
To make the web app consuming tokens a little more interesting, we can also add some custom authorization that only allows access to APIs depending on specific claims in the JWT bearer token.
Role-based Authorization
Authorizing based on roles is available out-of-the-box with ASP.NET Identity. As long as the bearer token used for authentication contains a roles element, ASP.NET Core’s JWT bearer authentication middleware will use that data to populate roles for the user.
So, a roles-based authorization attribute (like [Authorize(Roles = "Manager,Administrator")]
to limit access to managers and admins) can be added to APIs and work immediately.
Custom Authorization Policies
Custom authorization in ASP.NET Core is done through custom authorization requirements and handlers. ASP.NET Core documentation has an excellent write-up on how to use requirements and handlers to customize authorization. For a more in-depth look at ASP.NET Core authorization, check out this ASP.NET Authorization Workshop.
The important thing to know when working with JWT tokens is that in your AuthorizationHandler
‘s HandleRequirementAsync
method, all the elements from the incoming token are available as claims on the AuthorizationHandlerContext.User
. So, to validate that a custom claim is present from the JWT, you might confirm that the element exists in the JWT with a call to context.User.HasClaim
and then confirm that the claim is valid by checking its value.
Again, details on custom authorization policies can be found in ASP.NET Core documentation, but here’s a code snippet demonstrating claim validation in an AuthorizationHandler
that authorizes users based on the (admittedly strange) requirement that their office number claim be lower than some specified value. Notice that it’s necessary to parse the office number claim’s value from a string since (as mentioned in my previous post), ASP.NET Identity stores all claim values as strings.
// A handler that can determine whether a MaximumOfficeNumberRequirement is satisfied
internal class MaximumOfficeNumberAuthorizationHandler : AuthorizationHandler<MaximumOfficeNumberRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MaximumOfficeNumberRequirement requirement)
{
// Bail out if the office number claim isn't present
if (!context.User.HasClaim(c => c.Issuer == "http://localhost:5000/" && c.Type == "office"))
{
return Task.CompletedTask;
}
// Bail out if we can't read an int from the 'office' claim
int officeNumber;
if (!int.TryParse(context.User.FindFirst(c => c.Issuer == "http://localhost:5000/" && c.Type == "office").Value, out officeNumber))
{
return Task.CompletedTask;
}
// Finally, validate that the office number from the claim is not greater
// than the requirement's maximum
if (officeNumber <= requirement.MaximumOfficeNumber)
{
// Mark the requirement as satisfied
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// A custom authorization requirement which requires office number to be below a certain value
internal class MaximumOfficeNumberRequirement : IAuthorizationRequirement
{
public MaximumOfficeNumberRequirement(int officeNumber)
{
MaximumOfficeNumber = officeNumber;
}
public int MaximumOfficeNumber { get; private set; }
}
This authorization requirement can be registered in Startup.ConfigureServices
with a call to AddAuthorization
to add a requirement that an office number not exceed a particular value (200, in this example), and by adding the handler with a call to AddSingleton
:
// Add custom authorization handlers
services.AddAuthorization(options =>
{
options.AddPolicy("OfficeNumberUnder200", policy => policy.Requirements.Add(new MaximumOfficeNumberRequirement(200)));
});
services.AddSingleton<IAuthorizationHandler, MaximumOfficeNumberAuthorizationHandler>();
Finally, this custom authorization policy can protect APIs by decorating actions (or controllers) with appropriate Authorize
attributes with their policy argument set to the name used when defining the custom authorization requirement in startup.cs:
[Authorize(Policy = "OfficeNumberUnder200")]
Testing it All Together
Now that we have a simple web API that can authenticate and authorize based on tokens, we can try out JWT bearer token authentication in ASP.NET Core end-to-end.
The first step is to login with the authentication server we created in my previous post. Once that’s done, copy the token out of the server’s response.
Now, shut down the authentication server just to be sure that our web API can authenticate without it being online.
Then, launch our test web API and using a tool like Postman or Fiddler, create a request to the web API. Initially, the request should fail with a 401 error because the APIs are protected with an [Authorize]
attribute. To make the calls work, add an Authorization header with the value “bearer X” where “X” is the JWT bearer token returned from the authentication server. As long as the token hasn’t expired, its audience and authority match the expected values for this web API, and the user indicated by the token satisfies any custom authorization policies on the action called, a valid response should be served from our web API.
Here are a sample request and response from testing out the sample created in this post:
Request:
GET /api/values/1 HTTP/1.1
Host: localhost:5001
Authorization: bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkU1N0RBRTRBMzU5NDhGODhBQTg2NThFQkExMUZFOUIxMkI5Qzk5NjIiLCJ0eXAiOiJKV1QifQ.eyJ1bmlxdWVfbmFtZSI6IkJvYkBDb250b3NvLmNvbSIsIkFzcE5ldC5JZGVudGl0eS5TZWN1cml0eVN0YW1wIjoiM2M4OWIzZjYtNzE5Ni00NWM2LWE4ZWYtZjlmMzQyN2QxMGYyIiwib2ZmaWNlIjoiMjAiLCJqdGkiOiI0NTZjMzc4Ny00MDQwLTQ2NTMtODYxZi02MWJiM2FkZTdlOTUiLCJ1c2FnZSI6ImFjY2Vzc190b2tlbiIsInNjb3BlIjpbImVtYWlsIiwicHJvZmlsZSIsInJvbGVzIl0sInN1YiI6IjExODBhZjQ4LWU1M2ItNGFhNC1hZmZlLWNmZTZkMjU4YWU2MiIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMS8iLCJuYmYiOjE0Nzc1MDkyNTQsImV4cCI6MTQ3NzUxMTA1NCwiaWF0IjoxNDc3NTA5MjU0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAvIn0.Lmx6A3jhwoyZ8KAIkjriwHIOAYkgXYOf1zBbPbFeIiU2b-2-nxlwAf_yMFx3b1Ouh0Bp7UaPXsPZ9g2S0JLkKD4ukUa1qW6CzIDJHEfe4qwhQSR7xQn5luxSEfLyT_LENVCvOGfdw0VmsUO6XT4wjhBNEArFKMNiqOzBnSnlvX_1VMx1Tdm4AV5iHM9YzmLDMT65_fBeiekxQNPKcXkv3z5tchcu_nVEr1srAk6HpRDLmkbYc6h4S4zo4aPcLeljFrCLpZP-IEikXkKIGD1oohvp2dpXyS_WFby-dl8YQUHTBFHqRHik2wbqTA_gabIeQy-Kon9aheVxyf8x6h2_FA
Response:
HTTP/1.1 200 OK
Date: Thu, 15 Sep 2016 21:53:10 GMT
Transfer-Encoding: chunked
Content-Type: text/plain; charset=utf-8
Server: Kestrel
value
Conclusion
As shown here, authenticating using JWT bearer tokens is straightforward in ASP.NET Core, even in less common scenarios (such as the authentication server not being available). What’s more, ASP.NET Core’s flexible authorization policy makes it easy to have fine-grained control over access to APIs. Combined with my previous posts on issuing bearer tokens, you should have a good overview of how to use this technology for authentication in ASP.NET Core web apps.