Published: April 04 2023

.NET 7.0 + C# - JWT Authentication Tutorial without ASP.NET Core Identity

Built with .NET 7.0 and C#

In this tutorial we'll go through a simple example of how to implement custom JWT (JSON Web Token) authentication in a .NET 7.0 API with C#.

For an extended example that includes refresh tokens see .NET 6.0 - JWT Authentication with Refresh Tokens Tutorial with Example API.

Tutorial contents


Example .NET 7.0 Authentication API Overview

The example API has just two endpoints/routes to demonstrate authenticating with JWT and accessing a restricted route with JWT:

  • /users/authenticate - public route that accepts HTTP POST requests containing the username and password in the body. If the username and password are correct then a JWT authentication token and the user details are returned.
  • /users - secure route that accepts HTTP GET requests and returns a list of all the users in the application if the HTTP Authorization header contains a valid JWT token. If there is no auth token or the token is invalid then a 401 Unauthorized response is returned.

User data hardcoded for testing

A list of users is hardcoded in the user service so the tutorial can focus on JWT authentication, in production it's recommended to store user records in a database. For examples of how to connect to different databases using the Dapper ORM see the following .NET 7 CRUD API tutorials - SQL Server, Postgres, SQLite.

Code on GitHub

The tutorial project is available on GitHub at https://github.com/cornflourblue/dotnet-7-jwt-authentication-api.


Tools required to run the .NET 7.0 JWT Example API Locally

To follow the steps in this tutorial you'll need the following:

  • .NET SDK - includes the .NET runtime and command line tools
  • Visual Studio Code - code editor that runs on Windows, Mac and Linux
  • C# extension for Visual Studio Code - adds support to VS Code for developing .NET applications


Run the .NET JWT Authentication API Locally

  1. Download or clone the tutorial project code from https://github.com/cornflourblue/dotnet-7-jwt-authentication-api
  2. Start the api by running dotnet run from the command line in the project root folder (where the WebApi.csproj file is located), you should see the message Now listening on: http://localhost:4000. Follow the instructions below to test with Postman or hook up with one of the example single page applications available (Angular, React, Blazor or Vue).


Debugging in VS Code

You can start the application in debug mode in VS Code by opening the project root folder in VS Code and pressing F5 or by selecting Debug -> Start Debugging from the top menu. Running in debug mode allows you to attach breakpoints to pause execution and step through the application code. For more info on debugging .NET in VS Code see VS Code + .NET - Debug a .NET Web App in Visual Studio Code.

Before running in production

Before running in production also make sure that you update the Secret property in the appsettings.json file, it is used to sign and verify JWT tokens for authentication, change it to a random string to ensure nobody else can generate a JWT with the same secret and gain unauthorized access to your api. A quick and easy way is join a couple of GUIDs together to make a long random string (e.g. from https://www.guidgenerator.com/).


Test the .NET 7.0 JWT Auth API with Postman

Postman is a great tool for testing APIs, you can download it for free at https://www.postman.com/downloads.

Below are instructions on how to use Postman to authenticate a user to get a JWT token from the api, and then make an authenticated request with the JWT token to retrieve a list of users from the api.


How to authenticate a user with Postman

To authenticate a user with the api and get a JWT token follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the HTTP method to POST with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the authenticate route of your local API - http://localhost:4000/users/authenticate.
  4. Select the Body tab below the URL field, change the body type radio button to raw, and change the format dropdown selector to JSON.
  5. Enter a JSON object containing the test username and password in the Body textarea:
    {
        "username": "test",
        "password": "test"
    }
  6. Click the Send button, you should receive a "200 OK" response with the user details and a JWT token in the response body, make a copy of the token value because we'll be using it in the next step to make an authenticated request.

Here's a screenshot of Postman after the request is sent and the user has been authenticated:


How to make an authenticated request to retrieve all users

To make an authenticated request using the JWT token from the previous step, follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the HTTP method to GET with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the users route of your local API - http://localhost:4000/users.
  4. Select the Authorization tab below the URL field, set the Type selector to Bearer Token, and paste the JWT token from the previous authenticate step into the Token field.
  5. Click the Send button, you should receive a "200 OK" response containing a JSON array with all the user records in the system (just the one test user in the example).

Here's a screenshot of Postman after making an authenticated request to get all users:

 


Connect an Angular App with the .NET JWT Auth API

For full details about the example Angular application see the post Angular 14 - JWT Authentication Example & Tutorial. But to get up and running quickly just follow the below steps.

  1. Install Node.js and npm from https://nodejs.org.
  2. Download or clone the Angular tutorial code from https://github.com/cornflourblue/angular-14-jwt-authentication-example
  3. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  4. Remove or comment out the line below the comment // provider used to create fake backend located in the /src/app/app.module.ts file.
  5. Start the application by running npm start from the command line in the project root folder, this will launch a browser displaying the Angular example application and it should be hooked up with the .NET 7.0 JWT Auth API that you already have running.


Connect a React App with the .NET JWT Auth API

For full details about the example React application see the post React 18 + Redux - JWT Authentication Example & Tutorial. But to get up and running quickly just follow the below steps.

  1. Install Node.js and npm from https://nodejs.org.
  2. Download or clone the React tutorial code from https://github.com/cornflourblue/react-18-redux-jwt-authentication-example
  3. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  4. Remove or comment out the 2 lines below the comment // setup fake backend located in the /src/index.js file.
  5. Start the application by running npm start from the command line in the project root folder
  6. Open a browser and go to the application at http://localhost:3000, the React example application should be hooked up with the .NET 7.0 JWT Auth API that you already have running.


Connect a Blazor WebAssembly (WASM) App with the .NET JWT Auth API

For full details about the example Blazor application see the post Blazor WebAssembly - JWT Authentication Example & Tutorial. But to get up and running quickly just follow the below steps.

  1. Download or clone the tutorial project code from https://github.com/cornflourblue/blazor-webassembly-jwt-authentication-example
  2. Change the "fakeBackend" setting to "false" in the /wwwroot/appsettings.json file.
  3. Start the app by running dotnet run from the command line in the project root folder (where the BlazorApp.csproj file is located)
  4. Open a new browser tab and navigate to the URL http://localhost:5000, the Blazor app should be hooked up with the .NET 7.0 JWT Auth API that you already have running.

NOTE: To enable hot reloading during development so the app automatically restarts when a file is changed, start the app with the command dotnet watch run.


Connect a Vue App with the .NET JWT Auth API

For full details about the example Vue application see the post Vue 3 + Pinia - JWT Authentication Tutorial & Example. But to get up and running quickly just follow the below steps.

  1. Install Node.js and npm from https://nodejs.org.
  2. Download or clone the VueJS tutorial code from https://github.com/cornflourblue/vue-3-pinia-jwt-authentication-example
  3. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  4. Remove or comment out the 2 lines below the comment // setup fake backend located in the /src/main.js file.
  5. Start the application by running npm run dev from the command line in the project root folder.
  6. Open a browser and go to the application at http://localhost:3000, the Vue example application should be hooked up with the .NET 7.0 JWT Auth API that you already have running.
 

.NET 7.0 JWT Authentication Code Documentation

The .NET tutorial project is organised into the following folders:

Authorization
Contains the classes responsible for implementing custom JWT authentication and authorization in the API.

Controllers
Define the end points / routes for the API, controller action methods are the entry points into the API for client applications via HTTP requests.

Models
Represent request and response models for controller action methods. Request models define the parameters for incoming requests and response models define the data that is returned.

Services
Contain business logic, validation and data access code.

Entities
Represent the application data.

Helpers
Anything that doesn't fit into the above folders.

Click any of the below links to jump down to a description of each file along with its code:

 

.NET Allow Anonymous Attribute

Path: /Authorization/AllowAnonymousAttribute.cs

The custom [AllowAnonymous] attribute is used to allow public access to specific action methods when a controller class is decorated with the [Authorize] attribute. It's used in the users controller to allow anonymous access to the Authenticate method.

The logic for allowing public access is located in the custom authorize attribute below, authorization is skipped 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
{ }
 

.NET Custom Authorize Attribute

Path: /Authorization/AuthorizeAttribute.cs

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 user attached to the current request (context.HttpContext.Items["User"]).

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 user = (User?)context.HttpContext.Items["User"];
        if (user == null)
        {
            // not logged in or role not authorized
            context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
        }
    }
}
 

.NET Custom JWT Middleware

Path: /Authorization/JwtMiddleware.cs

The custom JWT middleware extracts the JWT token from the request Authorization header (if there is one) and validates it with the jwtUtils.ValidateToken() method. If validation is successful the user id from the token is returned and the authenticated user object is added to the HttpContext.Items collection which makes it accessible to all other classes within the scope of the current request.

If token validation fails (or there is no token) the request is not authenticated (a.k.a. anonymous) and only allowed to access public routes because there isn't an authenticated user object attached to the HTTP context. The authorization code that checks for the user object is located in the custom authorize attribute above. When an anonymous/unauthorized request is sent to a secure route the authorize attribute returns a 401 Unauthorized HTTP response.

namespace WebApi.Authorization;

using WebApi.Services;

public class JwtMiddleware
{
    private readonly RequestDelegate _next;

    public JwtMiddleware(RequestDelegate next)
    {
        _next = next;
    }

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

        await _next(context);
    }
}
 

.NET JWT Authentication Utils

Path: /Authorization/JwtUtils.cs

The JWT utils class contains methods for generating and validating JWT tokens.

The GenerateJwtToken() method returns a long lived JWT token that expires after 7 days, it contains the id of the specified user as the "id" claim, meaning the token payload will contain the property "id": <userId> (e.g. "id": 1). The token is created with the JwtSecurityTokenHandler class and digitally signed using the secret key stored in the app settings file.

The ValidateJwtToken() method attempts to validate the provided JWT token and return the user id ("id") from the token claims. If validation fails null is returned.

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(User user);
    public int? ValidateJwtToken(string? token);
}

public class JwtUtils : IJwtUtils
{
    private readonly AppSettings _appSettings;

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

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

    public string GenerateJwtToken(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? 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 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;
        }
    }
}
 

.NET Auth Users Controller

Path: /Controllers/UsersController.cs

The .NET users controller defines and handles all routes / endpoints for the api that relate to users, this includes authentication and standard CRUD operations. Within each route the controller calls the user service to perform the action required which keeps the controller lean and completely separated from the business logic and data access code.

Controller methods/routes are secure by default with the [Authorize] attribute on the class. The Authenticate method is decorated with the [AllowAnonymous] attribute which overrides the class-level [Authorize] attribute to make it publicly accessible. Auth logic is located in the custom authorize attribute.

namespace WebApi.Controllers;

using Microsoft.AspNetCore.Mvc;
using WebApi.Authorization;
using WebApi.Models;
using WebApi.Services;

[ApiController]
[Authorize]
[Route("[controller]")]
public class UsersController : ControllerBase
{
    private IUserService _userService;

    public UsersController(IUserService userService)
    {
        _userService = userService;
    }

    [AllowAnonymous]
    [HttpPost("authenticate")]
    public IActionResult Authenticate(AuthenticateRequest model)
    {
        var response = _userService.Authenticate(model);

        if (response == null)
            return BadRequest(new { message = "Username or password is incorrect" });

        return Ok(response);
    }

    [HttpGet]
    public IActionResult GetAll()
    {
        var users = _userService.GetAll();
        return Ok(users);
    }
}
 

.NET JWT User Entity

Path: /Entities/User.cs

The user entity class represents the data for a user in the application. Entity classes are used to pass data between different parts of the application (e.g. between services and controllers) and can be used to return http response data from controller action methods. If multiple types of entities or other custom data is required to be returned from a controller method then a custom model class should be created in the Models folder for the response.

The [JsonIgnore] attribute prevents the password property from being serialized and returned in api responses.

namespace WebApi.Entities;

using System.Text.Json.Serialization;

public class User
{
    public int Id { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public string? Username { get; set; }

    [JsonIgnore]
    public string? Password { get; set; }
}
 

.NET Authentication App Settings

Path: /Helpers/AppSettings.cs

The app settings class contains properties defined in the appsettings.json file and is used for accessing application settings via objects that are injected into classes using the .NET built in dependency injection (DI) system. For example the user service accesses app settings via an IOptions<AppSettings> appSettings object that is injected into the constructor.

Mapping of configuration sections to classes is done in the Program.cs file.

namespace WebApi.Helpers;

public class AppSettings
{
    public string? Secret { get; set; }
}
 

.NET Authenticate Request Model

Path: /Models/AuthenticateRequest.cs

The authenticate request model defines the parameters for incoming requests to the /users/authenticate route, it is attached to the route as the parameter to the Authenticate action method of the users controller. When an HTTP POST request is received by the route, the data from the body is bound to an instance of the AuthenticateRequest class, validated and passed to the method.

.NET Data Annotations are used to automatically handle model validation, the [Required] attribute sets both the username and password as required fields so if either are missing a validation error message is returned from the api.

namespace WebApi.Models;

using System.ComponentModel.DataAnnotations;

public class AuthenticateRequest
{
    [Required]
    public string? Username { get; set; }

    [Required]
    public string? Password { get; set; }
}
 

.NET Authenticate Response Model

Path: /Models/AuthenticateResponse.cs

The authenticate response model defines the data returned after successful authentication, it includes basic user details and a JWT access token.

namespace WebApi.Models;

using WebApi.Entities;

public class AuthenticateResponse
{
    public int Id { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public string? Username { get; set; }
    public string Token { get; set; }


    public AuthenticateResponse(User user, string token)
    {
        Id = user.Id;
        FirstName = user.FirstName;
        LastName = user.LastName;
        Username = user.Username;
        Token = token;
    }
}
 

.NET JWT User Service

Path: /Services/UserService.cs

The user service contains methods for authenticating user credentials and returning a JWT token, getting all users in the application and getting a single user by id.

I hardcoded the array of users in the example to keep it focused on JWT authentication, in a production application it is recommended to store user records in a database with hashed passwords. For examples of how to connect to different databases using the Dapper ORM see the following .NET 7 CRUD API tutorials - SQL Server, Postgres, SQLite.

The top of the file contains the IUserService interface which defines the methods of the user service, followed by the concrete UserService class that implements the interface.

On successful authentication the Authenticate() method generates a JWT (JSON Web Token) using the _jwtUtils.GenerateJwtToken() method which generates a token that is digitally signed using a secret key stored in appsettings.json. The returned JWT must be included in the HTTP Authorization header of subsequent requests to secure routes.

namespace WebApi.Services;

using WebApi.Authorization;
using WebApi.Entities;
using WebApi.Models;

public interface IUserService
{
    AuthenticateResponse? Authenticate(AuthenticateRequest model);
    IEnumerable<User> GetAll();
    User? GetById(int id);
}

public class UserService : IUserService
{
    // users hardcoded for simplicity, store in a db with hashed passwords in production applications
    private List<User> _users = new List<User>
    {
        new User { Id = 1, FirstName = "Test", LastName = "User", Username = "test", Password = "test" }
    };

    private readonly IJwtUtils _jwtUtils;

    public UserService(IJwtUtils jwtUtils)
    {
        _jwtUtils = jwtUtils;
    }

    public AuthenticateResponse? Authenticate(AuthenticateRequest model)
    {
        var user = _users.SingleOrDefault(x => x.Username == model.Username && x.Password == model.Password);

        // return null if user not found
        if (user == null) return null;

        // authentication successful so generate jwt token
        var token = _jwtUtils.GenerateJwtToken(user);

        return new AuthenticateResponse(user, token);
    }

    public IEnumerable<User> GetAll()
    {
        return _users;
    }

    public User? GetById(int id)
    {
        return _users.FirstOrDefault(x => x.Id == id);
    }
}
 

.NET Auth App Settings

Path: /appsettings.json

Root configuration file containing application settings for all environments.

IMPORTANT: The "Secret" property is used to sign and verify JWT tokens for authentication, change it to a random string to ensure nobody else can generate a JWT with the same secret and gain unauthorized access to your API. A quick and easy way is join a couple of GUIDs together to make a long random string (e.g. from https://www.guidgenerator.com/).

{
    "AppSettings": {
        "Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    }
}
 

.NET JWT Program

Path: /Program.cs

The .NET 7 Program file contains top-level statements which are converted by the C# compiler into a Main() method and Program class for the .NET program. The Main() method is the entry point for a .NET application, when an app is started it searches for the Main() method to begin execution. The top-level statements can be located anywhere in the project but are typically placed in the Program.cs file, and only one file can contain top-level statements within a .NET application.

The WebApplication class handles app startup, lifetime management, web server configuration and more. A WebApplicationBuilder is first created by calling the static method WebApplication.CreateBuilder(args), the builder is used to configure services for dependency injection (DI), a WebApplication instance is created by calling builder.Build(), the app instance is used to configure the HTTP request pipeline (middleware), then the app is started by calling app.Run().

I wrapped the add services... and configure HTTP... sections in curly brackets {} to group them together visually, but the brackets are completely optional.

Internally the WebApplicationBuilder class calls the ConfigureWebHostDefaults() extension method which configures hosting for the web app including setting Kestrel as the web server, adding host filtering middleware and enabling IIS integration. For more info on the default builder settings see https://docs.microsoft.com/aspnet/core/fundamentals/host/generic-host#default-builder-settings.

using WebApi.Authorization;
using WebApi.Helpers;
using WebApi.Services;

var builder = WebApplication.CreateBuilder(args);

// add services to DI container
{
    var services = builder.Services;
    services.AddCors();
    services.AddControllers();

    // configure strongly typed settings object
    services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));

    // configure DI for application services
    services.AddScoped<IJwtUtils, JwtUtils>();
    services.AddScoped<IUserService, UserService>();
}

var app = builder.Build();

// configure HTTP request pipeline
{
    // global cors policy
    app.UseCors(x => x
        .AllowAnyOrigin()
        .AllowAnyMethod()
        .AllowAnyHeader());

    // custom jwt auth middleware
    app.UseMiddleware<JwtMiddleware>();

    app.MapControllers();
}

app.Run("http://localhost:4000");
 

.NET Auth API csproj

Path: /WebApi.csproj

The csproj (C# project) is an MSBuild based file that contains target framework and NuGet package dependency information for the application. The ImplicitUsings feature is enabled which tells the compiler to auto generate a set of global using directives based on the project type, removing the need to include a lot of common using statements. The global using statements are auto generated when you build the project and can be found in the file /obj/Debug/net7.0/WebApi.GlobalUsings.g.cs.

For more info on the C# project file see .NET + MSBuild - C# Project File (.csproj) in a Nutshell.

<Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <RootNamespace>WebApi</RootNamespace>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.4" />
        <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.28.0" />
    </ItemGroup>
</Project>


Other versions of this tutorial

The JWT authentication API tutorial is also available in the following versions:

 


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