Skip to content

JWT and OIDC

Install the Microsoft.AspNetCore.Authentication.JwtBearer NuGet Package.

In the Program.cs:

builder.Services.AddAuthentication().AddJwtBearer(options =>
{
options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
});
builder.Services.AddAuthorization();

Go to a terminal in your project’s working directory and run:

Terminal window
dotnet user-jwts create
dotnet user-jwts create -n bob@aol.com
dotnet user-jwts create -n sue@aol.com --role Admin --role Boss
dotnet user-jwts create -n carl@aol.com --role Reader --valid-for 1095d

(the last example makes the token valid for three years.)

This will update your appsettings.development.json:

{
"Authentication": {
"Schemes": {
"Bearer": {
"ValidAudiences": [
"http://localhost:35191",
"https://localhost:0",
"http://localhost:5008"
],
"ValidIssuer": "dotnet-user-jwts"
}
}
}
}

Add To Swagger / OpenAPI

builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header with bearer token",
Name = "Authorization",
In = ParameterLocation.Header,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Id = "Bearer",
Type = ReferenceType.SecurityScheme
},
Scheme = "oauth2",
Name = "Bearer ",
In = ParameterLocation.Header
},
[]
}
});
});

In an HTTP File

# For more info on HTTP files go to https://aka.ms/vs/httpfile
@host=localhost
@port=5008
# dotnet user-jwts create -n carl@aol.com --role Reader --valid-for 1095d
@carlToken=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImNhcmxAYW9sLmNvbSIsInN1YiI6ImNhcmxAYW9sLmNvbSIsImp0aSI6IjhmYzQ1ZTY0Iiwicm9sZSI6IlJlYWRlciIsImF1ZCI6WyJodHRwOi8vbG9jYWxob3N0OjM1MTkxIiwiaHR0cHM6Ly9sb2NhbGhvc3Q6MCIsImh0dHA6Ly9sb2NhbGhvc3Q6NTAwOCJdLCJuYmYiOjE3MTQ1NjQwMzcsImV4cCI6MTgwOTE3MjAzNywiaWF0IjoxNzE0NTY0MDM3LCJpc3MiOiJkb3RuZXQtdXNlci1qd3RzIn0.V2TtyKtrxbyL5iRf2gax8122TPwgEspb0k7DZXEPgok
# dotnet user-jwts create -n sue@aol.com --role Admin --role Boss
@sueToken=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6InN1ZUBhb2wuY29tIiwic3ViIjoic3VlQGFvbC5jb20iLCJqdGkiOiJkYTVhYzY3ZiIsInJvbGUiOlsiQWRtaW4iLCJCb3NzIl0sImF1ZCI6WyJodHRwOi8vbG9jYWxob3N0OjM1MTkxIiwiaHR0cHM6Ly9sb2NhbGhvc3Q6MCIsImh0dHA6Ly9sb2NhbGhvc3Q6NTAwOCJdLCJuYmYiOjE3MTQ1NjUyMDAsImV4cCI6MTcyMjUxNDAwMCwiaWF0IjoxNzE0NTY1MjAwLCJpc3MiOiJkb3RuZXQtdXNlci1qd3RzIn0.3ksg9uDVqypqSnnZcheYrE_9bKEs7tZd1166tL4ViNw
GET http://{{host}}:{{port}}/tacos
Authorization: {{sueToken}}
###
# This will get a 403 - Forbidden (we know who you are, and no.)
GET http://{{host}}:{{port}}/tacos
Authorization: {{carlToken}}
###
# This will get a 401 - Unauthorized (We don't know who you are)
GET http://{{host}}:{{port}}/tacos

Accessing Claims in Middleware (Wolverine)

public static class UserDetectionMiddleware
{
public static async Task<(UserData?, ProblemDetails)> Load(ClaimsPrincipal principal,IDocumentSession session)
{
var userIdClaim = principal.FindFirst(ClaimTypes.NameIdentifier);
if (userIdClaim is not null)
{
var savedUser = await session.Query<User>().Where(u => u.Sub == userIdClaim.Value).SingleOrDefaultAsync();
if (savedUser is null)
{
var id = Guid.NewGuid();
session.Events.StartStream(id, new UserCreated(userIdClaim.Value));
await session.SaveChangesAsync();
return (new UserData(id, userIdClaim.Value), WolverineContinue.NoProblems);
}
else
{
return (new UserData(savedUser.Id, userIdClaim.Value), WolverineContinue.NoProblems);
}
}
return (null, new ProblemDetails { Detail = "No User", Status = 400 });
}
}

Adding Authorization Policies

See Policy-based authorization in ASP.NET Core | Microsoft Learn

builder.Services.AddAuthorization(options =>
{
options.AddPolicy("IsAdmin", policy => policy.RequireRole("Admin"));
});

Use with Minimal API:

app.MapGet("/tacos", () =>
{
return "Delicious";
}).RequireAuthorization("IsAdmin");

For Controller Methods, you can add the Authorize attribute:

[Authorize(Roles = "Admin", Policy = "IsCool")]

Stubbing with Alba

api.WithClaim(new Claim(ClaimTypes.NameIdentifier ,"jeff@hypertheory.com")); // adds "sub" claim
api.WithClaim(new Claim(ClaimTypes.Role, "Admin"));
api.WithClaim(new Claim(ClaimTypes.Role, "User"));
use the mock OAuth2 / OIDC server from here: [navikt/mock-oauth2-server: A scriptable/customizable web server for testing HTTP clients using OAuth2/OpenID Connect or applications with a dependency to a running OAuth2 server (i.e. APIs requiring signed JWTs from a known issuer) (github.com)](https://github.com/navikt/mock-oauth2-server)
```yml
services:
auth:
image: ghcr.io/navikt/mock-oauth2-server:2.1.9
ports:
- 9090:8080

Add the Microsoft.AspNetCore.Authentication.JwtBearer NuGet.

Configuring the Auth:

In your appsettings.Development.json:

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Authentication": {
"Schemes": {
"Bearer": {
"MetadataAddress": "http://localhost:9090/default/.well-known/openid-configuration",
"RequireHttpsMetadata": false,
"ValidateAudience": false
}
}
}
}

In Program.cs

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
// after build
app.UseAuthentication();
app.UseAuthorization();

Example Endpoint:

app.MapGet("/sayhi", (ClaimsPrincipal principal) =>
{
var user = principal.FindFirstValue(ClaimTypes.NameIdentifier);
return $"Hello, {user}";
})
.WithOpenApi().RequireAuthorization();
}

Generating Tokens with a Bash Script

#!/bin/bash
# Check if name argument is provided
if [ -z "$1" ]; then
echo "Usage: $0 <name> [role1] [role2] ..."
echo "Example: $0 boss@company.com SoftwareCenter Admin"
exit 1
fi
NAME=$1
shift # Remove the first argument (name) so we can process the rest as roles
echo "Generating token for $NAME"
# Detect OS and set clipboard command
if [[ "$OSTYPE" == "darwin"* ]]; then
CLIP_CMD="pbcopy"
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
CLIP_CMD="clip"
else
CLIP_CMD="xclip -selection clipboard" # Linux fallback
fi
# Build the dotnet command with roles
CMD="dotnet user-jwts create -n $NAME"
# Add each role as a --role argument
for role in "$@"; do
CMD="$CMD --role $role"
done
# Execute the command and copy token to clipboard
eval $CMD | grep 'Token:' | awk '{print $2}' | $CLIP_CMD
echo "Token for $NAME is in your clipboard."