GitHub Repository

All the code in this series can be found at https://github.com/dombarter/Solar.API

Motivation

In my previous post, I explored how to manage login, registration and role assignment of our users using .NET Identity. In this post I am going to add JWT based authentication and authorization to our .NET 6 Web API.

What are JWTs?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

(https://jwt.io/introduction)

JWTs are becoming increasingly popular in the world of single sign on, and are a very scalable, stateless and memory efficient way of providing authentication.

NuGet Packages

We need to install the following package:

  • Microsoft.AspNetCore.Authentication.JwtBearer

Define The JWT Configuration

JWTs are defined by some key parts including:

  • Issuer (who has issued the key)
  • Audience (who the key is intended for)
  • Signing Key (a private random string used to sign the key)

We need to define these in our appsettings.json:

{
  "Jwt": {
    "Key": "ThisIsMySecretKey",
    "Issuer": "localhost",
    "Audience": "localhost"
  }
}

Don’t worry too much about the value of the Issuer and Audience, just make sure they’re the same. When used in a single sign on setting - we might have one machine generate the key, which could then be consumed by multiple different places. In our case we’re creating and consuming it in the same place. It’s just a straight string comparison.

For more information see https://www.rfc-editor.org/rfc/rfc7519#section-4.1.

Amend The Startup

Now we need to alter our startup class, Program.cs to configure our authentication mechanism. Specifically we are telling the Web API that it should look out for a JWT in the request headers, and use this to authenticate the current user.

// Add JWTs
builder.Services.AddAuthentication(auth =>
{
    auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
    };
});

...

// Must be in this order
app.UseAuthentication();
app.UseAuthorization();

Token Service

We now need to make a token service, that will accept an IdentityUser and an expiry time, before generating a JWT we can return to the user.

// TokenService.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace Solar.Services.Token
{
    public class TokenService : ITokenService
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly IConfiguration _config;

        public TokenService(UserManager<IdentityUser> userManager, IConfiguration config)
        {
            _userManager = userManager;
            _config = config;
        }

        async public Task<string> GenerateJwtToken(IdentityUser user, TimeSpan expiration)
        {
            // Define the token claims (username and unique guid)
            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            };

            // Add the roles to the token
            foreach(var role in await _userManager.GetRolesAsync(user))
            {
                claims.Add(new Claim("role", role));
            }

            // Encode our private JWT key
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            // Put everything together
            var token = new JwtSecurityToken(
                issuer: _config["Jwt:Issuer"],
                audience: _config["Jwt:Audience"],
                expires: DateTime.UtcNow.Add(expiration),
                claims: claims,
                signingCredentials: creds
            );

            // Build the token as a string
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }
}

Consuming The Token Service

Now we have the token service setup we need to register it in the dependency injection container within Program.cs:

// Add token service
builder.Services.AddTransient<ITokenService, TokenService>();

We can then utilise the token service in our login action like so:

[HttpPost]
[Route("login")]
[AllowAnonymous]
public async Task<ActionResult<string>> Login([FromBody] LoginDto model)
{
    var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, false);

    if (!result.Succeeded)
    {
        return BadRequest("Incorrect email or password");
    }

    // Generate JWT
    var user = await _userManager.FindByNameAsync(model.Email);
    var token = await _tokenService.GenerateJwtToken(user, TimeSpan.FromMinutes(30));

    return Ok(token);
}

Role Based Authorization

Because we have configured our JWTs to include the role information, this means we can safely use the Authorize attributes across our controllers and actions:

[HttpGet]
[Route("one")]
[Authorize(Roles = "User")]
public ActionResult<string> GetRandomMoon()
{
    return Moons[Random.Next(Moons.Count)];
}

[HttpGet]
[Route("two")]
[Authorize(Roles = "Admin")]
public ActionResult<string> GetTwoRandomMoons()
{
    return $"{Moons[Random.Next(Moons.Count)]}, {Moons[Random.Next(Moons.Count)]}";
}

Accessing The Current User Information

Because we configured our JWTs to include the current username, this means we can grab the information of the user who the token belongs to - which could be helpful when making SQL etc.

[HttpGet]
[Route("user")]
[Authorize(Roles = "Admin, User")]
public async Task<ActionResult<IdentityUser>> GetLoggedInUser()
{
    var username = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    var user = await _userManager.FindByNameAsync(username);
    return new OkObjectResult(user);
}

Adding JWT support to Swagger

And finally, if you’d like to support the Authorize window in Swagger (adds the ability to pass the Bearer token with each subsequent request), add the following to your startup class:

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    var jwtSecurityScheme = new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = JwtBearerDefaults.AuthenticationScheme,
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Reference = new OpenApiReference
        {
            Type = ReferenceType.SecurityScheme,
            Id = JwtBearerDefaults.AuthenticationScheme
        }
    };

    options.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, jwtSecurityScheme);
    options.AddSecurityRequirement(new OpenApiSecurityRequirement(){{ jwtSecurityScheme, new string[] {} }});
});

Testing The JWTs Using Swagger

Load up swagger, login using your username and password, and grab the JWT from the response:

login

Now scroll to the top of the page, and click on the Authorize button and paste in your token:

authorize

Make sure to press Authorize to save the token!

And finally, scroll to one of the endpoints that requires authentication and test it out. If you have a valid token and the correct roles - you will see the content returned. If not you’ll get a contextual http response (401 etc):

moons

References