192 lines
6.8 KiB
Markdown
192 lines
6.8 KiB
Markdown
# @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<T>(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<string>;
|
|
```
|
|
|
|
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<T>(token, secret, options?)`
|
|
|
|
```ts
|
|
function verifyJwt<T extends JwtClaims = JwtClaims>(
|
|
token: string,
|
|
secret: string,
|
|
options?: { now?: number },
|
|
): Promise<T>;
|
|
```
|
|
|
|
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 <token>`.
|
|
- `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.
|