Skip to content

Token Validation

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.

const result = await authrim.validateToken(token);
ParameterTypeDescription
tokenstringThe raw JWT access token (without the Bearer prefix)
Additional validation rules are configured on the server instance with tokenValidation.
interface TokenValidationOptions {
clockToleranceSeconds?: number;
requiredScopes?: string[];
validateDPoP?: boolean;
tenantClaim?: string;
requireTenantClaim?: boolean;
requiredTenantId?: string;
allowedTenantIds?: string[];
tenantPredicate?: (
tenantId: string,
claims: AccessTokenClaims,
) => boolean | Promise<boolean>;
}
OptionTypeDescription
clockToleranceSecondsnumberClock skew tolerance for standard claim checks
requiredScopesstring[]Scopes that must be present in the token’s scope claim
validateDPoPbooleanReserved for DPoP-bound token validation policy in middleware integrations
tenantClaimstringClaim name that carries the tenant id. Defaults to tenant_id
requireTenantClaimbooleanReject tokens that do not contain a tenant claim
requiredTenantIdstringRequire the token tenant to match this exact tenant id
allowedTenantIdsstring[]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

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;
}
FieldTypeDescription
claimsAccessTokenClaimsParsed and validated JWT claims
tokenstringThe original token string
issuerstringValidated issuer
tenantIdstring | undefinedValidated tenant id when a tenant claim is present
tokenType'Bearer' | 'DPoP'Detected token type based on claims
expiresInnumber | undefinedSeconds until the token expires (calculated from exp)
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
}

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'],
},
});

The SDK supports the following JWS algorithms for signature verification:

AlgorithmKey TypeCurve / SizeDescription
RS256RSA2048+ bitsRSASSA-PKCS1-v1_5 with SHA-256
RS384RSA2048+ bitsRSASSA-PKCS1-v1_5 with SHA-384
RS512RSA2048+ bitsRSASSA-PKCS1-v1_5 with SHA-512
PS256RSA2048+ bitsRSASSA-PSS with SHA-256
PS384RSA2048+ bitsRSASSA-PSS with SHA-384
PS512RSA2048+ bitsRSASSA-PSS with SHA-512
ES256ECP-256ECDSA with SHA-256
ES384ECP-384ECDSA with SHA-384
ES512ECP-521ECDSA with SHA-512
EdDSAOKPEd25519Edwards-curve Digital Signature

The iss claim must exactly match one of the configured issuer values. Comparison uses a timing-safe algorithm to prevent side-channel attacks.

// Single issuer
const authrim = createAuthrimServer({
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
// Multiple issuers
const authrim = createAuthrimServer({
issuer: [
'https://auth.example.com',
'https://auth.partner.com',
],
audience: 'https://api.example.com',
});

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" — rejected

The exp claim is checked against the current time plus clockToleranceSeconds:

token is valid if: exp + clockToleranceSeconds > now

With the default tolerance of 60 seconds, a token that expired up to 60 seconds ago is still accepted.

If present, the nbf claim is checked against the current time minus clockToleranceSeconds:

token is valid if: nbf - clockToleranceSeconds <= now

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 <= now

Enforce required scopes using the requiredScopes option:

// Require specific scopes
const 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.

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 as DPoP
  • 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.

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);
}
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.

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 assertion
const roles = result.data.claims['roles'] as string[];
const tenantId = result.data.claims['tenant_id'] as string;
// Or use a type parameter for full type safety
interface 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'

validateToken() returns these codes in result.error.code for common validation failures:

CodeDescriptionTypical HTTP Status
token_malformedToken is not a valid compact JWT or exceeds the parser limits401
algorithm_mismatchToken uses alg: none or an unsupported algorithm401
signature_invalidSignature verification failed401
token_expiredToken exp claim has passed beyond clock tolerance401
token_not_yet_validToken nbf claim is in the future beyond clock tolerance401
invalid_issuerToken iss does not match configured issuer(s)401
invalid_audienceToken aud does not match configured audience(s)401
invalid_tenantToken tenant claim is missing, not allowed, or rejected by the tenant predicate403
insufficient_scopeToken is missing one or more required scopes403
jwks_key_not_foundNo JWKS key matches the token header500