Understanding JSON Web Tokens: the keys, with JWK

Signing and encrypting both need keys. RFC 7517 defines how to represent one as JSON, and how to publish a set of them.

A key is an object

{
  "kty": "EC",
  "crv": "P-256",
  "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
  "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
  "kid": "signing-2026-07",
  "use": "sig",
  "alg": "ES256"
}

kty is the only mandatory member. The others matter in practice: kid names the key, use says whether it signs or encrypts, alg restricts it to one algorithm.

Restrict your keys

use and key_ops look like documentation. They are not. A key that can both sign and encrypt is a key an attacker can use in a protocol you did not intend. Declare the narrowest purpose and have your library enforce it.

Key sets, and what to publish

A JWKSet is an object with a keys array. Publishing yours at a well-known URL lets others verify your signatures without any manual exchange. It is what OpenID Connect providers do.

Publish public keys only. This sounds obvious and it is the single most common accident: an RSA private key differs from its public counterpart by a few extra members, d, p, q, and serialising the wrong object publishes your signing key to the world.

Thumbprints

RFC 7638 defines a thumbprint: a hash over the key’s essential members, in a canonical order. Two representations of the same key give the same thumbprint, which makes it a stable identifier when you do not control the kid.

Rotation without downtime

The recipe is the same everywhere and worth stating once:

  1. Add the new key to the published set. Keep signing with the old one.
  2. Wait for consumers to refresh their copy. Their cache lifetime sets this delay, so publish a sane Cache-Control.
  3. Start signing with the new key. Keep the old one in the set.
  4. Once every token signed with the old key has expired, remove it.

Skipping step 2 is what causes a rotation to look instantaneous in staging and break in production.