Skip to Content
@exortek/jwsjson — general + flattened serialisation

jws.signJson / jws.verifyJson

JSON serialisation (RFC 7515 §7.2) — general (multi-signature) and flattened (single-signature) forms. One payload signed by one or more keys, delivered as JSON rather than the dotted compact string.

import { signJson, verifyJson } from '@exortek/jws' const jws = await signJson(payload, [ { key: hmacSecret, options: { alg: 'HS256', kid: 'hs1' } }, { key: rsaPrivateKey, options: { alg: 'RS256', kid: 'rs1' } }, ]) // General form: { payload, signatures: [ ..., ... ] } const { matchedSignatureIndex, header, payload } = await verifyJson(jws, jwks, { alg: ['HS256', 'RS256'], })

Forms

Single-signer output. Top-level protected / header / signature alongside the payload:

{ "payload": "eyJ4IjoxfQ", "protected": "eyJhbGciOiJIUzI1NiJ9", "header": { "source": "admin" }, "signature": "…" }

signJson picks the flattened form for one signer and the general form for two or more. verifyJson accepts either shape.

signJson(payload, signers)

Payload is serialised once (base64url) and each signer computes its own signature over ${encProtected}.${encPayload}. Per-signer options:

FieldTypeNotes
algstringREQUIRED. JOSE algorithm identifier.
kidstringShortcut for kid in the protected header.
headerRecord<string, unknown>Extra protected header parameters.
critstring[]Critical parameter list.
unprotectedRecord<string, unknown>Emitted as the JSON header field alongside protected.
await signJson({ user: 'u1' }, [ { key, options: { alg: 'ES256', kid: 'k1', unprotected: { source: 'admin' } } }, ])

verifyJson(jws, keyish, options)

Multi-signature verification returns the first signature that resolves + verifies. Signatures whose keys are unavailable (KEY_NOT_FOUND) or whose bytes are tampered with (INVALID_SIGNATURE) are skipped so a JWS with one valid + one tampered signature still authenticates.

matchedSignatureIndex tells the caller which signer landed the token — useful for audit logs or downstream routing.

If every signature fails, the most recent JwsError is thrown (or a final INVALID_SIGNATURE if none produced one).

Same security invariants as the compact verify:

  • options.alg allowlist is mandatory.
  • alg: 'none' on any signature is refused.
  • crit strict by default.
  • Multi-key JWK array + async resolver support the same shapes.

Out of scope for 1.0

b64: false in the JSON serialisation is not supported in 1.0. signJson always base64url-encodes the payload; verifyJson always decodes it as base64url. RFC 7797 §5 permits the unencoded payload in the JSON form too, but it is rare in practice — most b64: false deployments use the compact or detached form. Use sign / signDetached with b64: false for those; open an issue if you need the JSON variant.

kid dispatch in verifyJson reads only the protected header. RFC 7515 §7.2 allows a per-signature kid in the unprotected header member too. Signatures that carry kid only in the unprotected header currently fail with KEY_NOT_FOUND when using JWK-array dispatch. Move kid into the protected header, or use an async resolver that looks at both places.

Errors

CodeWhen
INVALID_ARGUMENTEmpty signers array, malformed signer spec.
MISSING_ALG_ALLOWLISTVerify options missing / malformed.
ALGORITHM_NONE_FORBIDDENAny signature (sign or verify) uses alg: 'none'.
ALGORITHM_MISMATCHSignature alg outside allowlist (per-signature).
INVALID_TOKENMalformed JSON shape — missing payload, no signature/signatures present.
INVALID_HEADERMalformed protected header per signature.
INVALID_KEYAlg-key mismatch per signature.
INVALID_SIGNATUREAll signatures failed to verify.
INVALID_PAYLOADsignJson: payload is undefined. verifyJson: payload decode failure.
CRIT_UNSUPPORTEDA signature’s crit lists a name the verifier doesn’t understand.
Last updated on