By AndyPublished
What Is JWKS? JSON Web Key Sets, jwks_uri and Verifying JWTs
keys array of public keys, each one a JSON Web Key (JWK). Identity providers publish it at a URL, the jwks_uri, so that any API can verify the signatures on the tokens they issue without sharing a secret. The format is defined in RFC 7517. When an API receives a JWT, it reads the kid from the token header, finds the JWK with the same kid in the set, and verifies the signature with that public key. This guide covers the JWK fields, how to find a provider's JWKS endpoint, caching and key rotation, and working verification code for Node.js and Python.JWKS Meaning and Full Form
The full form of JWKS is JSON Web Key Set. It is one of the JOSE (JSON Object Signing and Encryption) specifications alongside JWS (RFC 7515), JWE (RFC 7516), JWA (RFC 7518) and JWT (RFC 7519). RFC 7517 defines two things:
- ·JWK (JSON Web Key): a JSON object that represents one cryptographic key.
- ·JWK Set: a JSON object with a single required member,
keys, whose value is an array of JWKs (RFC 7517 §5).
A JWKS only makes sense for asymmetric algorithms such as RS256, PS256 and ES256, where the verifying key can be public. With HS256 the same secret signs and verifies, so publishing it would let anyone mint tokens. If that distinction is new, HS256 vs RS256 covers it.
JWKS vs JWT (and JWT vs JWK)
These are easy to mix up because the names differ by one letter:
- ·JWT is the token: a signed set of claims such as
sub,issandexp. See what is a JWT. - ·JWK is a key, expressed as JSON. It is not a token and carries no claims about a user.
- ·JWKS is a published list of JWKs, normally the issuer's current signing keys plus any keys that are being rotated in or out.
The relationship is one-directional: the issuer signs a JWT with a private key, and the JWKS publishes the matching public key. The JWT points at its key through the kid header.
JWKS Example
Below is the shape of a real JWKS, trimmed. The first key is RSA (the modulus is shortened here), the second is an EC P-256 key.
{
"keys": [
{
"kty": "RSA",
"kid": "943a3a5d7d919625a454e489b75c29adab57acba",
"use": "sig",
"alg": "RS256",
"n": "pIpnzA2ezyEERJSxiqpLBmMeIqATH-V6iuBtKIib...",
"e": "AQAB"
},
{
"kty": "EC",
"kid": "ec-2026-09",
"use": "sig",
"alg": "ES256",
"crv": "P-256",
"x": "XSjZksk5dCoq0zwbizuNVlQSWcPQ8wkP_dgK18zq3C4",
"y": "eI3sdta7q5ni5JH584cQtaPzY9x5lGJ57Ayz5MtHJ0U"
}
]
}A JWKS contains public material only. If you ever see d, p,q or k members in a published set, private or symmetric key material has leaked and the keys must be replaced.
JWKS Keys: the JWK Fields
RFC 7517 §4 defines the common parameters (kty, use,key_ops, alg, kid and the X.509 members). The key-type-specific members, such as n and e for RSA or crv, x and y for EC, are defined in RFC 7518 §6. OKP keys (Ed25519) come from RFC 8037.
| Field | Applies to | Meaning |
|---|---|---|
| kty | All keys | Key type: RSA, EC, oct (symmetric) or OKP (Ed25519/X25519). Required. |
| kid | All keys | Key ID. Matched against the kid header of the JWT to pick the right key. |
| use | All keys | Intended use: sig (signatures) or enc (encryption). |
| alg | All keys | Algorithm the key is meant for, e.g. RS256 or ES256. Optional but useful. |
| key_ops | All keys | Finer-grained alternative to use, e.g. ["verify"]. Rare in public JWKS. |
| x5c / x5t | All keys | X.509 certificate chain / certificate thumbprint for the same key. |
| n | RSA | Modulus, base64url-encoded big-endian integer. |
| e | RSA | Public exponent, almost always AQAB (65537). |
| crv | EC / OKP | Curve: P-256, P-384, P-521, or Ed25519 for OKP. |
| x, y | EC | Public point coordinates, base64url-encoded. OKP keys have x only. |
Providers add their own members too. Microsoft Entra ID's keys, for example, carry x5t,x5c and an issuer member. Libraries ignore members they do not understand, as RFC 7517 requires.
JWKS Endpoint, jwks_uri and JWKS URL
"JWKS endpoint", "JWKS URL" and jwks_uri all mean the same thing: the HTTPS URL where the set is served. jwks_uri is the name of the field in an OpenID Connect discovery document (and in OAuth authorisation server metadata, RFC 8414). The reliable way to find it is to read the issuer's discovery document rather than hard-coding a path:
# OpenID Connect discovery: <issuer>/.well-known/openid-configuration curl -s https://accounts.google.com/.well-known/openid-configuration | jq -r .jwks_uri # https://www.googleapis.com/oauth2/v3/certs curl -s https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration | jq -r .jwks_uri # https://login.microsoftonline.com/common/discovery/v2.0/keys
Paths vary between providers. Auth0 serves /.well-known/jwks.json, Keycloak serves/realms/<realm>/protocol/openid-connect/certs, and Google uses the/oauth2/v3/certs path shown above. Discovery removes the guesswork. For the wider picture of how discovery, ID tokens and access tokens fit together, see JWTs in OAuth 2.0 and OpenID Connect.
jwks_uri from configuration you control, never from the token. A JWT header can carry jku (a JWKS URL), and RFC 8725 §3.10 warns that following it blindly can lead to server-side request forgery and to accepting keys chosen by an attacker.JWKS Validation: How Verification Works
Verifying a JWT against a JWKS follows the same steps in every library:
- ·Decode the header (without trusting it yet) and read
kidandalg. - ·Check
algis on your allow-list. Never let the token choose between HS256 and RS256; see JWT security best practices. - ·Find the JWK whose
kidmatches, ideally also matchingkty,use: "sig"andalg. - ·If no key matches, re-fetch the JWKS once (the issuer may have rotated), subject to a cooldown.
- ·Verify the signature, then validate
iss,aud,expandnbf.
A valid signature only proves which key signed the token. The claim checks decide whether this API should accept it; audience validation in particular is what stops a token issued for another application from working on yours.
Node.js: jose createRemoteJWKSet
import { createRemoteJWKSet, jwtVerify } from 'jose';
// Create once at startup; it caches keys and handles unknown kids.
const JWKS = createRemoteJWKSet(new URL('https://www.googleapis.com/oauth2/v3/certs'));
export async function verifyGoogleIdToken(token) {
const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
issuer: 'https://accounts.google.com',
audience: process.env.GOOGLE_CLIENT_ID,
algorithms: ['RS256'],
});
return payload;
}In jose v6, createRemoteJWKSet keeps the set for 10 minutes by default (cacheMaxAge), re-fetches when a token's kid is not found but at most once per 30 seconds (cooldownDuration), and times out requests after 5 seconds (timeoutDuration). A token whose key still is not found fails withERR_JWKS_NO_MATCHING_KEY ("no applicable key found in the JSON Web Key Set").
Node.js: jwks-rsa with jsonwebtoken
If your code already uses jsonwebtoken, the jwks-rsa package fetches the key for a kid and returns it as PEM:
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const client = jwksClient({
jwksUri: 'https://YOUR_TENANT.auth0.com/.well-known/jwks.json',
cache: true, // default cacheMaxAge is 10 minutes
rateLimit: true,
});
function getKey(header, callback) {
client.getSigningKey(header.kid)
.then((key) => callback(null, key.getPublicKey()))
.catch(callback);
}
jwt.verify(token, getKey, { algorithms: ['RS256'], audience: 'https://api.example.com' },
(err, payload) => { /* ... */ });Python: PyJWT PyJWKClient
import jwt
from jwt import PyJWKClient
# Create once and reuse; the JWK set is cached (lifespan defaults to 300 s).
jwks_client = PyJWKClient("https://www.googleapis.com/oauth2/v3/certs")
def verify(token: str, client_id: str) -> dict:
signing_key = jwks_client.get_signing_key_from_jwt(token)
return jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=client_id,
issuer="https://accounts.google.com",
)PyJWKClient needs the crypto extra (pip install "pyjwt[crypto]"). When the kid is not in the set, get_signing_key_from_jwtraises PyJWKClientError: Unable to find a signing key that matches: "...".
JWKS Caching and Key Rotation
Fetching the JWKS on every request adds latency and makes your API depend on the provider's uptime for every call. Fetching it once at deploy breaks the day the provider rotates keys. The standard pattern sits between the two:
- ·Cache with a TTL. Respect
Cache-Controlif you can; Google's certs endpoint, for instance, sends amax-ageof several hours. Library defaults (10 minutes in jose and jwks-rsa, 5 minutes in PyJWKClient) are safe. - ·Refresh on unknown kid, with a cooldown so a flood of tokens carrying random
kidvalues cannot turn your API into a traffic amplifier against the provider. - ·Keep serving the cached set if a refresh fails, rather than rejecting every token during a brief provider outage.
From the issuer's side, rotation is a three-step overlap: publish the new public key in the JWKS, wait longer than consumers' cache TTL, start signing with the new key, then remove the old key once every token signed with it has expired. Skipping the waiting step is the classic cause of a burst of "invalid signature" errors right after a rotation. The kid header guide goes into rotation in more detail.
Converting a JWK to PEM
Many tools, including the jwtdecode.app verifier, take a public key in PEM (SPKI) format rather than a JWK. Converting is one call in Node.js 16+:
// jwk-to-pem.mjs — usage: node jwk-to-pem.mjs <jwks_uri> [kid]
import { createPublicKey } from 'node:crypto';
const [url, kid] = process.argv.slice(2);
const { keys } = await (await fetch(url)).json();
const jwk = kid ? keys.find((k) => k.kid === kid) : keys[0];
console.log(createPublicKey({ key: jwk, format: 'jwk' })
.export({ type: 'spki', format: 'pem' }));The same works for EC keys. In Python, jwt.PyJWK(jwk).key gives acryptography public key object:
import jwt
from cryptography.hazmat.primitives import serialization
key = jwt.PyJWK(jwk_dict).key
pem = key.public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo,
).decode()Paste the token into the decoder, read its kid, convert the matching JWK to PEM, and paste the PEM into the verification panel. Everything runs locally in the browser; the decoder does not fetch a JWKS itself, so the conversion step is yours. For the same workflow without a browser, see the verification guide.
Common JWKS Problems
- ·No matching key: the token came from a different tenant, environment or issuer than the JWKS you are loading, or you cached the set across a rotation. Compare the token's
isswith the discovery document you used. - ·Signature invalid with the right kid: usually an algorithm mismatch, such as a PS256 token checked as RS256.
- ·Works in dev, fails in production: the two environments use different issuers and therefore different JWKS URLs.
- ·Token has no kid: some issuers with a single key omit it. jose then accepts the token only if exactly one key in the set is compatible, and otherwise fails with
ERR_JWKS_MULTIPLE_MATCHING_KEYS.
The common JWT errors page lists the exact error strings for each library.
Summary
A JWKS (JSON Web Key Set, RFC 7517) is the issuer's list of public keys, served at the jwks_uri from its /.well-known/openid-configuration document. Each JWK carries a kty, a kid and the key material (n/e for RSA,crv/x/y for EC). Verifiers pick the key by the token's kid, cache the set, refresh it on an unknown kid with a cooldown, and still validate iss, aud and exp after the signature passes. Use createRemoteJWKSet in Node.js or PyJWKClient in Python rather than writing the fetch-and-cache logic yourself.