Authentication basics
A minimal auth flow: sign a token on login, verify it on every protected request, reject expired or tampered tokens automatically.
This example uses HS256 (symmetric) — the simplest and fastest path when the same service signs and verifies. If you're building a system where third parties need to verify tokens without being able to sign them, see JWKS endpoint instead.
Setup
import { generateJWK } from "unjwt/jwk";
// Generate once, persist. Treat like any other secret.
const signingKey = await generateJWK("HS256", { kid: "primary-2025" });
// { kty: "oct", k: "...", alg: "HS256", kid: "primary-2025" }
In production, load this key from a secrets manager (or at minimum, process.env). Do not generate a fresh key on every boot — all existing tokens would instantly become invalid.
import type { JWK_oct } from "unjwt/jwk";
import { isSymmetricJWK } from "unjwt/utils";
const parsed = JSON.parse(process.env.JWT_SIGNING_KEY!);
if (!isSymmetricJWK(parsed)) {
throw new Error("JWT_SIGNING_KEY must be a symmetric JWK");
}
const signingKey: JWK_oct = parsed;
Issue a token (login)
import { sign } from "unjwt/jws";
async function issueAccessToken(userId: string, roles: string[]) {
return sign({ sub: userId, roles }, signingKey, {
expiresIn: "15m",
protectedHeader: {
typ: "access+jwt",
},
});
}
What unjwt adds to the payload automatically:
iat— current time.exp—iat + 900(15 minutes fromexpiresIn).kid— copied fromsigningKey.kidinto the header.
What your code controls:
sub— subject, typically the user id.roles/scopes/ whatever else your app needs.typ: "access+jwt"— a distinct type so a refresh token from the same app can't be mistaken for an access token (RFC 8725 §3.11 recommendation).
Verify on a protected route
import { verify, type JWSVerifyResult } from "unjwt/jws";
async function requireAuth(
request: Request,
): Promise<JWSVerifyResult<{ sub: string; roles: string[] }>> {
const authHeader = request.headers.get("authorization");
if (!authHeader?.startsWith("Bearer ")) {
throw new Response("Unauthorized", { status: 401 });
}
const token = authHeader.slice("Bearer ".length);
try {
return await verify<{ sub: string; roles: string[] }>(token, signingKey, {
audience: "my-api",
issuer: "my-app",
typ: "access+jwt", // must match what we set on issue
maxTokenAge: "15m", // defense-in-depth on top of exp
});
} catch (error) {
throw new Response(`Invalid token: ${(error as Error).message}`, { status: 401 });
}
}
Then in a route:
async function getProfile(request: Request) {
const { payload } = await requireAuth(request);
return Response.json({ userId: payload.sub, roles: payload.roles });
}
What's checked automatically
When you call verify() with a payload that's a JSON object, unjwt runs these checks without needing you to opt in:
| Check | What it rejects |
|---|---|
| Signature | Token was tampered with, or signed by the wrong key. |
alg allowlist | Wrong algorithm — derived from signingKey.alg. |
exp | Token is past its expiration. |
nbf | Token is used before its "not before" time. |
iat | Not a finite number (if present). |
These are the checks that protect you from the classic JWT pitfalls (alg: none attack, algorithm confusion, expired-token reuse). You don't need to pass algorithms explicitly because signingKey.alg is set.
Adding issuer / audience
These are defence-in-depth. They catch bugs where the same key is accidentally reused across services:
await sign({ sub: userId, roles, iss: "my-app", aud: "my-api" }, signingKey, {
expiresIn: "15m",
protectedHeader: { typ: "access+jwt" },
});
await verify(token, signingKey, {
issuer: "my-app",
audience: "my-api",
});
Now a token signed for my-api can't verify as a token for admin-api, even if both use the same signing key during a shared-infrastructure transition.
Next steps
- Third parties need to verify your tokens? Switch to an asymmetric algorithm (e.g.
ES256) and publish a JWKS endpoint →. - Want automatic session handling? Use the H3 session adapters → — cookie-based sessions with built-in refresh hooks.
- Need long sessions without long-lived tokens? Go to Refresh token pattern →.