jwt
js/security/jwt.ts
fino:security/jwt - compact JWT/JWS and JWE helpers backed by JWK keys.
Learn more:
- JSON Web Token: https://www.rfc-editor.org/rfc/rfc7519
- JSON Web Signature: https://www.rfc-editor.org/rfc/rfc7515
- JSON Web Encryption: https://www.rfc-editor.org/rfc/rfc7516
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);