# @wrnexus/jwt > Dependency-free JSON Web Tokens (HS256) via Web Crypto, plus a bearer-token auth middleware for WrNexus. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Overview `@wrnexus/jwt` signs and verifies stateless JSON Web Tokens using the **HS256** (HMAC-SHA-256) algorithm. It has no runtime dependencies — signing and verification are implemented directly on the standard **Web Crypto** API (`crypto.subtle`), which Bun provides natively. It runs server-side and pairs with the session-based auth in `@wrnexus/core`, giving you a stateless option for API and mobile clients. Reach for it when you need bearer-token auth rather than cookie sessions. ## Installation ```bash bun add @wrnexus/jwt ``` > Private package — the machine must be authenticated to the `wrnexus` npm org > (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). ## API Single entry point (`@wrnexus/jwt`). All functions are async and return Promises. | Export | Kind | Description | | --------------------------------------- | --------- | --------------------------------------------------------------- | | `signJwt(payload, secret, options?)` | function | Sign claims into an HS256 token string. | | `verifyJwt(token, secret, options?)` | function | Verify a token and return its claims, or throw. | | `jwtAuth(options)` | function | Middleware that verifies a bearer JWT and sets `ctx.user`. | | `JwtError` | class | Error thrown on any signature/payload/expiry failure. | | `JwtClaims` | interface | Claims shape (`sub`, `iat`, `exp`, `nbf`, plus arbitrary keys). | | `SignOptions` | interface | Options for `signJwt`. | | `JwtAuthOptions` | interface | Options for `jwtAuth`. | ### `signJwt(payload, secret, options?)` ```ts function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise; ``` Signs `payload` with `secret` using HS256 and returns the encoded token (`header.body.signature`). An `iat` (issued-at) claim is always added. `SignOptions`: - `expiresIn?: number` — seconds until expiry; sets the `exp` claim. - `now?: number` — override the issued-at time (seconds), useful for testing. ### `verifyJwt(token, secret, options?)` ```ts function verifyJwt( token: string, secret: string, options?: { now?: number }, ): Promise; ``` Verifies the HS256 signature and returns the decoded claims typed as `T`. Throws `JwtError` when the token is malformed, the signature is invalid, the payload is not valid JSON, the token is expired (`exp`), or not yet valid (`nbf`). Pass `now` (seconds) to override the reference time for the `exp`/`nbf` checks. ### `jwtAuth(options)` ```ts function jwtAuth(options: JwtAuthOptions): Middleware; ``` Returns a WrNexus `Middleware` that reads a token, verifies it, and assigns the claims to `ctx.user`. `JwtAuthOptions`: - `secret: string` — the HMAC secret used to verify tokens. - `getToken?: (ctx: Context) => string | undefined` — how to extract the token. Defaults to reading `Authorization: Bearer `. - `required?: boolean` — when `true` (default), a missing or invalid token responds with `401 { ok: false, error: "Unauthorized" }`. When `false`, requests pass through and `ctx.user` is only set if a valid token is present. ## Usage ```ts import { signJwt, verifyJwt, jwtAuth, JwtError } from "@wrnexus/jwt"; const secret = process.env.JWT_SECRET!; // Sign a token that expires in one hour const token = await signJwt({ sub: user.id, role: "admin" }, secret, { expiresIn: 3600, }); // Verify it later try { const claims = await verifyJwt<{ sub: string; role: string }>(token, secret); console.log(claims.sub, claims.role); } catch (err) { if (err instanceof JwtError) { // invalid signature, expired, malformed, etc. } } ``` Protecting routes with the middleware: ```ts import { jwtAuth } from "@wrnexus/jwt"; // Require a valid bearer token; ctx.user holds the verified claims app.use(jwtAuth({ secret: process.env.JWT_SECRET! })); // Optional auth — populate ctx.user when present, but don't 401 app.use(jwtAuth({ secret: process.env.JWT_SECRET!, required: false })); ``` ## Requirements / Notes - **Bun-only.** Uses the standard Web Crypto API (`crypto.subtle.importKey`, `sign`, `verify`) plus `btoa`/`atob` and `TextEncoder`/`TextDecoder` — all provided by Bun. No third-party crypto dependency. - **Algorithm:** HS256 (HMAC with SHA-256) only. Asymmetric algorithms (RS/ES) are not supported. - Integrates with [`@wrnexus/core`](../core) for `Context`, `Middleware`, and `ctx.user`; it complements the framework's cookie/session auth with a stateless bearer-token flow for API and mobile clients. ## Access, refresh, scope, and cookie helpers ```ts import { createAccessToken, createRefreshToken, verifyAccessToken, verifyRefreshToken, extractBearerToken, requireScopes, jwtCookie, } from "@wrnexus/jwt"; ``` The helpers add explicit `type: "access" | "refresh"` claims, scope checks, refresh-token family metadata, no-store token responses, and secure cookie defaults. `__Host-` cookies are rejected unless they use `Path=/` and `Secure`; `SameSite=None` is rejected without `Secure`. ## 0.8 helper kit ```ts import { createTokenPair, verifyAccessToken, verifyRefreshToken, extractBearerToken, readJwtCookie, jwtCookie, clearJwtCookie, requireScopes, } from "@wrnexus/jwt"; const pair = await createTokenPair(user.id, { accessSecret: process.env.JWT_ACCESS_SECRET!, refreshSecret: process.env.JWT_REFRESH_SECRET!, scopes: ["profile:read"], family: sessionFamily, }); ``` The helper kit validates `__Host-` cookie invariants, cookie names and paths, `SameSite=None` security, typed access/refresh token types, scope requirements, and no-store token responses. In addition to local HS256 secrets/keyrings, the package verifies standards-based RS256 tokens through bounded remote JWKS caches: ```ts import { createRemoteJwks, verifyJwtWithJwks } from "@wrnexus/jwt"; const jwks = createRemoteJwks("https://issuer.example/.well-known/jwks.json"); const claims = await verifyJwtWithJwks(token, jwks, { issuer: "https://issuer.example", audience: "my-api", maxAge: 300, }); ``` JWKS URLs must use HTTPS. Responses have key-count/byte limits, accept only RS256 signing RSA keys, deduplicate concurrent refreshes, cache imported public keys, and force an immediate refresh for an unknown `kid` so issuer rotation does not wait for cache expiry. Never use decoded-but-unverified claims for an authorization decision.