JWT Validation and Authorization in ASP.NET Core

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.

Although this post focuses on .NET Core scenarios, there are also many options for using and validating bearer tokens in the .NET Framework, including the code shown here (which works on both .NET Core and the .NET Framework) and Azure Active Directory packages like Microsoft.Owin.Security.ActiveDirectory, which are covered in detail in Azure documentation.

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.

  1. First, the Authority property should not be set on the JwtBearerOptions. 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.
  2. A new property (TokenValidationParameters) must be set on the JwtBearerOptions. 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
});
The 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.

Resources

Tags

Join the conversation

Add Comment

  1. Hi,

    Could you provide a link to download the source code for the tutorial please? Thanks

  2. I also would like to have access to the source code, please!