Published: January 19 2023

.NET 7.0 Auth - Sign & Validate JWT Without Core Identity

Tutorial built with .NET 7.0

Other versions available:

This is a quick post to show how easy it is to sign and validate JWT auth tokens in .NET 7.0 without using the built-in Core Identity membership system. We'll also cover how to implement authentication with custom JWT middleware and a custom authorize attribute.

The code snippets below are from a .NET 7.0 auth tutorial I posted recently on how to authenticate with Facebook, the full tutorial and project code is available at .NET 7.0 - Facebook Authentication API Tutorial with Example.


Install the JWT Token Library via NuGet

This library provides support for creating, serializing and validating JSON Web Tokens.

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

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


Create a Signed JWT Token in .NET 7.0

This method generates a signed JWT authentication token with the specified account.Id as the "id" claim, meaning the token payload will contain the property "id": <account.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 GenerateJwtToken(Account account)
{
    // generate token that is valid for 15 minutes
    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_appSettings.Secret!);
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[] { new Claim("id", account.Id.ToString()) }),
        Expires = DateTime.UtcNow.AddMinutes(15),
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
    };
    var token = tokenHandler.CreateToken(tokenDescriptor);
    return tokenHandler.WriteToken(token);
}


Validate a JWT Auth Token in .NET 7.0

This method validates the provided JWT auth token and return the accountId from the token claims. If the token is null or validation fails null is returned.

public int? ValidateJwtToken(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 accountId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);

        // return account id from JWT token if validation successful
        return accountId;
    }
    catch
    {
        // return null if validation fails
        return null;
    }
}


Custom JWT Auth Middleware

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

On successful JWT validation the middleware retrieves the associated account from the database and assigns it to context.Items["Account"] which makes the current account object accessible to any code in the current request scope via HttpContext.Items["Account"]. The account object is then used by the custom authorize attribute to perform authorization.

The injected jwtUtils instance contains the above methods for creating and validating JWTs (GenerateJwtToken() & ValidateJwtToken()).

namespace WebApi.Authorization;

using Microsoft.Extensions.Options;
using WebApi.Helpers;

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, DataContext dataContext, IJwtUtils jwtUtils)
    {
        var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
        var accountId = jwtUtils.ValidateJwtToken(token);
        if (accountId != null)
        {
            // attach account to context on successful jwt validation
            context.Items["Account"] = await dataContext.Accounts.FindAsync(accountId.Value);
        }

        await _next(context);
    }
}


Custom Authorize Attribute

The custom [Authorize] attribute is added to controller classes or action methods that require the user to be authenticated.

When a controller class is decorated with the [Authorize] attribute all action methods in the controller 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 account attached to the current request (context.HttpContext.Items["Account"]). An authenticated account is attached by 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.

namespace WebApi.Authorization;

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApi.Entities;

[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 account = (Account?)context.HttpContext.Items["Account"];
        if (account == null)
        {
            // not logged in or role not authorized
            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 skips authorization if the action method is decorated with [AllowAnonymous].

The reason I created a custom AllowAnonymous attribute instead of using the one in the .NET Core framework (Microsoft.AspNetCore.Authorization) is for consistency with the other custom auth classes in the project and to avoid ambiguous reference errors between namespaces.

namespace WebApi.Authorization;

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


Custom .NET JWT Utils Class

This is the complete JWTUtils class that contains the GenerateToken() and ValidateToken() methods covered at the top of the post. It's used by the custom JWT middleware to validate tokens.

namespace WebApi.Authorization;

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

public interface IJwtUtils
{
    public string GenerateJwtToken(Account account);
    public int? ValidateJwtToken(string? token);
}

public class JwtUtils : IJwtUtils
{
    private readonly DataContext _context;
    private readonly AppSettings _appSettings;

    public JwtUtils(
        DataContext context,
        IOptions<AppSettings> appSettings)
    {
        _context = context;
        _appSettings = appSettings.Value;

        if (string.IsNullOrEmpty(_appSettings.Secret))
            throw new AppException("JWT secret not configured");
    }

    public string GenerateJwtToken(Account account)
    {
        // generate token that is valid for 15 minutes
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_appSettings.Secret!);
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[] { new Claim("id", account.Id.ToString()) }),
            Expires = DateTime.UtcNow.AddMinutes(15),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

    public int? ValidateJwtToken(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 accountId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);

            // return account id from JWT token if validation successful
            return accountId;
        }
        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