Manage JSON Web Tokens in development with dotnet user-jwts

By Rick Anderson

The dotnet user-jwts command line tool can create and manage app specific local JSON Web Tokens (JWTs).

Synopsis

dotnet user-jwts [<PROJECT>] [command]
dotnet user-jwts [command] -h|--help

Description

Creates and manages project specific local JSON Web Tokens.

Arguments

PROJECT | SOLUTION

The MSBuild project to apply a command on. If a project is not specified, MSBuild searches the current working directory for a file that has a file extension that ends in proj and uses that file.

Commands

Command Description
clear Delete all issued JWTs for a project.
create Issue a new JSON Web Token.
remove Delete a given JWT.
key Display or reset the signing key used to issue JWTs.
list Lists the JWTs issued for the project.
print Display the details of a given JWT.

Create

Usage: dotnet user-jwts create [options]

Option Description
-p | --project The path of the project to operate on. Defaults to the project in the current directory.
--scheme The scheme name to use for the generated token. Defaults to 'Bearer'.
-n | --name The name of the user to create the JWT for. Defaults to the current environment user.
--audience The audiences to create the JWT for. Defaults to the URLs configured in the project's launchSettings.json.
--issuer The issuer of the JWT. Defaults to 'dotnet-user-jwts'.
--scope A scope claim to add to the JWT. Specify once for each scope.
--role A role claim to add to the JWT. Specify once for each role.
--claim Claims to add to the JWT. Specify once for each claim in the format "name=value".
--not-before The UTC date & time the JWT should not be valid before in the format 'yyyy-MM-dd [[HH:mm[[:ss]]]]'. Defaults to the date & time the JWT is created.
--expires-on The UTC date & time the JWT should expire in the format 'yyyy-MM-dd [[[ [HH:mm]]:ss]]'. Defaults to 6 months after the --not-before date. Do not use this option in conjunction with the --valid-for option.
--valid-for The period the JWT should expire after. Specify using a number followed by duration type like 'd' for days, 'h' for hours, 'm' for minutes, and 's' for seconds, e.g. 365d'. Do not use this option in conjunction with the --expires-on option.
-o | --output The format to use for displaying output from the command. Can be one of 'default', 'token', or 'json'.
-h | --help Show help information

Examples

Run the following commands to create an empty web project and add the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package:

dotnet new web -o MyJWT
cd MyJWT
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Replace the contents of Program.cs with the following code:

using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/", () => "Hello, World!");
app.MapGet("/secret", (ClaimsPrincipal user) => $"Hello {user.Identity?.Name}. My secret")
    .RequireAuthorization();

app.Run();

In the preceding code, a GET request to /secret returns an 401 Unauthorized error. A production app might get the JWT from a Security token service (STS), perhaps in response to logging in via a set of credentials. For the purpose of working with the API during local development, the dotnet user-jwts command line tool can be used to create and manage app-specific local JWTs.

The user-jwts tool is similar in concept to the user-secrets tool, it can be used to manage values for the app that are only valid for the developer on the local machine. In fact, the user-jwts tool utilizes the user-secrets infrastructure to manage the key that the JWTs are signed with, ensuring it’s stored safely in the user profile.

The user-jwts tool hides implementation details, such as where and how the values are stored. The tool can be used without knowing the implementation details. The values are stored in a JSON file in the local machine's user profile folder:

File system path:

%APPDATA%\Microsoft\UserSecrets\<secrets_GUID>\user-jwts.json

Create a JWT

The following command creates a local JWT:

dotnet user-jwts create

The preceding command creates a JWT and updates the project’s appsettings.Development.json file with JSON similar to the following:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Authentication": {
    "Schemes": {
      "Bearer": {
        "ValidAudiences": [
          "http://localhost:8401",
          "https://localhost:44308",
          "http://localhost:5182",
          "https://localhost:7076"
        ],
        "ValidIssuer": "dotnet-user-jwts"
      }
    }
  }
}

Copy the JWT and the ID created in the preceding command. Use a tool like Curl to test /secret:

curl -i -H "Authorization: Bearer {token}" https://localhost:{port}/secret

Where {token} is the previously generated JWT.

Display JWT security information

The following command displays the JWT security information, including expiration, scopes, roles, token header and payload, and the compact token:

dotnet user-jwts print {ID} --show-all

Create a token for a specific user and scope

See Create in this topic for supported create options.

The following command creates a JWT for a user named MyTestUser:

dotnet user-jwts create --name MyTestUser --scope "myapi:secrets"

The preceding command has output similar to the following:

New JWT saved with ID '43e0b748'.
Name: MyTestUser
Scopes: myapi:secrets

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.{Remaining token deleted}

The preceding token can be used to test the /secret2 endpoint in the following code:

using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();

var app = builder.Build();

app.MapGet("/", () => "Hello, World!");
app.MapGet("/secret", (ClaimsPrincipal user) => $"Hello {user.Identity?.Name}. My secret")
    .RequireAuthorization();
app.MapGet("/secret2", () => "This is a different secret!")
    .RequireAuthorization(p => p.RequireClaim("scope", "myapi:secrets"));

app.Run();