Authentication In An ASP.NET Core API - Part 3: JSON Web Token

In this series, I am going to outline some basic approaches to authenticating your .NET Core API using either ASP.NET Core Identity or token-based authentication with a JSON Web Token (JWT). I will also explore how to configure your application to return proper response types to both Redirect To Login and Redirect To Access Denied events when using ASP.NET Core Identity.

Authentication In A Dot Net Core API

  1. ASP.NET Core Identity, Accessed Denied.
  2. ASP.NET Core Identity, Accessed Granted.
  3. Token based authentication with a JSON Web Token (JWT).

In part 2 (ASP.NET Core Identity, Accessed Granted.) of this series, I explored how to create a valid user using Identity and grant access to your ASP.NET Core API endpoints with that user. In this post, I will show you how I provide a JSON Web Token (JWT) to a valid user and use that token to authenticate the user using the JwtBearerMiddleware middleware.

Bearer Middleware: Access Denied

To start off, I pulled in the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package into my project. This package contains the necessary extensions needed to validate a bearer token, consume and decrypt header-payload data associated with a valid token, and have the token authentication pipeline sit nicely aside ASP.NET Core Identity.

Bearer Nuget Package

Once the package is pulled in, we need to add and configure the JwtBearerMiddleware middleware in our Startup.cs class. In the Startup.Configure function I added the following.

app.UseJwtBearerAuthentication(new JwtBearerOptions
{
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    TokenValidationParameters = new TokenValidationParameters
    {
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetSection("AppConfiguration:Key").Value)),
        ValidAudience = Configuration.GetSection("AppConfiguration:SiteUrl").Value,
        ValidateIssuerSigningKey = true,
        ValidateLifetime = true,
        ValidIssuer = Configuration.GetSection("AppConfiguration:SiteUrl").Value
    }
});

Here I am passing a JwtBearerOptions object to my JwtBearerMiddleware middleware, of which is configure to suit my needs. By adding this to my middleware pipeline, I am effectively creating another means of authentication that sits next to Identity. When a request comes in that needs to be authenticated, both Identity and the JwtBearerMiddleware middleware will check to see if their individual criteria are met in order to pass validation.

Based on "order of operation" concerns in regards to how you pull in middleware into your pipeline, I made a conscious decision to add Identity after the JwtBearerMiddleware middleware. By doing so, the event handlers we set up in Part 1 of this series will still have the desired outcome.

For more information on configuration options check out JwtBearerOptions Class.

Creating A JSON Web Token

Now that we have a way to check validation using a JSON Web Token, we need to be able to create a token for the client to use. In my AccountApiController controller, I added the following functions to just that.

[HttpPost("token")]
public async Task<IActionResult> Token([FromBody] UserRegisterAuthenticate model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    var user = await _userManager.FindByNameAsync(model.Email);

    if (user == null || _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, model.Password) != PasswordVerificationResult.Success)
    {
        return BadRequest();
    }

    var token = await GetJwtSecurityToken(user);

    return Ok(new
    {
        token = new JwtSecurityTokenHandler().WriteToken(token),
        expiration = token.ValidTo
    });
}

First up is the API endpoint, of which is accessed by making a POST request against the api/account/token route. We start by checking if our ModelState is in order. If it is not, we return a 400 level response. We then make a request to get the user entity from our data-store. We use that entity to see if a hashed version of the incoming model's password matches the hashed version we are storing in our database. If does not match, we again return a 400 level response. If it does match, we then know the user's request for a token is valid and we now need to build a token to pass back to that user.

We get our token by making a request to following function.

private async Task<JwtSecurityToken> GetJwtSecurityToken(UserEntity user)
{
    var userClaims =  await _userManager.GetClaimsAsync(user);

    return new JwtSecurityToken(
        issuer: _appConfiguration.Value.SiteUrl,
        audience: _appConfiguration.Value.SiteUrl,
        claims: GetTokenClaims(user).Union(userClaims),
        expires: DateTime.UtcNow.AddMinutes(10),
        signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_appConfiguration.Value.Key)), SecurityAlgorithms.HmacSha256)
    );
}

In this function, we are building up a JwtSecurityToken object to pass back to the caller.

  • We first grab any Identity based claims we have stored in the database.
    • We add those claims on top of the necessary JSON Web Token claims that are produced in a function called Startup.GetTokenClaims.
      • More on this in a bit.
  • Any configuration that is unique to a deployment is coming from the IOption<T> configuration object.
    • Notably, we use a secure and secret key stored in configuration to produce our Symmetric Security Key.

For more information about the configuration options of this object, you can check the official documentation JwtSecrityToken Class documentation.

To build the actual claims need for a valid JSON Web Token, I use the following function.

private static IEnumerable<Claim> GetTokenClaims(UserEntity user)
{
    return new List<Claim>
    {
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(JwtRegisteredClaimNames.Sub, user.UserName)
    };
}

What is represented in this function in regards to claims is a bear minimum. For more information about the long list of claims you can associate with your JSON Web Token, take a look at official JSON Web Token (JWT) standards documentation.

Access Granted

Now that we have our middleware configured and setup up, and a means for a client to get a validated token from our API, we should be good to go. Let's test this by making a request to our new api/account/token endpoint with valid credentials.

Request Token

If our credentials are correct, we will be passed back a token and the expiration date of said token.

Response Token

We then can use that token and pass it to any request that needs authentication by setting an Authorization header key with the value of bearer, followed by the token.

Validated Request

And that is that! In this series, I showed how I...

  • Setup ASP.NET Core Identity.
    • Blocked access to endpoints using Attributes.
    • Create a valid user record.
    • Grant access by login using a cookie.
  • Setup the JwtBearerMiddleware middleware.
    • Produce and return a valid token based on user credentials.
    • Use that token to authenticate a request to a secure endpoint.

If you missed any of the previous posts in this series, be sure to check out the links at the top of this page. As always, feel free to leave any comments or concerns in the comments section below.