Published: June 02 2021

.NET 5.0 - Create and Validate JWT Tokens + Use Custom JWT Middleware

Built with .NET 5.0

Other versions available:

This is a quick example of how to create and validate JWT tokens in .NET 5.0 using the JwtSecurityTokenHandler class which is part of the System.IdentityModel.Tokens.Jwt NuGet package. We'll also cover how to implement custom JWT authentication using custom JWT middleware and a custom authorize attribute.

The code samples use the JWT token handler and a few related classes to create and validate JWT tokens, but no other parts of the .NET Identity system are used.

All of the code in this tutorial is taken from a .NET API tutorial I posted recently, for more info or to download and test the API locally see .NET 5.0 - Simple API for Authentication, Registration and User Management.


Installing the JWT Token Library via NuGet

.NET CLI: dotnet add package System.IdentityModel.Tokens.Jwt

Visual Studio Package Manager Console: System.IdentityModel.Tokens.Jwt


Create a JWT Token in .NET 5.0

This code generates a JWT token with the specified user.Id as the "id" claim, meaning the token payload will contain the property "id": <user.Id> (e.g. "id": 123).

The _appSettings.Secret parameter on line 5 is a secret string used to sign and verify JWT tokens in the application, it can be any string.

public string GenerateToken(User user)
{
    // generate token that is valid for 7 days
    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }),
        Expires = DateTime.UtcNow.AddDays(7),
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
    };
    var token = tokenHandler.CreateToken(tokenDescriptor);
    return tokenHandler.WriteToken(token);
}


Validate a JWT Token in .NET 5.0

This code attempts to validate the provided JWT token and return the userId from the token claims. If the token is null or validation fails null is returned.

public int? ValidateToken(string token)
{
    if (token == null) 
        return null;

    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
    try
    {
        tokenHandler.ValidateToken(token, new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false,
            // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
            ClockSkew = TimeSpan.Zero
        }, out SecurityToken validatedToken);

        var jwtToken = (JwtSecurityToken)validatedToken;
        var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);

        // return user id from JWT token if validation successful
        return userId;
    }
    catch
    {
        // return null if validation fails
        return null;
    }
}


Validate Tokens in Custom JWT Middleware

Below is custom JWT middleware that validates the JWT token contained in the request "Authorization" header (if it exists).

On successful JWT validation the middleware retrieves the associated user from the database and assigns it to context.Items["User"] which makes the user object available to any other code running within the current request scope. The user object is then used by the custom Authorize attribute below to perform authorization.

The JwtUtils class used on line 24 contains the above methods for validating and generating JWT tokens, the full class is included below for completeness.

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using System.Linq;
using System.Threading.Tasks;
using WebApi.Helpers;
using WebApi.Services;

namespace WebApi.Authorization
{
    public class JwtMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly AppSettings _appSettings;

        public JwtMiddleware(RequestDelegate next, IOptions<AppSettings> appSettings)
        {
            _next = next;
            _appSettings = appSettings.Value;
        }

        public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils)
        {
            var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
            var userId = jwtUtils.ValidateToken(token);
            if (userId != null)
            {
                // attach user to context on successful jwt validation
                context.Items["User"] = userService.GetById(userId.Value);
            }

            await _next(context);
        }
    }
}


Check Token Validated with Custom Authorize Attribute

The custom [Authorize] attribute is used to restrict access to controllers or specified action methods. Only authorized requests are allowed to access action methods that are decorated with the [Authorize] attribute.

When a controller is decorated with the [Authorize] attribute all action methods are restricted to authorized requests, except for methods decorated with the custom [AllowAnonymous] attribute above.

Authorization is performed by the OnAuthorization method which checks if there is an authenticated user attached to the current request (context.HttpContext.Items["User"]). An authenticated user is attached by the above custom JWT middleware if the request contains a valid JWT token.

On successful authorization no action is taken and the request is passed through to the controller action method, if authorization fails a 401 Unauthorized response is returned.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Linq;
using WebApi.Entities;

namespace WebApi.Authorization
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class AuthorizeAttribute : Attribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            // skip authorization if action is decorated with [AllowAnonymous] attribute
            var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType<AllowAnonymousAttribute>().Any();
            if (allowAnonymous)
                return;

            // authorization
            var user = (User)context.HttpContext.Items["User"];
            if (user == null)
                context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
        }
    }
}


Custom Allow Anonymous Attribute

The custom [AllowAnonymous] attribute is used to allow anonymous access to specified action methods of controllers that are decorated with the [Authorize] attribute. The custom authorize attribute above skips authorization if the action method is decorated with [AllowAnonymous].

I created a custom allow anonymous (instead of using the built-in one) for consistency and to avoid ambiguous reference errors between namespaces.

using System;

namespace WebApi.Authorization
{
    [AttributeUsage(AttributeTargets.Method)]
    public class AllowAnonymousAttribute : Attribute
    { }
}


Custom JWT Utils Class

This is the complete JWTUtils class that contains the GenerateToken and ValidateToken methods covered at the start of the post. It is used by the custom JWT middleware above to validate tokens.

using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using WebApi.Entities;
using WebApi.Helpers;

namespace WebApi.Authorization
{
    public interface IJwtUtils
    {
        public string GenerateToken(User user);
        public int? ValidateToken(string token);
    }

    public class JwtUtils : IJwtUtils
    {
        private readonly AppSettings _appSettings;

        public JwtUtils(IOptions<AppSettings> appSettings)
        {
            _appSettings = appSettings.Value;
        }

        public string GenerateToken(User user)
        {
            // generate token that is valid for 7 days
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }),
                Expires = DateTime.UtcNow.AddDays(7),
                SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
            };
            var token = tokenHandler.CreateToken(tokenDescriptor);
            return tokenHandler.WriteToken(token);
        }

        public int? ValidateToken(string token)
        {
            if (token == null) 
                return null;

            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
            try
            {
                tokenHandler.ValidateToken(token, new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
                    ClockSkew = TimeSpan.Zero
                }, out SecurityToken validatedToken);

                var jwtToken = (JwtSecurityToken)validatedToken;
                var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);

                // return user id from JWT token if validation successful
                return userId;
            }
            catch
            {
                // return null if validation fails
                return null;
            }
        }
    }
}

 


Need Some .NET Help?

Search fiverr for freelance .NET developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by