Token Validation
Validate JWT access tokens issued by Passport — verify the RS256 signature using JWKS, check expiry, issuer, and audience before trusting the claims.
IPassportTokenValidator
namespace BizFirst.Essentials.Passport;
public interface IPassportTokenValidator
{
/// <summary>
/// Validates a JWT access token issued by Passport.
/// Verifies: signature (RS256 via JWKS), expiry, issuer, audience.
/// Does NOT check revocation — use token introspection for that.
/// </summary>
Task<TokenValidationResult> ValidateAsync(
string token,
CancellationToken ct = default);
}
public class TokenValidationResult
{
public bool IsValid { get; init; }
public ClaimsPrincipal? Principal { get; init; } // populated if valid
public TokenValidationError? Error { get; init; } // populated if invalid
public DateTimeOffset? ExpiresAt { get; init; }
public string? TokenId { get; init; } // jti claim
}
public enum TokenValidationError
{
SignatureInvalid,
TokenExpired,
IssuerMismatch,
AudienceMismatch,
MalformedToken,
KeyNotFound, // kid in token not in JWKS
ClaimsRequired // required claim missing
}
Validation Checklist
Passport tokens are RS256 JWTs. You MUST validate all of the following before processing claims:
| Check | Claim / Header | Expected Value |
|---|---|---|
| Algorithm | alg header | Must be RS256 — reject anything else |
| Signature | Token signature | Valid RSA-SHA256 using public key from JWKS (matched by kid) |
| Issuer | iss | https://passport.bizfirst.ai |
| Audience | aud | Your client ID (e.g., workflow-api-prod) |
| Expiry | exp | Must be in the future (allow ±60s clock skew) |
| Issued At | iat | Must be in the past — reject tokens with future iat |
| Subject | sub | Must be present — user/service identity |
JWKS Key Fetching
Passport signs tokens with a rotating RSA key pair. The public keys are published at the JWKS URI. Cache the JWKS response (10 minutes recommended) and refresh it when a token references an unknown kid.
GET /passport/.well-known/jwks.json
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "signing-key-2026-05",
"n": "0vx7agoebGcQSuu...base64url-modulus...",
"e": "AQAB"
}
]
}
ASP.NET Core — JWT Bearer Validation
// Program.cs — using Go.Essentials AddPassportAuthentication()
builder.Services.AddPassportAuthentication(options =>
{
options.Authority = builder.Configuration["Passport:Authority"];
options.ClientId = builder.Configuration["Passport:ClientId"];
options.ValidIssuers = new[] { "https://passport.bizfirst.ai" };
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromSeconds(60),
RequireExpirationTime = true,
RequireSignedTokens = true,
// JWKS is auto-fetched from the Authority discovery document
};
});
// Controller — access validated claims
[Authorize]
[ApiController]
public class WorkflowController : ControllerBase
{
[HttpGet("workflows")]
public IActionResult GetWorkflows()
{
// Claims are available from the validated token
var userId = User.FindFirstValue("sub");
var tenantId = User.FindFirstValue("tenant_id");
var roles = User.FindAll("roles").Select(c => c.Value);
return Ok(/* ... */);
}
}
Manual Token Validation (C#)
// Inject IPassportTokenValidator
public class ApiGatewayMiddleware(IPassportTokenValidator validator)
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
if (authHeader?.StartsWith("Bearer ") == true)
{
var token = authHeader["Bearer ".Length..];
var result = await validator.ValidateAsync(token);
if (!result.IsValid)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new
{
error = "invalid_token",
reason = result.Error?.ToString()
});
return;
}
// Attach principal for downstream use
context.User = result.Principal!;
}
await next(context);
}
}
Never decode a JWT and use its claims without first verifying the signature. A JWT is just base64-encoded JSON — anyone can craft one with arbitrary claims. Always validate the RS256 signature against Passport's JWKS before extracting any identity information.
Token Introspection (Revocation Check)
Local JWT validation does not detect revoked tokens (e.g., from admin session termination). For high-security scenarios, use the introspection endpoint:
POST /passport/token/introspect
Authorization: Basic base64(clientId:clientSecret)
Content-Type: application/x-www-form-urlencoded
token=eyJhbGciOiJSUzI1NiJ9...
// Response — token still active
{ "active": true, "sub": "user-guid", "exp": 1748120400, ... }
// Response — token revoked or expired
{ "active": false }
Introspection adds ~20-50ms latency. Use it for admin actions, financial transactions, or other high-stakes operations rather than every API request.