@nightowlsdev/auth-jwt
AuthDependency-free JWT primitives (HS256 sign/verify + ES256/RS256 JWKS verify) and signed-session helpers, on node:crypto only.
What it does
@nightowlsdev/auth-jwt provides small, dependency-free JWT primitives built on `node:crypto` only, no JOSE, no SDK: `signJwt`/`verifyJwt` (HS256) for tokens you sign yourself, `verifyJwtWithJwks` (ES256/RS256) for verifying a provider's tokens against its public JWKS, plus generic signed-session / cookie helpers (`mintSession`, `sessionCookie`). SERVER-ONLY and pure (the secret and `now` are parameters), so it is hermetically unit-testable with a known secret and a fixed clock. Use it to wire your app's existing JWTs into a Night Owls runner without an Auth0/Supabase-specific adapter.
Install
pnpm add @nightowlsdev/auth-jwtKey exports
- signJwt
- verifyJwt
- verifyJwtWithJwks
- mintSession
- sessionCookie
Usage
import * as pkg from "@nightowlsdev/auth-jwt";What it provides
auth-jwt is a small set of dependency-free JWT primitives built on node:crypto only — no JOSE, no SDK. It gives you signJwt / verifyJwt (HS256) for tokens you sign yourself, verifyJwtWithJwks (ES256/RS256) for verifying a provider's tokens against its public JWKS, and generic signed-session / cookie helpers (mintSession, readSessionToken, sessionCookie, readCookie). Every function is SERVER-ONLY and pure — the secret and now are parameters — so the whole thing is hermetically unit-testable with a known secret and a fixed clock.
When to use it
- You want to wire your app's existing JWTs into a Night Owls runner without an Auth0- or Supabase-specific adapter.
- You need to sign and verify your OWN short-lived session cookie (HS256 over a SESSION_SECRET) after authenticating an upstream identity.
- You want to verify a provider's asymmetric access token (e.g. a modern Supabase ES256/RS256 token) locally, with no vendor SDK and no per-request round-trip.
- You value a tiny, auditable, zero-dependency auth core over a full JOSE library.
When not to
- You want a drop-in AuthProvider for a specific IdP — use auth-supabase or auth-auth0; these are lower-level primitives you assemble yourself.
- You need signing with an asymmetric key (RS256/ES256) — this package signs HS256 only (it verifies asymmetric, but does not sign it).
- You need revocation out of the box — these tokens are stateless; add auth-sessions and mint a jti.
- You want a general-purpose JOSE toolkit (arbitrary algs, encryption) — this is deliberately narrow.
Alternatives
- auth-supabase / auth-auth0Your identity comes from Supabase or Auth0 and you want the claim-mapping, two-key discipline, and AuthProvider wiring done for you rather than assembling it from primitives.
- jose (directly)You need algorithms, key types, or JWE/encryption this package does not cover. auth-jwt intentionally supports only HS256 signing and ES256/RS256 verification.
Strengths
- Zero dependencies — node:crypto only — so it adds no supply-chain surface and is trivially portable.
- Pure functions (secret + now are parameters), so signing, verification, and expiry are hermetically testable against a fixed clock.
- Algorithm-confusion safe: it never reads the header's alg to CHOOSE a verifier — the caller fixes the algorithm and the verifier requires it, so the alg=none / HS-vs-RS confusion class is closed.
- Covers both halves: HS256 for tokens you sign, and an asymmetric (ES256/RS256) JWKS verify path for tokens an upstream issuer signs.
- Generic, provider-agnostic session helpers with a configurable cookie name, so a host can even mount more than one signed session.
Limits & trade-offs
- HS256 signing only — the asymmetric path is verify-only (you cannot mint an ES256/RS256 token with it).
- It ships no JWKS fetch or cache: verifyJwtWithJwks takes the JwkSet you pass in (that keeps it pure) — the network fetch and caching are yours to write.
- It is primitives, not a drop-in AuthProvider — you assemble the authenticate(req) => AuthContext yourself.
- Stateless: no revocation, so a leaked token is valid until it expires unless you layer auth-sessions on top.
- Not a general JOSE library — a token that is not well-formed HS256 (or ES256/RS256 on the JWKS path) is simply rejected.
How it works
signJwt(payload, secret, { expiresInSec?, nowSec }) produces an HS256 token; verifyJwt(token, secret, { nowSec, leewaySec? }) checks it structurally, requires the header alg to be exactly HS256, compares the signature timing-safely, and enforces exp with a small skew leeway — returning the claims or null, never throwing. verifyJwtWithJwks(token, jwks, opts) accepts only ES256/RS256, selects the key by kid, verifies with node:crypto (ES256 as raw r||s / IEEE-P1363), and checks exp. On top of that core, mintSession writes { sub: userId, tenant: tenantId, jti? } as a signed session and readSessionToken reads it back to a { tenantId, userId, jti? }; sessionCookie builds an httpOnly / SameSite=Lax / Secure Set-Cookie value and readCookie parses a named cookie from a raw header (a malformed cookie is tolerated, not a 500).
Examples
Mint and read a signed session cookie
Mint after verifying an upstream identity; read the cookie and verify on each request.
import { mintSession, readSessionToken, sessionCookie, readCookie, SESSION_COOKIE, SESSION_TTL_SEC } from "@nightowlsdev/auth-jwt";
const SECRET = process.env.SESSION_SECRET!;
const now = Math.floor(Date.now() / 1000);
// After you verified who the user is, mint a session and set it as an httpOnly cookie.
const token = mintSession({ tenantId: "acme", userId: "u1" }, SECRET, now);
const setCookie = sessionCookie(token, SESSION_TTL_SEC); // Set-Cookie value
// On each request: read the cookie, verify the token.
const raw = readCookie(req.headers.get("cookie"), SESSION_COOKIE);
const identity = readSessionToken(raw, SECRET, now); // { tenantId, userId, jti? } | nullVerify a provider's asymmetric access token
The verify path is pure — you fetch and cache the JWKS; it only checks the token.
import { verifyJwtWithJwks, type JwkSet } from "@nightowlsdev/auth-jwt";
const jwks: JwkSet = await fetchAndCacheProviderJwks(); // your fetch + cache
const now = Math.floor(Date.now() / 1000);
const claims = verifyJwtWithJwks(bearerToken, jwks, { nowSec: now });
// null on a bad signature, a non-ES256/RS256 alg, an unmatched kid, or expiry.Doing the parts it doesn't support
- Turning the primitives into a Night Owls AuthProviderWrap them in the one-method AuthProvider from @nightowlsdev/core: parse the Authorization header (or read the cookie), verify with verifyJwt / verifyJwtWithJwks, and return { tenantId, userId, capabilities } or null. Pass that provider to a runner exactly like auth-supabase / auth-auth0.
- Fetching and caching a JWKSverifyJwtWithJwks is pure and takes a JwkSet. Fetch the issuer's .well-known/jwks.json yourself, cache it (and refresh on an unknown kid), then hand the cached set in — that keeps verification deterministic and testable.
- Revoking a session before it expiresMint sessions with a jti (mintSession carries one when the identity has it) and record + check it against auth-sessions' makeSessionStore, so logout and admin kill work without shortening the TTL.
Related
- auth-sessions — The pg-backed revocable store that consumes the jti these primitives mint — logout and admin kill.
- auth-supabase — A drop-in Supabase AuthProvider if you would rather not assemble the verification yourself.
- auth-auth0 — A drop-in Auth0 AuthProvider built on jose, for comparison with rolling your own.
- core — Defines the AuthProvider / AuthContext seam you wrap these primitives into.