@lacspace/jwt
v1.4.1
Published
JSON Web Tokens (HS/RS/ES/EdDSA) with JWKS-by-kid key rotation, strict expiry/issuer/audience/subject/jti checks + secure random & CSRF tokens. Isomorphic over Web Crypto — Node, edge, browser.
Maintainers
Readme
@lacspace/jwt
JSON Web Tokens (HMAC) + secure random tokens — done safely.
1.4.1 — extractBearer reads headers in any case
HTTP header names are case-insensitive (RFC 9110 §5.1). Node lowercases
req.headers, but a hand-built object or another framework may not, and
extractBearer({ Authorization: "Bearer …" }) returned no token at all. The
same applies to the named cookie.
Sign and verify HS · RS · ES · EdDSA JWTs over Web Crypto with strict expiry / not-before / issuer / audience / subject / jti checks and constant-time signature comparison. Isomorphic — runs on edge and workers where the
jsonwebtokenpackage can't. Plus JWKS-by-kidkey rotation, opaque and CSRF tokens.
- 🎟️
sign/verify/decodewith typed claims - 🔑 HS256/384/512 · RS256/384/512 · ES256/384/512 · EdDSA (Ed25519)
- 🗝️ JWK & JWKS —
importJwk/exportJwk/generateKeyPair,createKeySet/resolveKeypick a key bykid - ⏱️
exp/nbf/iat,issuer,audience,subject,jwtid,clockTolerance - 🧯 Typed
JwtErrorwith acode(expired,signature,audience,subject,jti,key…) - 🎲
randomToken/csrfToken - ⚡ Zero deps (bar
@lacspace/crypto) · 🌍 isomorphic · fully typed
New in 1.4 — EdDSA, JWK/JWKS export & key rotation
import { sign, verify, generateKeyPair, exportJwk, importJwk, createKeySet, resolveKey } from "@lacspace/jwt";
// EdDSA (Ed25519) — modern, tiny signatures
const { privateKey, publicKey } = await generateKeyPair("EdDSA");
const token = await sign({ sub: "u1" }, privateKey, { algorithm: "EdDSA", keyId: "k1" });
await verify(token, publicKey, { algorithms: ["EdDSA"] });
// Export a public key to a JWK to publish in your JWKS
const jwk = { ...(await exportJwk(publicKey)), kid: "k1", alg: "EdDSA" };
// Key rotation / JWKS: verify against MULTIPLE keys, selected by the token's `kid`
const keyset = createKeySet({ keys: [jwk /* …more keys… */ ] });
await verify(token, keyset, { algorithms: ["EdDSA"] });
// Rotate HMAC secrets the same way (tagged by kid)
const hmacSet = createKeySet([{ kid: "k1", key: SECRET_1 }, { kid: "k2", key: SECRET_2 }]);
// Remote JWKS with an INJECTABLE fetch (off by default — never hits the network on its own)
const remote = createKeySet([], { url: "https://issuer/.well-known/jwks.json", fetchImpl: fetch });
// Custom protected header fields (typ / cty / any), plus subject & jti claim checks
await sign({ sub: "u1" }, secret, { header: { typ: "at+jwt", cty: "example" } });
await verify(token, secret, { subject: "u1", jwtid: "abc", clockTolerance: 30 });resolveKey(jwks, { kid, alg }) is the low-level JWK selector. Everything is additive:
existing sign/verify calls and the HS*/RS*/ES* algorithms are unchanged.
Install
npm install @lacspace/jwtUsage
import { sign, verify, JwtError } from "@lacspace/jwt";
const token = await sign({ sub: "user_1", role: "admin" }, process.env.JWT_SECRET!, {
expiresIn: 3600, // seconds
issuer: "lacspace",
});
try {
const payload = await verify(token, process.env.JWT_SECRET!, { issuer: "lacspace" });
payload.sub; // "user_1"
} catch (e) {
if (e instanceof JwtError) console.log(e.code); // "expired" | "signature" | …
}Opaque & CSRF tokens
import { randomToken, csrfToken } from "@lacspace/jwt";
randomToken(); // 43-char URL-safe (32 bytes) — session ids, reset tokens
csrfToken(); // CSRF tokenAPI
| Export | Description |
| --- | --- |
| sign(payload, secret, opts?) | algorithm, expiresIn, issuer, audience, subject, keyId, header |
| verify(token, secret, opts?) | throws JwtError; checks sig + claims (issuer, audience, subject, jwtid, clockTolerance, requireTyp) |
| decode(token) | header + payload, no verification |
| generateKeyPair(alg) | extractable RS*/ES*/EdDSA key pair (tests, rotation, JWKS) |
| importJwk / exportJwk / importPkcs8 / importSpki | key import/export (RS*/ES*/EdDSA) |
| createKeySet(source, opts?) | resolver that picks a key by kid — JWKS, rotation, injectable remote fetchImpl |
| resolveKey(jwks, { kid, alg }) | select a single JWK from a set |
| randomToken(bytes?) / csrfToken() | secure random tokens |
Algorithms: HS256/384/512, RS256/384/512, ES256/384/512, EdDSA (Ed25519).
The Lacspace Security Kit
| Package | For |
| --- | --- |
| @lacspace/crypto | AES encryption & hashing |
| @lacspace/password | Password hashing |
| @lacspace/jwt | JWTs & tokens (this package) |
| @lacspace/apikey | API keys |
| @lacspace/otp | TOTP/HOTP 2FA |
| @lacspace/webauthn | Passkeys / biometric |
| @lacspace/mfa | 2FA/3FA orchestration |
| @lacspace/lock | Account lockout |
| @lacspace/headers | Secure headers / CSP |
| @lacspace/redact | Log redaction |
New in 1.1 — framework adapters & cookies
import { authenticate, extractBearer, toAuthCookie, clearAuthCookie, expressJwt } from "@lacspace/jwt";
// Verify straight from a Fetch Request / Next route / Node req (Bearer or cookie)
const payload = await authenticate(req, secret); // throws JwtError on failure
const payload2 = await authenticate(req, secret, { cookieName: "session" });
// Hardened auth cookie (HttpOnly + Secure + SameSite by default)
res.headers.set("Set-Cookie", toAuthCookie(token, { name: "session", maxAge: 3600 }));
res.headers.set("Set-Cookie", clearAuthCookie({ name: "session" })); // logout
// Express: verifies the token → req.user, else 401
app.use(expressJwt(secret));New in 1.2 — RS256/ES256, JWKS & refresh tokens
import { sign, verify, importPkcs8, importSpki, createRemoteJWKS, issueTokenPair, rotateRefreshToken } from "@lacspace/jwt";
// Asymmetric: sign with a private key, verify with the public key (clients can't forge)
const priv = await importPkcs8(PRIVATE_PEM, "ES256");
const token = await sign({ sub: "u1" }, priv, { algorithm: "ES256", keyId: "k1" });
// Consume 3rd-party OIDC tokens (Auth0/Cognito/Google) via cached JWKS
const jwks = createRemoteJWKS("https://issuer/.well-known/jwks.json");
const claims = await verify(idToken, jwks, { algorithms: ["RS256"] });
// Refresh-token flow with rotation + reuse detection
const { accessToken, refreshToken, refreshJti } = await issueTokenPair({ sub: "u1" }, secret);
const next = await rotateRefreshToken(refreshToken, secret, { isUsed: (jti) => store.has(jti) });
store.add(next.usedJti); // mark the old one spentissueTokenPair stamps the access token with typ: "access" and the refresh token with
typ: "refresh". Since both are signed with the same secret, always verify a refresh token
with { requireTyp: "refresh" } so an access token can never be replayed as a refresh token
(and gate protected routes with { requireTyp: "access" }):
const claims = await verify(refreshToken, secret, { requireTyp: "refresh" }); // rejects non-refresh typrequireTyp is off by default, so existing verify(...) calls are unchanged.
Plus audience: string[] matching and importJwk().
Licensing
This package is free under the Lacspace Free Licence — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
Not every Lacspace package is free. We also offer Commercial (paid), Client-specific, and Private (proprietary) packages under separate terms. See the full Lacspace Licence Centre.
The Lacspace Developer Platform
@lacspace/jwt is part of 80+ zero-dependency, isomorphic TypeScript packages — one standard library for the modern web. Explore the ecosystem:
- 📦 This package, documented — https://developer.lacspace.com/packages/jwt
- 🗂️ All 80+ packages — https://developer.lacspace.com/packages
- 🧭 Developer handbook — guides & runnable recipes — https://developer.lacspace.com/handbook
- 🧪 Live playground — run any package in your browser — https://developer.lacspace.com/playground
- 🖥️ Finished app templates — https://templates.lacspace.com
- 🚀 Scaffold a full app —
npm create lacspace-app@latest
Free under the Lacspace Free Licence — a permissive, free-to-use licence.
