Token Validation
Overview
Section titled “Overview”Token validation is the core function of @authrim/server. The validateToken() method performs a complete validation pipeline — parsing the JWT, fetching the signing key, verifying the signature, and checking all standard claims.
validateToken API
Section titled “validateToken API”const result = await authrim.validateToken(token);Parameters
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
token | string | The raw JWT access token (without the Bearer prefix) |
Additional validation rules are configured on the server instance with tokenValidation. |
TokenValidationOptions
Section titled “TokenValidationOptions”interface TokenValidationOptions { clockToleranceSeconds?: number; requiredScopes?: string[]; validateDPoP?: boolean; tenantClaim?: string; requireTenantClaim?: boolean; requiredTenantId?: string; allowedTenantIds?: string[]; tenantPredicate?: ( tenantId: string, claims: AccessTokenClaims, ) => boolean | Promise<boolean>;}| Option | Type | Description |
|---|---|---|
clockToleranceSeconds | number | Clock skew tolerance for standard claim checks |
requiredScopes | string[] | Scopes that must be present in the token’s scope claim |
validateDPoP | boolean | Reserved for DPoP-bound token validation policy in middleware integrations |
tenantClaim | string | Claim name that carries the tenant id. Defaults to tenant_id |
requireTenantClaim | boolean | Reject tokens that do not contain a tenant claim |
requiredTenantId | string | Require the token tenant to match this exact tenant id |
allowedTenantIds | string[] | Require the token tenant to be one of these tenant ids |
tenantPredicate | (tenantId, claims) => boolean | Promise<boolean> | Custom tenant predicate for advanced ABAC/ReBAC checks |
Return Value: TokenValidationResult
Section titled “Return Value: TokenValidationResult”validateToken() returns a discriminated result object. It does not throw for normal validation failures.
type TokenValidationResult = | { data: ValidatedToken; error: null } | { data: null; error: { code: string; message: string } };When validation succeeds, data contains the validated token:
interface ValidatedToken { claims: AccessTokenClaims; token: string; issuer: string; tenantId?: string; tokenType: 'Bearer' | 'DPoP'; expiresIn?: number;}| Field | Type | Description |
|---|---|---|
claims | AccessTokenClaims | Parsed and validated JWT claims |
token | string | The original token string |
issuer | string | Validated issuer |
tenantId | string | undefined | Validated tenant id when a tenant claim is present |
tokenType | 'Bearer' | 'DPoP' | Detected token type based on claims |
expiresIn | number | undefined | Seconds until the token expires (calculated from exp) |
AccessTokenClaims
Section titled “AccessTokenClaims”interface AccessTokenClaims { iss: string; sub: string; aud: string | string[]; exp: number; iat: number; nbf?: number; jti?: string; scope?: string; client_id?: string; cnf?: { jkt?: string }; [key: string]: unknown; // Custom claims}Validation Pipeline
Section titled “Validation Pipeline”The SDK validates tokens through the following pipeline. Each step must pass for the token to be accepted:
flowchart TD
A["Receive JWT String"] --> B["Size Check<br>(max 8 KB)"]
B --> C["Parse JWT<br>(Header + Payload + Signature)"]
C --> D["Algorithm Check<br>(reject alg: none)"]
D --> E["Fetch Signing Key<br>(JWKS by kid)"]
E --> F["Verify Signature<br>(RS/PS/ES/EdDSA)"]
F --> G["Validate iss<br>(timing-safe)"]
G --> H["Validate aud<br>(timing-safe)"]
H --> I["Validate exp<br>(+ clock tolerance)"]
I --> J["Validate nbf<br>(+ clock tolerance)"]
J --> K["Validate iat<br>(not in future)"]
K --> L["Check Required Scopes"]
L --> M["Check Tenant Policy"]
M --> N["Detect Token Type<br>(Bearer vs DPoP)"]
N --> O["Return TokenValidationResult"]
If any step fails, the SDK returns { data: null, error } with a specific error code. See the Error Types section below.
Configure scope and tenant validation when you create the server:
const authrim = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com', tokenValidation: { requiredScopes: ['orders:read'], requireTenantClaim: true, allowedTenantIds: ['tenant_a', 'tenant_b'], },});Supported Algorithms
Section titled “Supported Algorithms”The SDK supports the following JWS algorithms for signature verification:
| Algorithm | Key Type | Curve / Size | Description |
|---|---|---|---|
| RS256 | RSA | 2048+ bits | RSASSA-PKCS1-v1_5 with SHA-256 |
| RS384 | RSA | 2048+ bits | RSASSA-PKCS1-v1_5 with SHA-384 |
| RS512 | RSA | 2048+ bits | RSASSA-PKCS1-v1_5 with SHA-512 |
| PS256 | RSA | 2048+ bits | RSASSA-PSS with SHA-256 |
| PS384 | RSA | 2048+ bits | RSASSA-PSS with SHA-384 |
| PS512 | RSA | 2048+ bits | RSASSA-PSS with SHA-512 |
| ES256 | EC | P-256 | ECDSA with SHA-256 |
| ES384 | EC | P-384 | ECDSA with SHA-384 |
| ES512 | EC | P-521 | ECDSA with SHA-512 |
| EdDSA | OKP | Ed25519 | Edwards-curve Digital Signature |
Claims Validation Details
Section titled “Claims Validation Details”Issuer (iss)
Section titled “Issuer (iss)”The iss claim must exactly match one of the configured issuer values. Comparison uses a timing-safe algorithm to prevent side-channel attacks.
// Single issuerconst authrim = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com',});
// Multiple issuersconst authrim = createAuthrimServer({ issuer: [ 'https://auth.example.com', 'https://auth.partner.com', ], audience: 'https://api.example.com',});Audience (aud)
Section titled “Audience (aud)”The aud claim must contain at least one of the configured audience values. The aud claim can be a single string or an array of strings. All comparisons are timing-safe.
// Token with aud: "https://api.example.com" — matches// Token with aud: ["https://api.example.com", "other"] — matches// Token with aud: "https://other.example.com" — rejectedExpiration (exp)
Section titled “Expiration (exp)”The exp claim is checked against the current time plus clockToleranceSeconds:
token is valid if: exp + clockToleranceSeconds > nowWith the default tolerance of 60 seconds, a token that expired up to 60 seconds ago is still accepted.
Not Before (nbf)
Section titled “Not Before (nbf)”If present, the nbf claim is checked against the current time minus clockToleranceSeconds:
token is valid if: nbf - clockToleranceSeconds <= nowIssued At (iat)
Section titled “Issued At (iat)”The iat claim is sanity-checked to ensure the token was not issued in the future (with clock tolerance):
token is valid if: iat - clockToleranceSeconds <= nowScope Validation
Section titled “Scope Validation”Enforce required scopes using the requiredScopes option:
// Require specific scopesconst authrim = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com', tokenValidation: { requiredScopes: ['read:users', 'write:users'], },});The SDK checks that all specified scopes are present in the token’s scope claim (space-delimited string). If any required scope is missing, validateToken() returns an error with code insufficient_scope.
Bearer vs DPoP Auto-Detection
Section titled “Bearer vs DPoP Auto-Detection”The SDK automatically detects the token type based on the cnf (confirmation) claim:
- If the token contains
cnf.jkt(JWK Thumbprint), the token is classified asDPoP - Otherwise, the token is classified as
Bearer
const result = await authrim.validateToken(token);
if (result.error) { throw new Error(result.error.message);}
if (result.data.tokenType === 'DPoP') { // This token is sender-constrained — validate the DPoP proof too await authrim.validateDPoP(dpopProof, { method: 'GET', uri: 'https://api.example.com/resource', accessToken: token, expectedThumbprint: result.data.claims.cnf!.jkt!, });}For full DPoP validation, see DPoP Validation.
Code Examples
Section titled “Code Examples”Basic Token Validation
Section titled “Basic Token Validation”import { createAuthrimServer } from '@authrim/server';
const authrim = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com',});await authrim.init();
try { const result = await authrim.validateToken(accessToken); if (result.error) { throw new Error(result.error.message); }
console.log('User:', result.data.claims.sub); console.log('Scopes:', result.data.claims.scope); console.log('Expires in:', result.data.expiresIn, 'seconds');} catch (error) { console.error('Token validation failed:', error.message);}With Required Scopes
Section titled “With Required Scopes”const authrim = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com', tokenValidation: { requiredScopes: ['read:orders', 'write:orders'], },});
const result = await authrim.validateToken(accessToken);
if (result.error?.code === 'insufficient_scope') { return res.status(403).json({ error: 'insufficient_scope' });}
if (result.error) { return res.status(401).json({ error: 'invalid_token' });}
// Token is valid and has both scopes.Accessing Custom Claims
Section titled “Accessing Custom Claims”Tokens may contain custom claims added by the authorization server (e.g., roles, tenant ID). Access them through the claims object:
const result = await authrim.validateToken(accessToken);if (result.error) { throw new Error(result.error.message);}
// Access custom claims with type assertionconst roles = result.data.claims['roles'] as string[];const tenantId = result.data.claims['tenant_id'] as string;
// Or use a type parameter for full type safetyinterface MyTokenClaims extends AccessTokenClaims { roles: string[]; tenant_id: string;}
const claims = result.data.claims as MyTokenClaims;console.log(claims.roles); // ['admin', 'user']console.log(claims.tenant_id); // 'tenant-123'Error Codes
Section titled “Error Codes”validateToken() returns these codes in result.error.code for common validation failures:
| Code | Description | Typical HTTP Status |
|---|---|---|
token_malformed | Token is not a valid compact JWT or exceeds the parser limits | 401 |
algorithm_mismatch | Token uses alg: none or an unsupported algorithm | 401 |
signature_invalid | Signature verification failed | 401 |
token_expired | Token exp claim has passed beyond clock tolerance | 401 |
token_not_yet_valid | Token nbf claim is in the future beyond clock tolerance | 401 |
invalid_issuer | Token iss does not match configured issuer(s) | 401 |
invalid_audience | Token aud does not match configured audience(s) | 401 |
invalid_tenant | Token tenant claim is missing, not allowed, or rejected by the tenant predicate | 403 |
insufficient_scope | Token is missing one or more required scopes | 403 |
jwks_key_not_found | No JWKS key matches the token header | 500 |
Next Steps
Section titled “Next Steps”- DPoP Validation — Validate sender-constrained tokens with DPoP proof verification
- JWKS Management — Key discovery, caching, and rotation internals
- Introspection & Revocation — Query and revoke tokens at the authorization server
- Security Considerations — Production security checklist and best practices