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
Flattened (1 signer)
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:
| Field | Type | Notes |
|---|---|---|
alg | string | REQUIRED. JOSE algorithm identifier. |
kid | string | Shortcut for kid in the protected header. |
header | Record<string, unknown> | Extra protected header parameters. |
crit | string[] | Critical parameter list. |
unprotected | Record<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.algallowlist is mandatory.alg: 'none'on any signature is refused.critstrict 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
| Code | When |
|---|---|
INVALID_ARGUMENT | Empty signers array, malformed signer spec. |
MISSING_ALG_ALLOWLIST | Verify options missing / malformed. |
ALGORITHM_NONE_FORBIDDEN | Any signature (sign or verify) uses alg: 'none'. |
ALGORITHM_MISMATCH | Signature alg outside allowlist (per-signature). |
INVALID_TOKEN | Malformed JSON shape — missing payload, no signature/signatures present. |
INVALID_HEADER | Malformed protected header per signature. |
INVALID_KEY | Alg-key mismatch per signature. |
INVALID_SIGNATURE | All signatures failed to verify. |
INVALID_PAYLOAD | signJson: payload is undefined. verifyJson: payload decode failure. |
CRIT_UNSUPPORTED | A signature’s crit lists a name the verifier doesn’t understand. |