How-to guide

Write a JWT principal resolver

A JWT scheme maps each validated token to a TraxPrincipal through an ITraxPrincipalResolver<JwtTokenInput>. Without one it uses DefaultJwtPrincipalResolver, whose claim mapping is on AddTraxJwtAuth.

Check whether you need one

Google, Auth0, Entra, Cognito and Okta emit sub, name and role claims the default resolver reads, so a host that only needs the token's own identity writes no resolver:

using Trax.Api.Auth.Jwt;
 
services.AddTraxJwtAuth("https://login.example.com", "my-api");

Write one when:

  • roles live in a claim the default does not read: Entra app roles, Okta group URIs, namespaced Auth0 claims;
  • the principal needs data the token does not carry: a tenant, permissions, feature flags;
  • unknown or suspended subjects must be refused: an allow-list, a revocation check;
  • claims need reshaping: a namespace prefix stripped, several role sources merged.

For Cognito tokens, CognitoJwtPrincipalResolver already reads groups and federated identities (UseCognito).

Write the resolver

Read the validated claims from input.Principal, look the subject up, and return null to refuse it. null fails authentication:

using Trax.Api.Auth;
using Trax.Api.Auth.Jwt;
 
public sealed record User(string Id, string DisplayName, string[] Roles, string TenantId, bool IsSuspended);
 
public interface IUsers
{
    Task<User?> FindAsync(string id, CancellationToken ct);
}
 
public sealed class TenantJwtResolver(IUsers users) : ITraxPrincipalResolver<JwtTokenInput>
{
    public async ValueTask<TraxPrincipal?> ResolveAsync(JwtTokenInput input, CancellationToken ct)
    {
        var sub = input.Principal.FindFirst("sub")?.Value;
        if (sub is null)
            return null;
 
        var user = await users.FindAsync(sub, ct);
        if (user is null || user.IsSuspended)
            return null;
 
        return new TraxPrincipal(user.Id, user.DisplayName, user.Roles,
            Claims: new Dictionary<string, string> { ["tenant"] = user.TenantId },
            PrincipalType: JwtDefaults.PrincipalType);
    }
}

Return the subject as Id, unqualified: the scheme adds its own name, so the principal's id becomes TraxJwt:{sub}.

Register it

Pass it as the type argument. It is registered scoped, so it can take a DbContext or other scoped services:

using Trax.Api.Auth.Jwt;
 
services.AddTraxJwtAuth<TenantJwtResolver>(jwt => jwt.UseAuthority("https://login.example.com", "my-api"));

On a host with several issuers, each named scheme takes its own resolver: AddTraxJwtAuth<TResolver>(schemeName, jwt => ...).

SDK Reference

AddTraxJwtAuth | ITraxPrincipalResolver | TraxPrincipal