Portal Community

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:

CheckClaim / HeaderExpected Value
Algorithmalg headerMust be RS256 — reject anything else
SignatureToken signatureValid RSA-SHA256 using public key from JWKS (matched by kid)
Issuerisshttps://passport.bizfirst.ai
AudienceaudYour client ID (e.g., workflow-api-prod)
ExpiryexpMust be in the future (allow ±60s clock skew)
Issued AtiatMust be in the past — reject tokens with future iat
SubjectsubMust 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 Trust Unvalidated Tokens

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.