w4c-workflows-api/Services/JwtValidator.cs
2026-09-13 11:35:17 +03:00

61 lines
2.2 KiB
C#

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace w4c_workflows.Services;
/// <summary>
/// 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.
/// </summary>
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;
}
}
}