jwt

js/security/jwt.ts

fino:security/jwt - compact JWT/JWS and JWE helpers backed by JWK keys.

Learn more:

This module signs and verifies compact JWTs with HMAC, RSA, RSASSA-PSS, and ECDSA algorithms, and encrypts or decrypts compact JWE payloads with direct symmetric keys or RSA-OAEP key wrapping. Key inputs may be single JWKs, arrays of JWKs, or JWKS containers; verification and decryption select compatible keys from token headers.

The helpers validate common registered claims such as issuer, audience, subject, expiration, not-before, token type, JWT ID, required claims, and maximum token age when options request them. They do not fetch remote JWKS documents or implement application authorization policy. JWE support is the compact serialization with AES-GCM content encryption and dir, RSA-OAEP, or RSA-OAEP-256 key management; JSON serialization, ECDH-ES, PBES2, CBC-HS, compression, and additional OAEP variants are intentionally outside this release baseline.

import { jwkFromSecret } from 'fino:security/jwk';
import { jwtSign, jwtVerify } from 'fino:security/jwt';

const key = jwkFromSecret(sessionSecret, 'HS256', 'current');
const token = await jwtSign({ sub: 'user-123' }, key, {
  algorithm: 'HS256',
  expiresIn: 900,
});
const { payload } = await jwtVerify(token, key, { clockTolerance: 30 });

Types

type JwtAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512' | 'PS256' | 'PS384' | 'PS512' | 'ES256' | 'ES384' | 'ES512'

Supported compact JWS algorithms.

none is intentionally unsupported. HMAC algorithms require oct keys, RSA algorithms require RSA keys, and ECDSA algorithms require EC keys with a compatible curve.

import type { JwtAlgorithm } from 'fino:security/jwt';

const algorithm: JwtAlgorithm = 'HS256';

type JweAlgorithm = 'dir' | 'RSA-OAEP' | 'RSA-OAEP-256'

Supported compact JWE key-management algorithms.

dir uses the supplied symmetric key directly as the content-encryption key. RSA-OAEP variants wrap a fresh content-encryption key for each token.

import type { JweAlgorithm } from 'fino:security/jwt';

const algorithm: JweAlgorithm = 'RSA-OAEP-256';

type JweEncryption = 'A128GCM' | 'A256GCM'

Supported compact JWE content-encryption algorithms.

A128GCM uses a 16-byte content-encryption key; A256GCM uses 32 bytes.

import type { JweEncryption } from 'fino:security/jwt';

const encryption: JweEncryption = 'A256GCM';

type JwtKeyInput = JsonWebKeyLike | JsonWebKeySet | JsonWebKeyLike[]

Key input accepted by verification and decryption helpers.

A single JWK is used directly. Arrays and JWKS containers are searched by header kid and alg, returning the first matching key or throwing when no key matches.

import type { JwtKeyInput } from 'fino:security/jwt';

const keys: JwtKeyInput = { keys: [{ kty: 'oct', kid: 'current', k: 'secret' }] };

Interfaces

interface JwtSignOptions {

Options for signing a compact JWT.

The algorithm is required. iat is added by default, and expiresIn or notBefore add relative exp and nbf claims based on the current Unix time in seconds.

import type { JwtSignOptions } from 'fino:security/jwt';

const options: JwtSignOptions = { algorithm: 'HS256', expiresIn: 3600 };

Properties

algorithm: JwtAlgorithm

JWS signing algorithm.

The key must be compatible with the selected algorithm. none is not part of this type and is rejected defensively at runtime.

import type { JwtSignOptions } from 'fino:security/jwt';

const options: JwtSignOptions = { algorithm: 'RS256' };
header?: Record<string, unknown>

Additional protected header fields.

These fields are merged after the default { typ: 'JWT', alg }, so they can add values such as kid. Avoid overriding alg.

import type { JwtSignOptions } from 'fino:security/jwt';

const options: JwtSignOptions = { algorithm: 'HS256', header: { kid: 'current' } };
expiresIn?: number

Lifetime in seconds from signing time.

When provided, exp is set to now + expiresIn. Omit it to leave the JWT without an expiration claim.

import type { JwtSignOptions } from 'fino:security/jwt';

const options: JwtSignOptions = { algorithm: 'HS256', expiresIn: 900 };
notBefore?: number

Delay in seconds before the JWT becomes valid.

When provided, nbf is set to now + notBefore.

import type { JwtSignOptions } from 'fino:security/jwt';

const options: JwtSignOptions = { algorithm: 'HS256', notBefore: 30 };
issuedAt?: number | false

Issued-at claim value, or false to omit iat.

Defaults to the current Unix time in seconds. Numeric values are used as-is.

import type { JwtSignOptions } from 'fino:security/jwt';

const options: JwtSignOptions = { algorithm: 'HS256', issuedAt: false };

interface JwtVerifyOptions {

Claim checks and clock controls used during JWT verification.

All supplied checks must pass after signature verification. Failures throw errors from jwtVerify() rather than returning null.

import type { JwtVerifyOptions } from 'fino:security/jwt';

const options: JwtVerifyOptions = { issuer: 'https://issuer.example', audience: 'api' };

Properties

audience?: string | string[]

Expected aud claim.

A string or any value in the provided list may match. JWT payload aud can be a string or array of strings.

import type { JwtVerifyOptions } from 'fino:security/jwt';

const options: JwtVerifyOptions = { audience: ['api', 'admin'] };
issuer?: string

Expected iss claim.

When supplied, payload iss must be exactly equal or verification throws.

import type { JwtVerifyOptions } from 'fino:security/jwt';

const options: JwtVerifyOptions = { issuer: 'https://issuer.example' };
subject?: string

Expected sub claim.

When supplied, payload sub must be exactly equal or verification throws.

import type { JwtVerifyOptions } from 'fino:security/jwt';

const options: JwtVerifyOptions = { subject: 'user-123' };
algorithms?: JwtAlgorithm[]

Allowed protected-header algorithms.

When supplied, the token alg must be one of these values before key selection and signature verification.

const options: JwtVerifyOptions = { algorithms: ['RS256'] };
requiredClaims?: string[]

Required payload claim names.

Each listed claim must be present in the verified payload. Values may be any JSON value except undefined.

const options: JwtVerifyOptions = { requiredClaims: ['sub', 'iat'] };
maxTokenAge?: number

Maximum age in seconds since the iat claim.

Tokens without numeric iat fail when this option is supplied.

const options: JwtVerifyOptions = { maxTokenAge: 300 };
typ?: string | string[]

Expected JOSE typ protected-header value.

A string or any value in the provided list may match.

const options: JwtVerifyOptions = { typ: 'JWT' };
jwtId?: string | string[]

Expected jti claim.

A string or any value in the provided list may match. Replay prevention is application policy; this only compares the claim value.

const options: JwtVerifyOptions = { jwtId: 'token-123' };
clockTolerance?: number

Clock tolerance in seconds for exp and nbf.

Defaults to 0. Positive values allow small clock skew during validation.

import type { JwtVerifyOptions } from 'fino:security/jwt';

const options: JwtVerifyOptions = { clockTolerance: 30 };
now?: number

Current Unix time in seconds for claim checks.

Defaults to the current wall clock. Supplying it is useful for tests.

import type { JwtVerifyOptions } from 'fino:security/jwt';

const options: JwtVerifyOptions = { now: 1_700_000_000 };

interface JwtEncryptOptions {

Options for encrypting a compact JWE with a JSON payload.

The algorithm controls key management and encryption controls AES-GCM content encryption. Additional header fields are protected by authenticated encryption.

import type { JwtEncryptOptions } from 'fino:security/jwt';

const options: JwtEncryptOptions = { algorithm: 'dir', encryption: 'A256GCM' };

Properties

algorithm: JweAlgorithm

JWE key-management algorithm.

dir requires an oct JWK whose decoded k length matches encryption. RSA-OAEP variants use a public RSA key to encrypt a fresh CEK.

import type { JwtEncryptOptions } from 'fino:security/jwt';

const options: JwtEncryptOptions = { algorithm: 'RSA-OAEP-256', encryption: 'A256GCM' };
encryption: JweEncryption

JWE content-encryption algorithm.

A128GCM requires a 16-byte CEK and A256GCM requires a 32-byte CEK.

import type { JwtEncryptOptions } from 'fino:security/jwt';

const options: JwtEncryptOptions = { algorithm: 'dir', encryption: 'A128GCM' };
header?: Record<string, unknown>

Additional protected JWE header fields.

Fields are merged after typ, alg, and enc, so use this for values such as kid. Avoid overriding algorithm fields.

import type { JwtEncryptOptions } from 'fino:security/jwt';

const options: JwtEncryptOptions = { algorithm: 'dir', encryption: 'A256GCM', header: { kid: 'enc-1' } };

interface JwtResult {

Decoded compact JWT or JWE result.

Verification and decryption return the protected header and JSON payload as plain records. Claim validation is performed only by jwtVerify().

import type { JwtResult } from 'fino:security/jwt';

const result: JwtResult = { header: { alg: 'HS256' }, payload: { sub: 'user-123' } };

Properties

header: Record<string, unknown>

Decoded protected header.

Values are parsed from JSON and are not narrowed beyond the record shape.

import type { JwtResult } from 'fino:security/jwt';

const result: JwtResult = { header: { alg: 'HS256' }, payload: {} };
const alg = result.header.alg;
payload: Record<string, unknown>

Decoded JSON payload.

Values are parsed from JSON. JWT registered claims remain in this object after verification.

import type { JwtResult } from 'fino:security/jwt';

const result: JwtResult = { header: { alg: 'HS256' }, payload: { sub: 'user-123' } };
const sub = result.payload.sub;

Functions

async function jwtSign( payload: Record<string, unknown>, key: JsonWebKeyLike, options: JwtSignOptions ): Promise<string>

Sign a compact JWS/JWT.

Adds iat by default and optional relative exp and nbf claims. The returned string is header.payload.signature. Key import, unsupported algorithms, and crypto signing failures reject the promise.

import { jwtSign } from 'fino:security/jwt';
import { jwkFromSecret } from 'fino:security/jwk';

const key = jwkFromSecret('shared-secret', 'HS256');
const token = await jwtSign({ sub: 'user-123' }, key, { algorithm: 'HS256' });

async function jwtVerify( token: string, keys: JwtKeyInput, options: JwtVerifyOptions = { } ): Promise<JwtResult>

Verify a compact JWS/JWT and return decoded header and payload.

Throws for malformed compact tokens, unsupported algorithms, missing matching keys, failed signatures, and failed claim checks. The helper selects keys from JWKS input using protected header kid and alg.

import { jwtSign, jwtVerify } from 'fino:security/jwt';
import { jwkFromSecret } from 'fino:security/jwk';

const key = jwkFromSecret('shared-secret', 'HS256');
const token = await jwtSign({ sub: 'user-123' }, key, { algorithm: 'HS256' });
const result = await jwtVerify(token, key, { subject: 'user-123' });

async function jwtEncrypt( payload: Record<string, unknown>, key: JsonWebKeyLike, options: JwtEncryptOptions ): Promise<string>

Encrypt a compact JWE with a JSON payload using AES-GCM content encryption.

The returned string is the five-part compact JWE form. With dir, the symmetric key is used directly as the CEK. With RSA-OAEP, a fresh CEK is generated and encrypted for the recipient. Key import and crypto failures reject the promise.

import { jwtEncrypt } from 'fino:security/jwt';
import { generateJwk } from 'fino:security/jwk';

const key = await generateJwk({ kty: 'oct', alg: 'dir', length: 256 });
const token = await jwtEncrypt({ sub: 'user-123' }, key, { algorithm: 'dir', encryption: 'A256GCM' });

async function jwtDecrypt(token: string, keys: JwtKeyInput): Promise<JwtResult>

Decrypt a compact JWE and return decoded header and JSON payload.

Throws for malformed compact tokens, missing matching keys, CEK length mismatches, AES-GCM authentication failures, and JSON decode failures. The thrown error message is prefixed with JWE decryption failed: for inner decryption errors.

import { jwtDecrypt, jwtEncrypt } from 'fino:security/jwt';
import { generateJwk } from 'fino:security/jwk';

const key = await generateJwk({ kty: 'oct', alg: 'dir', length: 256 });
const token = await jwtEncrypt({ sub: 'user-123' }, key, { algorithm: 'dir', encryption: 'A256GCM' });
const result = await jwtDecrypt(token, key);