Skip to Content
@exortek/jwsOverview

@exortek/jws

JSON Web Signature for Node.js 22+ — RFC 7515 (core), RFC 7518 §3 (algorithms), RFC 7797 (unencoded payload), RFC 8037 (Ed25519 / Ed448), RFC 8812 (secp256k1), RFC 8725 (best current practices). Zero dependencies. Pure node:crypto.

Compact and JSON serialisation, detached content, unencoded payload, HMAC / RSA / RSA-PSS / ECDSA / EdDSA — with five modern guarantees jose doesn’t enforce out of the box.

Why this exists

jose is the reference JOSE library, and it is excellent. Its verify contract, however, lets you leave the algorithm allowlist off — which is the #1 way JWT authenticators have been bypassed for a decade. Other Node JOSE libraries either predate modern secure defaults (jsonwebtoken) or skip the JWS layer entirely (fast-jwt).

@exortek/jws sits in the JWS layer, is server-only, has zero runtime dependencies, and refuses to let the caller opt out of the checks that matter.

The five guarantees

Every one of these is enforced by construction. There is no flag, no environment variable, no unsafe: true option that turns them off.

1. Algorithm allowlist is mandatory on verify

await verify(token, key, { alg: ['ES256'] }) // ✓ await verify(token, key) // ✗ MISSING_ALG_ALLOWLIST await verify(token, key, {}) // ✗ MISSING_ALG_ALLOWLIST await verify(token, key, { alg: [] }) // ✗ MISSING_ALG_ALLOWLIST

No default, no fallback.

2. alg: 'none' is refused everywhere

Sign side, verify side, JSON serialisation surface. The algorithm table has no none entry, and even before that the sign / verify entry points short-circuit with ALGORITHM_NONE_FORBIDDEN. CVE-2015-2951 regressions are pinned by dedicated tests.

3. crit is strict by default

Unknown critical headers raise CRIT_UNSUPPORTED. Opt in named extensions via options.knownCriticalHeaders. b64 is built-in for RFC 7797.

4. Async key resolver is first-class

await verify(token, async header => store.get(header.kid), { alg: ['ES256'] })

Plain function. Return a KeyObject, JWK, or Buffer; throw KEY_NOT_FOUND yourself if the store misses. jose’s equivalent requires the createLocalJWKSet factory dance.

5. Granular ErrorCode enum

13 machine-branchable codes. switch (err.code) reads well, is grep-able, and doesn’t rot when a message changes phrasing.

What ships

  • Compact serialisationsign(payload, key, { alg }) and verify(token, key, { alg: [...] }).
  • Detached content (RFC 7515 §F)signDetached(bytes, key, opts) / verifyDetached(token, bytes, key, opts) — payload rides out-of-band.
  • Unencoded payload (RFC 7797)sign(payload, key, { alg, b64: false }), crit: ['b64'] auto-added, . in payload refused.
  • JSON serialisationsignJson / verifyJson, general + flattened forms, multi-signature over one payload.
  • Async key resolververify(token, async header => key, opts).
  • JWK array with kid dispatchverify(token, jwks, opts).
  • UNSAFE inspectiondecode(token) and decodeProtectedHeader(token) for reading a token without verifying (extract kid before resolver lookup, debug from a log).

Algorithm matrix

FamilyalgNode primitive
HMACHS256, HS384, HS512crypto.createHmac(sha).digest()
RSA-PKCS#1RS256, RS384, RS512crypto.sign(sha, ..., { padding: RSA_PKCS1_PADDING })
RSA-PSSPS256, PS384, PS512crypto.sign(sha, ..., { padding: RSA_PKCS1_PSS_PADDING, saltLength })
ECDSAES256, ES384, ES512crypto.sign(sha, ..., ecKey) + raw R‖S ↔ DER conversion
ECDSA (BTC)ES256Ksecp256k1 (RFC 8812) — Web3 interop
EdDSAEdDSA (Ed25519, Ed448)crypto.sign(null, ..., okpKey) per RFC 8037

Post-quantum roadmap

ML-DSA (FIPS 204) and ML-KEM (FIPS 203) are on the roadmap. The gate is node:crypto native support:

  • OpenSSL added ML-DSA / ML-KEM in 3.5 (April 2025).
  • Node.js 22–24 ships OpenSSL 3.0–3.4 — no native PQ yet.
  • Node.js 25 / 26 (2026–2027) will expose them once the OpenSSL bump lands.
  • draft-ietf-jose-pqc (JOSE registrations) is still a draft.

When both boxes tick we add alg: 'ML-DSA-{44,65,87}' to this same sign / verify surface. Until then, @noble/post-quantum is the credible JavaScript alternative.

Where next?

  • sign — compact, detached, and b64: false sign flows.
  • verify — mandatory allowlist, polymorphic keys, kid dispatch.
  • decode — UNSAFE inspection.
  • json — general + flattened multi-signature.
  • errors — full JwsError + ErrorCode catalogue.
Last updated on