How to issue JWT Web tokens locally using dotnet user-jwts?
I recently got a task to implement standard OpenIdConnect/OAuth authentication/authorization with JWT Web tokens to a .NET 8-based API. It's been a while since when I last configured this, so I needed to check a few things first. While searching I spotted an article about generating JWT tokens with built-in dotnet CLI tool. How have I missed this? This is super useful because you can generate tokens locally on your machine. You have full freedom to adjust scopes and claims as you like.
There are already blog posts and articles available about this topic. This blog post is written in the spirit of lessons learned what I discovered and what wasn't mentioned in other mediums.
Glossary
Before starting it's good to define some key terms which are used in this blog post.
JSON Web tokens (JWT)
JSON Web Tokens (JWT) are an open standard (RFC 7519) that defines a compact and self-contained way to securely transmit information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.
Source: JSON Web Token Introduction - jwt.io
Authority
Authority is the address of the token-issuing authentication server. The JWT bearer authentication middleware will use this URI to find and retrieve the public key that can be used to validate the token's signature.
Source: JWT Validation and Authorization in ASP.NET Core - .NET Blog (microsoft.com)
Issuer
OpenID Connect specifies the iss field to be used in ID Tokens in order to describe who the issuer of the token is. The issuer is the one that assembles the token and signs it (for signed JWTs). The iss field should be a HTTPS URL pointing to the Identity Provider. If a token is received with issuer address other than the Identity Provider currently in use, it will have a different authority.
Source: The Claims Authority and its Role in Issuing Claims | Curity
Token signing
JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
Source: JSON Web Token Introduction - jwt.io
What is dotnet user-jwts tool and why should you use it?
The dotnet user-jwts is a command line tool that creates local app-specific JSON Web Tokens (JWTs).
The biggest benefits of using dotnet user-jwts are flexibility and increased development experience.
The tool can highly increase the development experience because developers can quickly create and adjust tokens locally without issuing tokens from the external server which always takes some time. Typically tokens issued by the external server are short-handed which means that the developer needs to issue tokens multiple times during the e.g. API testing (clients credential flow). Developers can save a nice amount of time just by creating a local token with specific claims and a long expiration time.
Commands
See all commands from here. I listed here the most common commands that I have used.
Issue a new JSON Web Token
dotnet user-jwts create
When the first token is issued command line tool first creates the signing key which is used to sign JSON Web Tokens. The tool uses Secret Manager to persist the signing key and JSON Web Tokens in JSON files in the local machine's user profile folder. User secret Id is defined in the Visual Studio project settings which maps the specific project to files created by Secret Manager.
Signing key: %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json
JSON Web Tokens: %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\user-jwts.json
Issued JSON Web token is always project-specific. When the create command is executed the tool automatically creates the following app settings configuration for a project with a valid audience and issuer.
"Authentication": {
"Schemes": {
"Bearer": {
"ValidAudiences": [
"http://localhost:64755",
"https://localhost:44379",
"http://localhost:5048",
"https://localhost:7103"
],
"ValidIssuer": "dotnet-user-jwts"
}
}
}
Issue a new JSON Web Token with custom sub, scope, and claim
dotnet user-jwts create -n kalle@test.local --scope "api.read" --claim "role=admin"
Display or reset the signing key used to issue JWTs
dotnet user-jwts key
Lists the JWTs issued for the project
dotnet user-jwts list
Display the details of a given JWT
dotnet user-jwts print [ID]
First iteration
I decided to first test how this works as introduced in the Microsoft article.
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();
This worked smoothly. However, I want to configure explicitly authentication-related settings in the code instead of using all settings from app settings. Let's try this in the next iteration.
Second iteration
As said I want to explicitly configure authentication in a code like this.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var authority = builder.Configuration.GetValue<string>("Authentication:Schemes:Bearer:ValidAuthority");
var audiences = builder.Configuration.GetSection("Authentication:Schemes:Bearer:ValidAudiences").Get<string[]>();
var issuer = builder.Configuration.GetValue<string>("Authentication:Schemes:Bearer:ValidIssuer");
builder.Services
.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
})
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = authority;
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
ValidAudiences = audiences,
ValidIssuer = issuer,
RequireSignedTokens = true
};
options.TokenValidationParameters = tokenValidationParameters;
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
But this didn't work. I started to receive the following error:
Bearer error="invalid_token", error_description="The signature key was not found".
Yeah, let's add the IssuerSigningKey for TokenValidationParameters so this should fix the problem, right? I copy-pasted IssuerSigningKey to the app settings configuration from dotnet user-jwts key command.
if (builder.Environment.IsDevelopment())
{
// This is for dotnet user-jwts
var signingKey = builder.Configuration.GetValue<string>("Authentication:Schemes:Bearer:SigningKey");
if (!string.IsNullOrEmpty(signingKey))
{
tokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
}
}
No, still the same error.
After a couple of hours of investigation without idea, I decided to take a look dotnet user-jwts's source code. Hopefully, I could find something to help fix this issue. Yeah, I found new clues. I noticed from the KeyCommand class that the output is converted to Base 64. That must be the reason.
reporter.Output(Resources.FormatKeyCommand_Confirmed(Convert.ToBase64String(signingKeyMaterial)));
Let's add the one-liner to convert from the Base64 string.
if (builder.Environment.IsDevelopment())
{
// This is for dotnet user-jwts
var signingKey = builder.Configuration.GetValue<string>("Authentication:Schemes:Bearer:SigningKey");
if(!string.IsNullOrEmpty(signingKey))
{
var signingKeyBytes = Convert.FromBase64String(signingKey);
tokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(signingKeyBytes);
}
}
Yes, that was the trick. Now it works also with this code-based configuration.
Other alternatives
I found that there is also a similar tool available called Phoesion.DevJwt. There is also Nuget package available so you can generate tokens programmatically for different kinds of testing scenarios.
Comments