Starting with ASP.NET Core 7.0, .NET SDK 7 has built-in support for the dotnet user-jwts command, which helps you manage the keys and JWT Tokens you need during development.

dotnet user-jwts

Create example project

  1. lock the .NET SDK version to 7.0.203 (and subsequent versions)

    1
    
    dotnet new globaljson --sdk-version 7.0.203 --roll-forward latestFeature
    
  2. Building ASP.NET Core Web API Projects

    1
    2
    3
    4
    5
    6
    
    dotnet new webapi -n AspNetCoreUserJwtsDemo
    cd AspNetCoreUserJwtsDemo
    dotnet new gitignore
    git init
    git add .
    git commit -m "Initial commit"
    

Initial authentication and authorization

  1. Install the Microsoft.AspNetCore.Authentication.JwtBearer package.

    1
    
    dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
    
  2. Modify Program.cs to register the JwtBearer service to the DI container

    1
    2
    
    builder.Services.AddAuthentication("Bearer").AddJwtBearer();
    builder.Services.AddAuthorization();
    
  3. Modify the Program.cs setting to require Bearer Token authentication and authorization for all Controllers.

    1
    
    app.MapControllers().RequireAuthorization();
    
  4. Theoretically, all APIs are inaccessible

    First adjust the Properties/launchSettings.json file.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    {
    "$schema": "https://json.schemastore.org/launchsettings.json",
    "profiles": {
        "http": {
        "commandName": "Project",
        "dotnetRunMessages": true,
        "launchBrowser": false,
        "launchUrl": "WeatherForecast",
        "applicationUrl": "http://localhost:5000",
        "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
        }
        }
    }
    }
    

    Launch the website.

    1
    
    dotnet run
    

    Send the request via cURL.

    1
    
    curl -i http://localhost:5000/WeatherForecast
    

    As a result, you will get the HTTP/1.1 401 Unauthorized response!

    1
    2
    3
    4
    5
    
    HTTP/1.1 401 Unauthorized
    Content-Length: 0
    Date: Fri, 28 Apr 2023 13:15:33 GMT
    Server: Kestrel
    WWW-Authenticate: Bearer
    

Use dotnet user-jwts create to create a JWT Token

  1. Run the following command to create a JWT Token

    1
    
    dotnet user-jwts create
    

    The execution results are as follows:

    1
    2
    3
    4
    
    New JWT saved with ID '3165f978'.
    Name: wakau
    
    Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Indha2F1Iiwic3ViIjoid2FrYXUiLCJqdGkiOiIzMTY1Zjk3OCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCIsIm5iZiI6MTY4MjY4ODMxNiwiZXhwIjoxNjkwNTUwNzE2LCJpYXQiOjE2ODI2ODgzMTcsImlzcyI6ImRvdG5ldC11c2VyLWp3dHMifQ.WergzBiHz7UFAeBotkpaM9lpn8is0J5Fpm0D2yCfB-A
    

    Default Issuer is dotnet-user-jwts.

  2. Check if you can use this Token to request the API

    Restart the site (be sure to start in the Development environment)

    1
    
    dotnet run
    

    Send the request via cURL.

    1
    
    curl -i -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Indha2F1Iiwic3ViIjoid2FrYXUiLCJqdGkiOiIzMTY1Zjk3OCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCIsIm5iZiI6MTY4MjY4ODMxNiwiZXhwIjoxNjkwNTUwNzE2LCJpYXQiOjE2ODI2ODgzMTcsImlzcyI6ImRvdG5ldC11c2VyLWp3dHMifQ.WergzBiHz7UFAeBotkpaM9lpn8is0J5Fpm0D2yCfB-A" http://localhost:5000/WeatherForecast
    

    As a result, you will get the HTTP/1.1 200 OK response!

    1
    2
    3
    4
    5
    6
    7
    
    HTTP/1.1 200 OK
    Content-Type: application/json; charset=utf-8
    Date: Fri, 28 Apr 2023 13:20:48 GMT
    Server: Kestrel
    Transfer-Encoding: chunked
    
    [{"date":"2023-04-29","temperatureC":-18,"temperatureF":0,"summary":"Bracing"},{"date":"2023-04-30","temperatureC":54,"temperatureF":129,"summary":"Bracing"},{"date":"2023-05-01","temperatureC":51,"temperatureF":123,"summary":"Balmy"},{"date":"2023-05-02","temperatureC":35,"temperatureF":94,"summary":"Warm"},{"date":"2023-05-03","temperatureC":5,"temperatureF":40,"summary":"Chilly"}]
    

    Wouldn’t you think, “It’s that simple?”

About what dotnet user-jwts create does behind the scenes

In fact, this dotnet user-jwts create command does more than just create a Token.

  1. Automatically add the following Authentication:Schemes:Bearer setting to the appsettings.Development.json configuration file.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    {
    "Logging": {
        "LogLevel": {
        "Default": "Information",
        "Microsoft.AspNetCore": "Warning"
        }
    },
    "Authentication": {
        "Schemes": {
        "Bearer": {
            "ValidAudiences": [
            "http://localhost:5000"
            ],
            "ValidIssuer": "dotnet-user-jwts"
        }
        }
    }
    }
    
  2. Automatically register a set of <UserSecretsId> secret numbers required by the Secret Manager in AspNetCoreUserJwtsDemo.csproj.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    <Project Sdk="Microsoft.NET.Sdk.Web">
    
    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <UserSecretsId>98ca3101-9491-4d1c-98d9-d44e2da12ea0</UserSecretsId>
    </PropertyGroup>
    
    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.5" />
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
    </ItemGroup>
    
    </Project>
    
  3. Automatically create %APPDATA%\Microsoft\UserSecrets\<secrets_GUID>\secrets.json file (Secret Manager)

    For Linux/macOS it would be here ~/.microsoft/usersecrets/<secrets_GUID>/secrets.json.

    This contains the JWT SigningKeys required during development! (only used in the Development environment).

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    {
        "Authentication:Schemes:Bearer:SigningKeys": [
            {
                "Id": "751e5adb",
                "Issuer": "dotnet-user-jwts",
                "Value": "3oovYRPOGgclHgCL78QLhk0gYFoQro\u002B4UQZripf02ec=",
                "Length": 32
            }
        ]
    }
    
  4. Automatically create the %APPDATA%\Microsoft\UserSecrets\<secrets_GUID>\user-jwts.json file

    For Linux/macOS it would be here ~/.microsoft/usersecrets/<secrets_GUID>/user-jwts.json

    This has all the JWT Tokens created by dotnet user-jwts create, examples are below.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    {
        "3165f978": {
            "Id": "3165f978",
            "Scheme": "Bearer",
            "Name": "wakau",
            "Audience": "http://localhost:5000",
            "NotBefore": "2023-04-28T13:25:16+00:00",
            "Expires": "2023-07-28T13:25:16+00:00",
            "Issued": "2023-04-28T13:25:17+00:00",
            "Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Indha2F1Iiwic3ViIjoid2FrYXUiLCJqdGkiOiIzMTY1Zjk3OCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCIsIm5iZiI6MTY4MjY4ODMxNiwiZXhwIjoxNjkwNTUwNzE2LCJpYXQiOjE2ODI2ODgzMTcsImlzcyI6ImRvdG5ldC11c2VyLWp3dHMifQ.WergzBiHz7UFAeBotkpaM9lpn8is0J5Fpm0D2yCfB-A",
            "Scopes": [],
            "Roles": [],
            "CustomClaims": {}
        }
    }
    

    It is expected that JWT Tokens will be issued for 91 days

Common dotnet user-jwts command usage

Assume our JWT ID is 3165f978.

  1. List all issued JWT Tokens (dotnet user-jwts list)

    1
    
    dotnet user-jwts list
    
    1
    2
    3
    4
    5
    6
    7
    
    Project: 'G:\Projects\AspNetCoreUserJwtsDemo\AspNetCoreUserJwtsDemo.csproj'
    User Secrets ID: '98ca3101-9491-4d1c-98d9-d44e2da12ea0'
    ---------------------------------------------------------------------------------------------------------------------------------------
    |       ID | Scheme |           Audience(s) |                                  Issued On |                                 Expires On |
    ---------------------------------------------------------------------------------------------------------------------------------------
    | 3165f978 | Bearer | http://localhost:5000 | 2023-04-28T13:25:17.0000000+00:00          | 2023-07-28T13:25:16.0000000+00:00          |
    ---------------------------------------------------------------------------------------------------------------------------------------
    
  2. Show full information of JWT Token (dotnet user-jwts print)

    1
    
    dotnet user-jwts print 3165f978 --show-all
    
  3. Delete the issued JWT Token (dotnet user-jwts remove)

    1
    
    dotnet user-jwts remove 3165f978
    
  4. Obtain the current JWT Token issuing key (dotnet user-jwts key)

    1
    
    dotnet user-jwts key
    
    1
    
    Signing Key: '3oovYRPOGgclHgCL78QLhk0gYFoQro+4UQZripf02ec='
    
  5. Reset the JWT Token Issuing Key currently in use

    1
    
    dotnet user-jwts key --reset --force
    
    1
    
    New signing key created: 'p3Nr5hi9yJtuH5/FAzKnoMrw09RYgE5ERLaM5WvgKok='
    
  6. Reset the currently used JWT Token issuance key and use the customized Issuer name

    1
    
    dotnet user-jwts key --reset --force --issuer AspNetCoreUserJwtsDemo
    
    1
    
    New signing key created: 'p3Nr5hi9yJtuH5/FAzKnoMrw09RYgE5ERLaM5WvgKok='
    

    If you create multiple sets of Issuer with different names, he will create multiple signature keys in secrets.json.

  7. Create Issuer as JWT Token for AspNetCoreUserJwtsDemo (dotnet user-jwts create)

    1
    
    dotnet user-jwts create --issuer AspNetCoreUserJwtsDemo
    
    1
    2
    3
    4
    5
    
    New JWT saved with ID '75bf89c7'.
    Name: wakau
    Issuer: AspNetCoreUserJwtsDemo
    
    Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Indha2F1Iiwic3ViIjoid2FrYXUiLCJqdGkiOiI3NWJmODljNyIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCIsIm5iZiI6MTY4MjY5MDQ2MCwiZXhwIjoxNjkwNTUyODYwLCJpYXQiOjE2ODI2OTA0NjEsImlzcyI6IkFzcE5ldENvcmVVc2VySnd0c0RlbW8ifQ.qGWR-uVUDaaBJfJAgNJGNcchLwPtmKI0W6W4OsX5KfA
    

    If you create multiple sets of Issuer with different names, he will create multiple signature keys in secrets.json.

  8. Clear all issued JWT Token (dotnet user-jwts clear)

    1
    
    dotnet user-jwts clear
    
    1
    2
    3
    4
    
    Are you sure you want to delete 2 JWT(s) for 'G:\Projects\AspNetCoreUserJwtsDemo\AspNetCoreUserJwtsDemo.csproj'?
    [Y]es / [N]o
    y
    Deleted 2 token(s) from 'G:\Projects\AspNetCoreUserJwtsDemo\AspNetCoreUserJwtsDemo.csproj' successfully.
    

More application tips

  1. Issuing JWT Token with admin and user roles

    Adjust API Controller to add [Authorize(Roles = "admin, manager")] attribute

    Set either admin or manager to be authorized to access this API

    1
    2
    3
    4
    5
    6
    7
    
    using Microsoft.AspNetCore.Authorization;
    
    [Authorize(Roles = "admin, manager")]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    { ... }
    

    Create a JWT Token and write Claims for role in the Token.

    1
    
    dotnet user-jwts create --role admin --role user
    

    Restart the site (be sure to start in the Development environment).

    1
    
    dotnet run
    

    Send the request via cURL.

    1
    
    curl -i -H "Authorization: Bearer {token}" http://localhost:5000/WeatherForecast
    

    As a result, you will get the HTTP/1.1 200 OK response!

    1
    2
    3
    4
    5
    6
    7
    
    HTTP/1.1 200 OK
    Content-Type: application/json; charset=utf-8
    Date: Fri, 28 Apr 2023 13:20:48 GMT
    Server: Kestrel
    Transfer-Encoding: chunked
    
    [{"date":"2023-04-29","temperatureC":-18,"temperatureF":0,"summary":"Bracing"},{"date":"2023-04-30","temperatureC":54,"temperatureF":129,"summary":"Bracing"},{"date":"2023-05-01","temperatureC":51,"temperatureF":123,"summary":"Balmy"},{"date":"2023-05-02","temperatureC":35,"temperatureF":94,"summary":"Warm"},{"date":"2023-05-03","temperatureC":5,"temperatureF":40,"summary":"Chilly"}]
    

    Create a JWT Token and write Claims for role in the Token (only user role).

    1
    
    dotnet user-jwts create --role user
    

    Restart the site (be sure to start in the Development environment).

    1
    
    dotnet run
    

    Send the request via cURL.

    1
    
    curl -i -H "Authorization: Bearer {token}" http://localhost:5000/WeatherForecast
    

    As a result, you will get the HTTP/1.1 200 OK response!

    1
    2
    3
    4
    
    HTTP/1.1 403 Forbidden
    Content-Length: 0
    Date: Fri, 28 Apr 2023 14:18:58 GMT
    Server: Kestrel
    
  2. Sign JWT Token containing myapi:secrets Scope

    When issuing a JWT Token, you can restrict the scope of authorization of a specific JWT Token according to the issued scope claim, and of course we need to define the corresponding Policy authorization policy in the program.

    First, declare the policy in the DI container: (Program.cs).

    1
    2
    3
    4
    5
    6
    7
    
    builder.Services.AddAuthentication("Bearer").AddJwtBearer();
    builder.Services.AddAuthorization(options =>
    {
        options.AddPolicy("MyAPIOnly", policy => policy.RequireClaim("scope","myapi:secrets"));
        options.AddPolicy("AdminOnly", policy => policy.RequireClaim(ClaimTypes.Role, "admin"));
        options.AddPolicy("UserOnly", policy => policy.RequireClaim(ClaimTypes.Role, "user"));
    });
    

Create JWT Token and write Claims to scope in Token (only myapi:secrets range)

1
dotnet user-jwts create --scope "myapi:secrets" --role "admin"

Adjust the API Controller to add [Authorize(Roles = "admin, manager", Policy = "MyAPIOnly")] properties.

1
2
3
4
5
6
7
using Microsoft.AspNetCore.Authorization;

[Authorize(Roles = "admin, manager", Policy = "MyAPIOnly")]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{ ... }

Note: The above syntax needs to be interpreted in such a way that the role is either admin or manager and that it also conforms to the MyAPIOnly policy!