Skip to Content
@exortek/jwkthumbprint — RFC 7638 / 9278

jwk.thumbprint / jwk.thumbprintURI / jwk.matches

Stable, kty-defined digests over the JWK’s required members. A thumbprint gives every key a canonical identifier independent of kid / whitespace / member order — perfect for equality checks, kid fallbacks, and cross-projection comparisons.

import { jwk } from '@exortek/jwk' const digest = await jwk.thumbprint(publicJwk) // 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs' const uri = await jwk.thumbprintURI(publicJwk) // 'urn:ietf:params:oauth:jwk-thumbprint:sha-256:NzbLsXh8u…' const same = await jwk.matches(publicJwk, privateJwk) // true — thumbprint of both projections is identical.

jwk.thumbprint(jwk, digest='sha256')

RFC 7638 §3. The digest is computed over a canonical JSON projection of the JWK’s kty-required members — nothing else. kid / use / alg / key_ops are ignored on purpose so the identity is stable through decoration changes.

digestOutput length (bytes)Use case
'sha256'32Default. RFC-standard.
'sha384'48Higher security margin.
'sha512'64Maximum classical strength.

The canonical projection

Per kty, the members that participate in the digest — in lexicographic order:

ktyMembers hashed
ECcrv, kty, x, y
RSAe, kty, n
octk, kty
OKPcrv, kty, x

The JSON is minimal — no whitespace, no trailing newline — and byte-for-byte compatible with the RFC 7638 §3.1 reference vector. Our test suite pins this exactly.

jwk.thumbprintURI(jwk, digest='sha256')

RFC 9278 §3. Wraps a thumbprint in the canonical URI form so it can be used as an identifier value in JOSE / OAuth / OIDC contexts (sub, iss, JWKS kid fallback, etc):

urn:ietf:params:oauth:jwk-thumbprint:sha-256:<base64url-digest>

The digest label follows RFC 9278 (sha-256, sha-384, sha-512 — hyphenated), not the Node function name (sha256).

jwk.matches(a, b, digest='sha256')

Semantic equality across projections. Two JWKs match when their kty-required members produce the same digest, regardless of kid / use / alg decoration — even when one is the private form and the other is the public projection.

const { publicJwk, privateJwk } = await jwk.generate('EC', { curve: 'P-256' }) await jwk.matches(publicJwk, privateJwk) // true

Constant-time comparison is unnecessary here — thumbprints are public identifiers, and string equality is the correct primitive. matches computes both digests with Promise.all and returns a plain ===.

Common uses

Set kid from thumbprint. JWKS producers often want a stable, content-derived kid:

const jwk = /* … */ jwk.kid = await jwk.thumbprint(jwk)

Detect key rotation. Store the thumbprint of every key you’ve issued; a new JWK from an OIDC provider whose thumbprint you don’t recognise is one you must re-authorize.

Verify a claim in a JWT. sub claims carrying a thumbprint URI can be checked with a single call:

const expected = await jwk.thumbprintURI(publicJwk) if (jwtPayload.sub !== expected) throw new Error('sub mismatch')

Errors

CodeWhen
UNSUPPORTED_ALGORITHMdigest not one of sha256 / sha384 / sha512.
UNSUPPORTED_KTYkty not one of the four thumbprintable kinds.
MISSING_REQUIRED_MEMBERA member required for the canonical projection is absent.
INVALID_ARGUMENTThe JWK isn’t an object.
Last updated on