JWT
TriFrost includes first-class support for working with JSON Web Tokens (JWT), with utilities designed for both stateless authentication and runtime-agnostic crypto handling.
Unlike external libraries that assume a specific runtime and work with specific runtime libraries, TriFrost’s JWT helpers are:
- Fully typed
- Lightweight
- Compatible with HMAC, RSA, and ECDSA
- Designed to run in Node, Bun, and Cloudflare Workers without modification
Whether you’re signing short-lived tokens for frontend clients or verifying RS256 tokens across services, these tools are built in, production-ready, and work everywhere TriFrost runs.
Overview
TriFrost’s JWT support centers around three utility functions:
jwtSign(secret, options)
: signs a payload into a JWTjwtVerify(token, secret, options)
: verifies a JWT’s signature and claimsjwtDecode(token)
: decodes a JWT without verifying it
Each is zero-dependency, relies on native WebCrypto APIs, and returns useful, structured data, or throws typed errors for clean handling.
✍️ Signing a JWT
import {jwtSign} from '@trifrost/core';
const token = await jwtSign(ctx.env.SECRET, {
subject: 'user-123',
payload: {role: 'admin'},
issuer: 'api-service',
expiresIn: 3600, // 1 hour
});
Available Options (JWTSignOptions):
algorithm
:'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512' | 'ES256' | 'ES384' | 'ES512'
Signing algorithm
default:HS256
issuer
: string
Issuer claim (iss
), string identifying the issuing entity (eg:'api.trifrost.dev'
)subject
: string
Subject claim (sub
), identifies the principal that is the subject of the JWT (eg: user id). Take Note: Automatically gets coerced to stringaudience
: string|string[]
Audience claim (aud
), string or string array identifying the intended audience (eg:'app.trifrost.dev'
)expiresIn
: number|null
Seconds until expiration (exp
). Pass as null to not set expiry
default:3600
notBefore
: number
Seconds before which the token is not valid (nbf
). Eg60
= 1 minutepayload
:Record<string, any>
Custom payload data to embed in the token. Eg{"role":"user"}
jwtid
: string
Unique identifier for the JWT (jti
). Particularly useful to protect against replay attacks by storing used ids and verifyingtype
: string
Type of token (typ
header field).
default:'JWT'
🛡 Verifying a JWT
import {jwtVerify} from '@trifrost/core';
const payload = await jwtVerify(token, ctx.env.SECRET, {
algorithm: 'HS256',
issuer: 'api-service',
});
Available Options (JWTVerifyOptions):
algorithm
:'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512' | 'ES256' | 'ES384' | 'ES512'
Algorithm to be used for signature verification
default:HS256
leeway
: number
Clock skew tolerance in seconds forexp
andnbf
.
default:0
issuer
: string
Expected issuer (iss
) claim (eg:'api.trifrost.dev'
)subject
:(val:string) => boolean
Function to validate the subject (sub
) claim. Take Note: This should purely do for example regex validation and is not async.audience
: string|string[]
Expected audience (aud
) claim. Can be array of allowed audiences (eg:'app.trifrost.dev'
)type
: string
Expected value oftyp
header field.
default:'JWT'
Failure throws one of:
JWTMalformedError
JWTTimeError
: expired / not yet validJWTClaimError
: issuer, audience, subject mismatchJWTSignatureError
: bad signatureJWTAlgorithmError
: mismatch or unsupportedJWTTypeError
: invalid token type
These errors are typed subclasses of JWTError
(which in turn extends from Error
), so you can catch specific types or handle them generically.
👁 Decoding without verifying
import {jwtDecode} from '@trifrost/core';
const tokenData = jwtDecode(token);
/* tokenData = {
sub: 'user-123',
role: 'admin',
_header: { alg: 'HS256', typ: 'JWT' }
} */
This function simply decodes and parses the JWT’s header and payload. It does not verify signature, time, or claims, it’s intended for inspection only.
✅ Supported Algorithms
All of the below algorithms are supported and powered by native WebCrypto:
HMAC
HMAC (Hash-Based Message Authentication Code) is a symmetric cryptographic technique that uses a shared secret and a hash function to verify integrity and authenticity.
- Fast and simple
- Ideal for internal APIs or single-tenant systems
- Requires shared secret to be kept private on both sides
Supported:
'HS256'
: HMAC with SHA-256'HS384'
: HMAC with SHA-384'HS512'
: HMAC with SHA-512
Best Practice: Use secrets that are at least as long as the hash output, for example 256 bits (32 bytes) for
HS256
.Avoid short or guessable secrets.
Use a high-entropy string or randomly generated key (e.g. via
crypto.randomUUID()
).
🧰 Generate a secure HMAC secret:
npm create trifrost@latest
# → Choose: Security Keys → HS256 (or HS384/HS512)
Output:
SECRET="ZtFqM9TPeDp+Y0..."
Add to your projects' .env
file and use as such:
jwtSign(ctx.env.SECRET, { algorithm: 'HS256', ... });
👉 See TriFrost Creation CLI for more info
RSA
RSA is an asymmetric cryptographic algorithm using a public/private key pair. It is widely used and supported across platforms.
- Suitable for multi-party or federated environments
- Verifiers only need the public key
- Signers must protect the private key
Supported:
'RS256'
: RSA PKCS#1 v1.5 with SHA-256'RS384'
: RSA PKCS#1 v1.5 with SHA-384'RS512'
: RSA PKCS#1 v1.5 with SHA-512
Key sizes: 2048 bits minimum (standard), 4096 bits recommended for RS384/RS512.
🧰 Generate RSA keypair:
npm create trifrost@latest
# → Choose: Security Keys → RS512 (or RS256/RS384) → 2048/4096 bits
Output:
PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..."
PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
Add to your projects' .env
file and use as such:
jwtSign(ctx.env.PRIVATE_KEY, { algorithm: 'RS256', ... });
jwtVerify(token, ctx.env.PUBLIC_KEY, { algorithm: 'RS256' });
👉 See TriFrost Creation CLI for more info
ECDSA
ECDSA (Elliptic Curve Digital Signature Algorithm) is an asymmetric algorithm known for smaller keys and fast verification.
- Smaller token size than RSA
- Efficient on constrained environments
- Requires curve-matching keys
Supported:
'ES256'
: ECDSA with P-256 curve and SHA-256'ES384'
: ECDSA with P-384 curve and SHA-384'ES512'
: ECDSA with P-521 curve and SHA-512
🧰 Generate ECDSA keypair:
npm create trifrost@latest
# → Choose: Security Keys → ES256 (or ES384/ES512)
Output:
PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..."
PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
Add to your projects' .env
file and use as such:
jwtSign(ctx.env.PRIVATE_KEY, { algorithm: 'ES256', ... });
jwtVerify(token, ctx.env.PUBLIC_KEY, { algorithm: 'ES256' });
👉 See TriFrost Creation CLI for more info
All supported algorithms are powered by native WebCrypto, and key caching is handled automatically.
Examples
Auth with BearerAuth
You can easily combine jwtVerify()
with the built-in BearerAuth middleware to protect routes with minimal code:
import {jwtVerify, BearerAuth} from '@trifrost/core';
app.group('/api', router => {
router
.use(BearerAuth({
validate: async (ctx, token) => {
try {
const payload = await jwtVerify(token, ctx.env.JWT_SECRET, {
algorithm: 'HS256',
issuer: 'api-service',
});
return {id: payload.sub, role: payload.role};
} catch {
return false;
}
}
}))
.get('/me', ctx => ctx.json({id: ctx.state.$auth.id}));
});
If validate()
returns an object, it gets stored in ctx.state.$auth
and is accessible downstream. Returning false
short-circuits with a 401 Unauthorized
.
Asymmetric Auth with RS256
If you're using RS256 or other asymmetric schemes (e.g. public/private key pairs):
import {jwtSign, jwtVerify} from '@trifrost/core';
/* Issue token */
const token = await jwtSign(ctx.env.PRIVATE_KEY, {
algorithm: 'RS256',
subject: user.id,
payload: {role: user.role},
issuer: 'auth-service',
audience: 'client-app',
});
/* Verify later */
await jwtVerify(token, ctx.env.PUBLIC_KEY, {
algorithm: 'RS256',
issuer: 'auth-service',
audience: 'client-app',
});
Best Practices
- ✅ Use
ctx.env
for secrets — never hardcode keys. - ✅ Always add
exp
,aud
, andiss
claims — don’t skip claim validation (jwtVerify
handles this for you). - ✅ Log and audit errors carefully.
- ✅ Prefer asymmetric keys (RS256/ES256) for distributed trust.
- ✅ Treat
jwtDecode()
as unsafe and only for development, unless followed by verification (jwtVerify
internally also decodes).