using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.Tokens; namespace w4c_workflows.Services; /// /// Validates the main w4c JWT (signed with the shared Auth:JwtSigningKey) and /// extracts the tenant id. Used exclusively by the /api/keys* surface so a /// logged-in tenant can mint/rotate/revoke its own operator keys. /// public static class JwtValidator { static JwtValidator() { // Keep inbound claims exactly as issued ("tenant_id", "sub", ...). // JwtSecurityTokenHandler remaps claims to WS-Federation URIs by // default, which makes FindFirstValue("tenant_id") return null. JwtSecurityTokenHandler.DefaultMapInboundClaims = false; } public static ClaimsPrincipal? Validate( string? authHeader, string signingKey, string? issuer = null, string? audience = null) { if (string.IsNullOrEmpty(signingKey) || string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) return null; try { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)); var parameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = key, // Issuer/audience are enforced only when the deployment declares // them. w4c-auth may not stamp them; a shared symmetric key alone // is weaker, so operators are encouraged to set Auth:JwtIssuer / // Auth:JwtAudience and get the extra binding. ValidateIssuer = !string.IsNullOrWhiteSpace(issuer), ValidIssuer = issuer, ValidateAudience = !string.IsNullOrWhiteSpace(audience), ValidAudience = audience, ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(1), }; return new JwtSecurityTokenHandler().ValidateToken( authHeader["Bearer ".Length..].Trim(), parameters, out _); } catch { return null; } } }