AuthServer
Token Validation
Validate access tokens for protected APIs.
Access tokens are RS256 JWTs. Validate on the server for each API call and always require the audience for the API that will process the token. SqlOS SDK/middleware validation is session-aware; plain JWKS validation is not.
SqlOS validation is stateful: after signature, issuer, audience, and JWT lifetime checks, it loads the current session and requires that the session is not revoked or idle/absolutely expired. It also requires an active user and, for organization-scoped tokens, an active organization and membership. This is what makes logout and offboarding visible before the JWT's exp time.
using SqlOS.AuthServer.Extensions;
app.UseSqlOSAccessTokenValidation(options =>
{
options.ExpectedAudience = "https://api.example.com";
options.ShouldValidate = http => http.Request.Path.StartsWithSegments("/api");
});The middleware rejects requests at startup if ExpectedAudience is empty. On successful authentication it sets HttpContext.User and stores the SqlOSValidatedToken on the current request:
var validated = httpContext.GetSqlOSValidatedToken();
var clientId = validated?.ClientId;
var audience = validated?.Audience;Use GetSqlOSValidatedToken() for token diagnostics or auth-server metadata. Your app should still resolve the current FGA subject explicitly from the authenticated principal or from its own API-key/session mapping.
using System.IdentityModel.Tokens.Jwt;
var bearerToken = httpContext.Request.Headers.Authorization.ToString();
if (!bearerToken.StartsWith("Bearer "))
return Results.Unauthorized();
var expectedAudience = "https://api.example.com";
var validated = await authService.ValidateAccessTokenAsync(
bearerToken["Bearer ".Length..].Trim(),
expectedAudience,
ct);
if (validated == null)
return Results.Unauthorized();
var subjectId = validated.Principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
if (string.IsNullOrWhiteSpace(subjectId))
return Results.Unauthorized();
var orgId = validated.OrganizationId;ValidateAccessTokenAsync validates issuer, signature, JWT lifetime, exact audience, and that the referenced SqlOS session exists and is not revoked or absolutely expired. It does not apply the session idle timeout to each already-issued access token; idle expiry is enforced when the client refreshes.
SqlOS FGA APIs take explicit subjectId values. For bearer tokens, a typical app-owned resolver reads the JWT subject claim from the authenticated principal:
using System.IdentityModel.Tokens.Jwt;
var subjectId = http.User.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
if (string.IsNullOrWhiteSpace(subjectId))
return Results.Unauthorized();
await db.ProvisionUserSubjectAsync(
subjectId,
displayName: subjectId,
cancellationToken: ct);If your app accepts API keys, agent tokens, or another credential type, resolve those credentials to the relevant service-account or agent subject ID in your own request layer, then pass that explicit subject ID to FGA.
ValidateAccessTokenWithoutAudienceForIntrospectionOnlyAsync exists only for diagnostics and token introspection flows that do not authenticate a protected API request. It validates issuer, signature, lifetime, and session existence/revocation/absolute expiry, but it intentionally does not validate aud.
Validation keeps the session check online for every request, so session revocation and absolute expiry take effect immediately. SqlOS reduces database churn by caching public validation keys briefly and by persisting session/client LastSeenAt at most once per configured debounce interval. Configure these intervals with AccessTokenValidationSigningKeyCacheTtl and AccessTokenValidationLastSeenDebounceInterval on AuthServer.
When a token names a kid missing from a replica's cache, SqlOS performs an authoritative refresh shared by concurrent validations. This lets a token issued immediately after rotation validate on another healthy replica without waiting for the normal cache TTL. Unknown-key refreshes are rate-limited across identifiers, retained negative identifiers are bounded, and an unsuccessful lookup does not extend the original cache expiry, so attacker-selected values cannot create an unbounded SQL or memory workload. If SQL is unavailable during the refresh, SqlOS keeps the last known public keys, logs the failure, and rejects the unknown key; it never accepts a token without the configured issuer, audience, algorithm, and signature checks.
External services can validate SqlOS JWTs without calling the SDK by using the JWKS endpoint:
GET /sqlos/auth/.well-known/jwks.jsonAnd the OAuth metadata endpoint:
GET /sqlos/auth/.well-known/oauth-authorization-serverJWKS-only validation is necessarily stateless. It cannot observe a SqlOS session revocation, idle expiry, user deactivation, organization deactivation, or membership removal until the JWT expires. Use UseSqlOSAccessTokenValidation, RequireSqlOSAccessToken, or ValidateAccessTokenAsync when immediate lifecycle enforcement matters. If a resource server must remain JWKS-only, keep access-token lifetimes short and treat that delay as an explicit deployment tradeoff.
| Claim | Description |
|---|---|
sub | User subject ID |
sid | Session ID |
client_id | OAuth client |
org_id | Organization (if scoped) |
iss | Issuer URL |
aud | Audience |
exp | Expiration |