JWT Testing
> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See API Security.
The Trax.Api.Auth.Jwt.Testing package ships test fixtures for hosts that integration-test Trax JWT authentication without reaching out to a real identity provider. Reference it from test projects only; never deploy it in production.
What it provides
| Type | Purpose |
|---|---|
TestJwksServer | In-process Kestrel host serving an OIDC discovery document and JWKS endpoint. Generates an RSA key pair at startup and publishes the public key. |
TestTokenIssuer | Mints signed JWTs (RS256 via TestJwksServer or HS256 via Symmetric). |
TestTokenBuilder | Fluent builder for individual tokens: subject, claims, roles, audience override, expiry, lifetime. |
TestJwksServer
Start a server, mint a token, send it through your WebApplicationFactory host:
await using var jwks = await TestJwksServer.StartAsync();
services.AddTraxJwtAuth(jwt =>
jwt.UseAuthority(jwks.Issuer, "test-aud")
.AllowHttpMetadata()); // required: TestJwksServer serves HTTP
var token = jwks.CreateIssuer("test-aud").Mint(b =>
b.WithSubject("alice").WithRole("admin"));
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);The server listens on a random loopback port and disposes cleanly. Each instance generates its own key pair, so two servers in the same process have distinct issuers and kids. Properties:
| Member | Description |
|---|---|
Issuer | The iss claim minted tokens carry. Defaults to http://127.0.0.1:{port}; override with TestJwksServerOptions.IssuerOverride. |
JwksUri | {Issuer}/.well-known/jwks.json |
SigningKey | The published RsaSecurityKey for the current key (most-recently-added). |
SigningCredentials | RS256 credentials for the current key. |
SigningKeyIds | List of every kid currently published in the JWKS document. |
SigningCredentialsByKid | Snapshot of every published credential keyed by kid. |
CreateIssuer(audience) | Returns a TestTokenIssuer pre-configured for this server. Calls to WithSigningKey(kid) on the returned issuer switch which key signs. |
AddSigningKey(rsa?) | Publish another signing key and make it current. Returns the assigned kid. |
RemoveSigningKey(kid) | Remove a previously-added key from the JWKS. |
TestJwksServerOptions
| Option | Default | Purpose |
|---|---|---|
ListenUrl | http://127.0.0.1:0 | Kestrel bind URL. Use http://+:8080 to bind a fixed port. |
IssuerOverride | null | Replace the iss value advertised in discovery. Useful when reverse-proxied or when matching a production issuer URL shape. |
PathPrefix | "" | Mount JWKS and discovery under a path prefix (e.g. /local_us-east-1_xxx). Must start with / if non-empty. |
JWKS rotation
await using var jwks = await TestJwksServer.StartAsync();
// Publish a second key. New tokens sign with this one by default;
// the original key stays in the JWKS so previously-issued tokens
// still validate.
var newKid = jwks.AddSigningKey();
var issuer = jwks.CreateIssuer("test-aud");
var oldToken = issuer.WithSigningKey(jwks.SigningKeyIds.First(k => k != newKid))
.Mint(b => b.WithSubject("alice"));
var newToken = issuer.Mint(b => b.WithSubject("bob")); // signed with newKid
// Retire the old key. Tokens signed with it now fail validation.
jwks.RemoveSigningKey(jwks.SigningKeyIds.First(k => k != newKid));The JWT bearer handler refreshes its JWKS cache on kid miss, so tokens signed with a freshly-added key validate without restarting the host.
TestTokenIssuer
Mints individual tokens. Construct via TestJwksServer.CreateIssuer for RS256, or TestTokenIssuer.Symmetric for HS256:
// HS256 for internal-token tests:
var keyBytes = Encoding.UTF8.GetBytes(new string('k', 32));
var issuer = TestTokenIssuer.Symmetric("nwyc-web", "nwyc-trax", keyBytes);
var token = issuer.Mint(b =>
b.WithSubject("alice")
.WithClaim("subscription_tier", "pro")
.WithLifetime(TimeSpan.FromMinutes(10)));TestTokenBuilder methods
| Method | Effect |
|---|---|
WithSubject(sub) | Sets the sub claim (replaces any previous value). |
WithClaim(type, value) | Appends a claim. Multiple calls with the same type append, do not replace. |
WithRole(role) | Appends a ClaimTypes.Role claim. |
WithAudience(aud) | Overrides the audience for this token only. |
WithIssuer(iss) | Overrides the issuer for this token only. |
WithNotBefore(dt) | Sets the nbf timestamp explicitly. |
WithExpires(dt) | Sets the exp timestamp explicitly. |
WithLifetime(timespan) | Sets nbf to one minute ago and exp to now + timespan. |
Defaults: nbf is one minute ago, exp is five minutes from now.
Test patterns
Token a real handler would reject (wrong audience, wrong signature, expired) is what you exercise to prove the negative path:
// Token signed with a different key than the JwksServer publishes.
var rogueKey = RSA.Create(2048);
var rogueCreds = new SigningCredentials(new RsaSecurityKey(rogueKey), SecurityAlgorithms.RsaSha256);
var rogueIssuer = new TestTokenIssuer(jwks.Issuer, "test-aud", rogueCreds);
var badToken = rogueIssuer.Mint(b => b.WithSubject("intruder"));
// Server should return 401: signature mismatch.Expired token:
var token = jwks.CreateIssuer("test-aud").Mint(b =>
b.WithSubject("alice").WithExpires(DateTime.UtcNow.AddMinutes(-5)));JWKS rotation in place: call AddSigningKey() on a running server, mint with the new key, send through the same host. The bearer handler refreshes its JWKS cache on kid miss and accepts the new token. See the rotation snippet above.
Cognito-shaped tokens: pair the server with CognitoTokenIssuer via jwks.CreateCognitoIssuer() to mint tokens that round-trip through UseCognito to a Cognito-shaped TraxPrincipal.
Caveats
TestJwksServerserves over plain HTTP. Tests must callAllowHttpMetadata()on theJwtBuilderso the bearer handler accepts the non-HTTPS authority.- The server runs Kestrel in-process. It cannot share a port with another host; each test owning its own server is the expected pattern.
- HS256 keys via
TestTokenIssuer.Symmetricmust be at least 32 bytes (HS256 minimum).
SDK Reference
> AddTraxJwtAuth | UseCognito | Cognito Issuer | AddTraxJwtDispatcher