blog post image
Andrew Lock avatar

Andrew Lock

~9 min read

A look behind the JWT bearer authentication middleware in ASP.NET Core

This is the next in a series of posts about Authentication and Authorisation in ASP.NET Core. In the first post we had a general introduction to authentication in ASP.NET Core, and then in the previous post we looked in more depth at the cookie middleware, to try and get to grips with the process under the hood of authenticating a request.

In this post, we take a look at another middleware, the JwtBearerAuthenticationMiddleware, again looking at how it is implemented in ASP.NET Core as a means to understanding authentication in the framework in general.

What is Bearer Authentication?

The first concept to understand is Bearer authentication itself, which uses bearer tokens. According to the specification, a bearer token is:

A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can. Using a bearer token does not require a bearer to prove possession of cryptographic key material (proof-of-possession).

In other words, by presenting a valid token you will be automatically authenticated, without having to match or present any additional signature or details to prove it was granted to you. It is often used in the OAuth 2.0 authorisation framework, such as you might use when signing in to a third-party site using your Google or Facebook accounts for example.

In practice, a bearer token is usually presented to the remote server using the HTTP Authorization header:

Authorization: Bearer BEARER_TOKEN

where BEARER_TOKEN is the actual token. An important point to bear in mind is that bearer tokens entitle whoever is in it's possession to access the resource it protects. That means you must be sure to only use tokens over SSL/TLS to ensure they cannot be intercepted and stolen.

What is a JWT?

A JSON Web Token (JWT) is a web standard that defines a method for transferring claims as a JSON object in such a way that they can be cryptographically signed or encrypted. It is used extensively in the internet today, in particular in many OAuth 2 implementations.

JWTs consist of 3 parts:

  1. Header: A JSON object which indicates the type of the token (JWT) and the algorithm used to sign it
  2. Payload: A JSON object with the asserted Claims of the entity
  3. Signature: A string created using a secret and the combined header and payload. Used to verify the token has not been tampered with.

These are then base64Url encoded and separated with a .. Using JSON Web Tokens allows you to send claims in a relatively compact way, and to protect them against modification using the signature. One of their main advantages is that they can allow stateless applications by including the storing the required claims in the token, rather than server side in a session store.

I won't go into all the details of JWT tokens, or the OAuth framework here, as that is a huge topic on it's own. In this post I'm more interested in how the middleware and handlers interact with ASP.NET Core authentication framework. If you want to find out more about JSON web tokens, I recommend you check out jwt.io and auth0.com as they have some great information and tutorials.

Just to give a vague idea of what JSON Web Tokens looks like in practice, the payload and header given below:

{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "name": "Andrew Lock"
}

could be encoded in the following header:

Authorisation: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQW5kcmV3IExvY2sifQ.RJJq5u9ITuNGeQmWEA4S8nnzORCpKJ2FXUthuCuCo0I

JWT bearer authentication in ASP.NET Core

You can add JWT bearer authentication to your ASP.NET Core application using the Microsoft.AspNetCore.Authentication.JwtBearer package. This provides middleware to allow validating and extracting JWT bearer tokens from a header. There is currently no built-in mechanism for generating the tokens from your application, but if you need that functionality, there are a number of possible projects and solutions to enable that such as IdentityServer 4. Alternatively, you could create your own token middleware as is shown in this post.

Once you have added the package to your project.json, you need to add the middleware to your Startup class. This will allow you to validate the token and, if valid, create a ClaimsPrinciple from the claims it contains.

You can add the middleware to your application using the UseJwtBearerAuthentication extension method in your Startup.Configure method, passing in a JwtBearerOptions object:

app.UseJwtBearerAuthentication(new JwtBearerOptions
{
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = "https://issuer.example.com",

        ValidateAudience = true,
        ValidAudience = "https://yourapplication.example.com",

        ValidateLifetime = true,
    }
});

There are many options available on the JwtBearerOptions - we'll cover some of these in more detail later.

The JwtBearerMiddleware

In the previous post we saw that the CookieAuthenticationMiddleware inherits from the base AuthenticationMiddleware<T>, and the JwtBearerMiddleware is no different. When created, the middleware performs various precondition checks, and initialises some default values. The most important check is to initialise the ConfigurationManager, if it has not already been set.

The ConfigurationManager object is responsible for retrieving, refreshing and caching the configuration metadata required to validate JWTs, such as the issuer and signing keys. These can either be provided directly to the ConfigurationManager by configuring the JwtBearerOptions.Configuration property, or by using a back channel to fetch the required metadata from a remote endpoint. The details of this configuration is outside the scope of this article.

As in the cookie middleware, the middleware implements the only required method from the base class, CreateHandler(), and returns a newly instantiated JwtBearerHandler.

The JwtBearerHandler HandleAuthenticateAsync method

Again, as with the cookie authentication middleware, the handler is where all the work really takes place. JwtBearerHandler derives from AuthenticationHandler<JwtBearerOptions>, overriding the required HandleAuthenticateAsync() method.

This method is responsible for deserialising the JSON Web Token, validating it, and creating an appropriate AuthenticateResult with an AuthenticationTicket (if the validation was successful). We'll walk through the bulk of it in this section, but it is pretty long, so I'll gloss over some of it!

On MessageReceived

The first section of the HandleAuthenticateAsync method allows you to customise the whole bearer authentication method.

// Give application opportunity to find from a different location, adjust, or reject token
var messageReceivedContext = new MessageReceivedContext(Context, Options);

// event can set the token
await Options.Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.CheckEventResult(out result))
{
    return result;
}

// If application retrieved token from somewhere else, use that.
token = messageReceivedContext.Token;

This section calls out to the MessageReceived event handler on the JwtBearerOptions object. You are provided the full HttpContext, as well as the JwtBearerOptions object itself. This allows you a great deal of flexibility in how your applications uses tokens. You could validate the token yourself, using any other side information you may require, and set the AuthenticateResult explicitly. If you take this approach and handle the authentication yourself, the method will just directly return the AuthenticateResult after the call to messageReceivedContext.CheckEventResult.

Alternatively, you could obtain the token from somewhere else, such as a different header, or even a cookie. In that case, the handler will use the provided token for all further processing.

Read Authorization header

In the next section, assuming a token was not provided by the messageReceivedContext, the method tries to read the token from the Authorization header:

if (string.IsNullOrEmpty(token))
{
    string authorization = Request.Headers["Authorization"];

    // If no authorization header found, nothing to process further
    if (string.IsNullOrEmpty(authorization))
    {
        return AuthenticateResult.Skip();
    }

    if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
    {
        token = authorization.Substring("Bearer ".Length).Trim();
    }

    // If no token found, no further work possible
    if (string.IsNullOrEmpty(token))
    {
        return AuthenticateResult.Skip();
    }
}

As you can see, if the header is not found, or it does not start with the string "Bearer ", then the remainder of the authentication is skipped. Authentication would pass to the next handler until it finds a middleware to handle it.

Update TokenValidationParameters

At this stage we have a token, but we still need to validate and deserialise it to a ClaimsPrinciple. The next section of HandleAuthenticationAsync uses the ConfigurationManager object created when the middleware was instantiated to update the issuer and signing keys that will be used to validate the token:

if (_configuration == null && Options.ConfigurationManager != null)
{
    _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}

var validationParameters = Options.TokenValidationParameters.Clone();
if (_configuration != null)
{
    if (validationParameters.ValidIssuer == null && !string.IsNullOrEmpty(_configuration.Issuer))
    {
        validationParameters.ValidIssuer = _configuration.Issuer;
    }
    else
    {
        var issuers = new[] { _configuration.Issuer };
        validationParameters.ValidIssuers = (validationParameters.ValidIssuers == null ? issuers : validationParameters.ValidIssuers.Concat(issuers));
    }

    validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : validationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys));
}

First _configuration, a private field, is updated with the latest (cached) configuration details from the ConfigurationManager. The TokenValidationParameters specified when configuring the middleware are then cloned for this request, and augmented with the additional configuration. Any other validation specified when the middleware was added will also be validated (for example, we included ValidateIssuer, ValidateAudience and ValidateLifetime requirements in the example above).

Validating the token

Everything is now set for validating the provided token. The JwtBearerOptions object contains a list of ISecurityTokenValidator so you can potentially use custom token validators, but the default is to use the built in JwtSecurityTokenHandler. This will validate the token, confirm it meets all the requirements and has not been tampered with, and then return a ClaimsPrinciple.

List<Exception> validationFailures = null;
SecurityToken validatedToken;
foreach (var validator in Options.SecurityTokenValidators)
{
    if (validator.CanReadToken(token))
    {
        ClaimsPrincipal principal;
        try
        {
            principal = validator.ValidateToken(token, validationParameters, out validatedToken);
        }
        catch (Exception ex)
        {
            //... Logging etc

            validationFailures = validationFailures ?? new List<Exception>(1);
            validationFailures.Add(ex);
            continue;
        }

        // See next section - returning a success result.
    }
}

So for each ISecurityTokenValidator in the list, we check whether it can read the token, and if so attempt to validate and deserialise the principal. If that is successful, we continue on to the next section, if not, the call to ValidateToken will throw.

Thankfully, the built in JwtSecurityTokenHandler handles all the complicated details of implementing the JWT specification correctly, so as long as the ConfigurationManager is correctly setup, you should be able to validate most types of token.

I've glossed over the catch block somewhat, but we log the error, add it to the validationFailures error collection, potentially refresh the configuration from ConfigurationManager and try the next handler.

When validation is successful

If we successfully validate a token in the loop above, then we can create an authentication ticket from the principal provided.

Logger.TokenValidationSucceeded();

var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), Options.AuthenticationScheme);
var tokenValidatedContext = new TokenValidatedContext(Context, Options)
{
    Ticket = ticket,
    SecurityToken = validatedToken,
};

await Options.Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.CheckEventResult(out result))
{
    return result;
}
ticket = tokenValidatedContext.Ticket;

if (Options.SaveToken)
{
    ticket.Properties.StoreTokens(new[]
    {
        new AuthenticationToken { Name = "access_token", Value = token }
    });
}

return AuthenticateResult.Success(ticket);

Rather than returning a success result straight away, the handler first calls the TokenValidated event handler. This allows us to fully customise the extracted ClaimsPrincipal, even replacing it completely, or rejecting it at this stage by creating a new AuthenticateResult.

Finally the handler optionally stores the extracted token in the AuthenticationProperties of the AuthenticationTicket for use elsewhere in the framework, and returns the authenticated ticket using AuthenticateResult.Success.

When validation fails

If the security token could not be validated by any of the ISecurityTokenValidators, the handler gives one more chance to customise the result.

if (validationFailures != null)
{
    var authenticationFailedContext = new AuthenticationFailedContext(Context, Options)
    {
        Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
    };

    await Options.Events.AuthenticationFailed(authenticationFailedContext);
    if (authenticationFailedContext.CheckEventResult(out result))
    {
        return result;
    }

    return AuthenticateResult.Fail(authenticationFailedContext.Exception);
}

return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");

The AuthenticationFailed event handler is invoked, and again can set the AuthenticateResult directly. If the handler does not directly handle the event, or if there were no configured ISecurityTokenValidators that could handle the token, then authentication has failed.

Also worth noting is that any unexpected exceptions thrown from event handlers etc will result in a similar call to Options.Events.AuthenticationFailed before the exception bubbles up the stack.

The JwtBearerHandler HandleUnauthorisedAsync method

The other significant method in the JwtBearerHandler is HandleUnauthorisedAsync, which is called when a request requires authorisation but is unauthenticated. In the CookieAuthenticationMiddleware, this method redirects to a logon page, while in the JwtBearerHandler, a 401 will be returned, with the WWW-Authenticate header indicating the nature of the error, as per the specification.

Prior to returning a 401, the Options.Event handler gets one more attempt to handle the request with a call to Options.Events.Challenge. As before, this provides a great extensibility point should you need it, allowing you to customise the behaviour to your needs.

SignIn and SignOut

The last two methods in the JwtBearerHandler, HandleSignInAsync and HandleSignOutAsync simply throw a NotSupportedException when called. This makes sense when you consider that the tokens have to come from a different source.

To effectively 'sign in', a client must request a token from the (remote) issuer and provide it when making requests to your application. Signing out from the handler's point of view would just require you to discard the token, and not send it with future requests.

Summary

In this post we looked in detail at the JwtBearerHandler as a means to further understanding how authentication works in the ASP.NET Core framework. It is rare you would need to dive into this much detail when simply using the middleware, but hopefully it will help you get to grips of what is going on under the hood when you add it to your application.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?