I created a secure API 😎. For authentication/authorization I used JWT (JSON Web Token, pronounced "jot" RFC 7519), but now I expect to have an easy way to test it Β using a token πŸ€”. I have to check and be sure that my implementation rejects incorrect, fake tokens and allows access to API with correct ones with expected scopes. Where can I get an token for free πŸ†“? Absolutely as always, we have an option to write ourself code to generate and sign JWT tokens or we can register a client with scopes in our Identity Server if we have it.

Announcing .NET 7 Preview 5
Announcing .NET 7 Preview 5
https://devblogs.microsoft.com/dotnet/announcing-dotnet-7-preview-5/

I have a good news, on 14th of June this year Microsoft announced .NET 7 Preview 5 and this announcement had a great tool dotnet user-jwts. This tool will help us to create and manage development-time JWTs for project we are building. When I looked at it I was very eager to try it and dive into source code how .NET team implemented it.

Let`s start with easiest part, try it! 🀩

Steps:

  • Install prerequisite tools
  • Create minimal API with JWT authentication/authorization
  • Generate JWTs using dotnet user-jwts
  • Call API using generated JWT

Prerequisites πŸ”¨

Tools you have to install before start:

When completed with installation, execute following commands in your terminal to verify that all is installed correctly.

Check if .NET 7 Preview is installed

dotnet --list-sdks

Expected output (depends on .NET 7 Preview version you installed)

...
7.0.100-preview.6.22352.1 [C:\Program Files\dotnet\sdk]

Check if dotnet user-jwts is installed

dotnet user-jwts

Expected output

Usage: dotnet user-jwts [options] [command]

Options:
  -p|--project  The path of the project to operate on. Defaults to the project in the current directory.
  -h|--help     Show help information

Commands:
  clear   Remove all issued JWTs for a project
  create  Issue a new JSON Web Token
  key     Display or reset the signing key used to issue JWTs
  list    Lists the JWTs issued for the project
  print   Print the details of a given JWT
  remove  Remove a given JWT

Use "dotnet user-jwts [command] --help" for more information about a command.

Create minimal API with JWT authentication / authorization

Create a new ASP.NET Core Web API project and include JWT Bearer authentication library by using following command.

Create minimal API

dotnet new web -n SecureApi
cd SecureApi
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 7.0.0-preview.6.22330.3
Command to create new empty minimal API project and add JWT authentication package.

After creating you should have following directories and files.

D:.
β”‚   appsettings.Development.json
β”‚   appsettings.json
β”‚   Program.cs
β”‚   SecureApi.csproj
β”‚
β”œβ”€β”€β”€obj
β”‚       project.assets.json
β”‚       project.nuget.cache
β”‚       SecureApi.csproj.nuget.dgspec.json
β”‚       SecureApi.csproj.nuget.g.props
β”‚       SecureApi.csproj.nuget.g.targets
β”‚
└───Properties
        launchSettings.json
API directory structure.

Let`s try to build our project and check if nothing fails.

dotnet build

Expected output: βœ… Build succeeded 😊

Open newly created application using Visual Studio Code by File -> Open Folder.

Enable JWT authentication / authorization

Open Program.cs and do following changes. For authorization part we will use policy-based authorization by specifying scope claim as a required with expected value demo.

var builder = WebApplication.CreateBuilder(args);
// Enable JWT authentication
builder.Authentication.AddJwtBearer();
var app = builder.Build();

// Public endpoint ~/api
app.MapGet("/api", 
    () => "Public content!");

// Protected endpoint ~/api/protected with JWT and scope=demo:secrets required policy
app.MapGet("/api/protected", 
    () => "This is a secure content!")
    .RequireAuthorization(
        policy => policy.RequireClaim("scope", "demo:secrets"));

app.Run();
Minimal API with public /api and protected /api/protectedendpoints.

Next we have to specify default authentication scheme for our API to Bearer. Modifyappsettings.json file.

{
  ...
  "Authentication": {
    "DefaultScheme" : "Bearer",
    "Schemes": {
      "Bearer": {
        "Audience": "https://api.craftbakery.dev",
        "ClaimsIssuer": "https://craftbakery.dev"
      }
    }
  }
}
appsettings.json file Authentication section.

We are ready to test our API. Let`s try to run it by executing command dotnet run. Open Thunder Client in Visual Studio Code and call following endpoints GET http://localhost:5035/api and GET http://localhost:5035/api/protected.

The results should be as following.

Public endpoint response

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Sun, 31 Jul 2022 12:38:17 GMT
Server: Kestrel
Transfer-Encoding: chunked

Public content!

Request GET http://localhost:5035/api response.

Protected endpoint response

HTTP/1.1 401 Unauthorized
Content-Length: 0
Date: Sun, 31 Jul 2022 12:41:19 GMT
Server: Kestrel
WWW-Authenticate: Bearer
Request GET http://localhost:5035/api/protected response.

Great! Look`s very good. Public endpoint returned us unprotected content, but protected endpoint returned unauthorized error with response header with required Bearer authentication, because in request we did not specify Authorization header value.

Generate JWTs using dotnet user-jwts

user-jwts is command line tool to manage JWT tokens in a user application. It is written using C# . The source code is available on GitHub repository.

When we first execute create command user-jwts tool will automatically initiate project Secrets Manager. Secrets secrets.json will be used to store token signing keys which will be used for JWT signing. Generated JWTs will be signed using HMAC algorithm. Default token expiration is three months.

To create JWT token we will use create command. To get full list of options execute command dotnet user-jwts create -h.

Options
name demo, audience http://api.craftbakery.dev, scope demo:secrets

dotnet user-jwts create --name 'demo' --audience 'http://api.craftbakery.dev' --scope 'demo:secrets'
Command to create JWT token with audience and scope.
If you specify custom issuer when generate token, JWT Bearer handler will fail on signing key validation IDX10503 when you execute your API.

Output

New JWT saved with ID '60fce0fe'.
Name: demo
Audience(s): http://api.craftbakery.dev
Scopes: demo:secrets

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImRlbW8iLCJzdWIiOiJkZW1vIiwianRpIjoiYzE5M2ZjNDYiLCJzY29wZSI6ImRlbW86c2VjcmV0cyIsImF1ZCI6Imh0dHA6Ly9hcGkuY3JhZnRiYWtlcnkuZGV2IiwibmJmIjoxNjU5Mjk0MTE2LCJleHAiOjE2NjcyNDI5MTYsImlhdCI6MTY1OTI5NDExNywiaXNzIjoiZG90bmV0LXVzZXItand0cyJ9.mK8cOqZJByDfMxJqBiCmjFLT1rGbpk6q-w6_vwjK7Ew
Create command default output.

Command create also modify appsettings.Development.json file by adding Authentication section with values we specified in options.

{
  ...
  "Authentication": {
    "Schemes": {
      "Bearer": {
        "Audiences": [
          "http://api.craftbakery.dev"
        ],
        "ClaimsIssuer": "dotnet-user-jwts"
      }
    },
    "DefaultScheme": "Bearer"
  }
}
appsettings.Development.json file Authentication section.

We can decode generated token using https://jwt.ms/ to see what is inside. You should get similar output.

{
  "alg": "HS256",
  "typ": "JWT"
}.{
  "unique_name": "demo",
  "sub": "demo",
  "jti": "c193fc46",
  "scope": "demo:secrets",
  "aud": "http://api.craftbakery.dev",
  "nbf": 1659294116,
  "exp": 1667242916,
  "iat": 1659294117,
  "iss": "dotnet-user-jwts"
}.[Signature]
Decoded generated JWT token using https://jwt.ms.

Generated data can be found in following locations

  • JWT signing keys stored in - %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json
  • Generated JWT tokens stored in - %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\user-jwts.json

In the preceding file paths, replace <user_secrets_id> with the UserSecretsId value specified in the project file. You can use command list to get full list of generated JWTs for project dotnet use-jtws list.

If you create JWT with same name --name it will corrupt appsettings.Development.json file. Think this will be fixed till release. To fix it just execute command dothet user-jwts clear to clean all generated JWTs.

Call API using generated JWT

At this moments we are ready to call our protected API endpoint GET http://localhost:5035/api/protected. We have to include Authorization: Bearer <token> header in request.

Request

GET /api/protected HTTP/1.1
Accept: */*
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6.......
Connection: keep-alive
Host: localhost:5035

Response

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Server: Kestrel

This is a secure content!

Great! πŸŽ‰We successfully called secure API with JWT token. All default Bearer token validations completed successfully and request was authorized according defined authorization policy. Using this tool we can test our API using different tokens by excluding required scope or add different audience. This allow us to be sure that our API handles correct and incorrect tokens in a right way ❀️. Don`t be afraid to read dotnet source code. There you can find a lot of valuable knowledge.

Happy tokening! πŸ”‘

[eof.]