Skip to content
Night Owls.dev
Jump to a page

@nightowlsdev/auth-jwt

Auth

Dependency-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-jwt

Key exports

  • signJwt
  • verifyJwt
  • verifyJwtWithJwks
  • mintSession
  • sessionCookie

Usage

auth-jwt.ts
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.

auth-jwt-example-1.ts
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? } | null

Verify a provider's asymmetric access token

The verify path is pure — you fetch and cache the JWKS; it only checks the token.

auth-jwt-example-2.ts
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-sessionsThe pg-backed revocable store that consumes the jti these primitives mint — logout and admin kill.
  • auth-supabaseA drop-in Supabase AuthProvider if you would rather not assemble the verification yourself.
  • auth-auth0A drop-in Auth0 AuthProvider built on jose, for comparison with rolling your own.
  • coreDefines the AuthProvider / AuthContext seam you wrap these primitives into.