The missing piece in every JWT explainer
Most JWT guides explain header, payload, and signature — but skip the part that actually matters for verification in a real OAuth/OIDC system: where does the verifier get the key to check that signature? For tokens signed with an asymmetric algorithm (RS256, ES256), the answer is a JWKS — a JSON Web Key Set — published at a well-known URL that anyone can fetch and use to verify tokens, without ever holding a secret.
What a JWK looks like
A JSON Web Key (JWK) is a single public key represented as JSON, defined by RFC 7517. For an RSA key used to verify RS256-signed tokens:
{
"kty": "RSA",
"use": "sig",
"kid": "2026-07-key-1",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e": "AQAB"
}Two fields do the real work: `n` and `e` are the RSA public key's modulus and exponent, both base64url-encoded — together they reconstruct the actual public key mathematically. kid (key ID) is a label used to pick the right key out of a set. alg and use are hints about how the key is meant to be used, not enforcement — the consuming library should still confirm the algorithm matches what it expects.
The JWKS: a set of keys at a public URL
A JWKS is just a JSON object with a keys array. Identity providers (Auth0, Okta, AWS Cognito, Google, Firebase Auth) publish theirs at a predictable, well-known path:
GET https://your-tenant.auth0.com/.well-known/jwks.json{
"keys": [
{ "kty": "RSA", "kid": "2026-07-key-1", "use": "sig", "alg": "RS256", "n": "...", "e": "AQAB" },
{ "kty": "RSA", "kid": "2026-01-key-0", "use": "sig", "alg": "RS256", "n": "...", "e": "AQAB" }
]
}Notice there are two keys — this is what makes key rotation possible without downtime.
Why kid matters: rotating keys without breaking anyone
If a provider rotates its signing key, any token signed with the *old* key would fail verification the instant the old public key disappeared from the JWKS — but tokens issued minutes before rotation are often still in flight. The fix is the kid claim in the JWT's own header, which tells the verifier exactly which JWKS entry to use:
// JWT header — decoded from the first segment
{ "alg": "RS256", "typ": "JWT", "kid": "2026-01-key-0" }The verification flow:
1. Decode the JWT header (base64url, no verification yet) to read "kid".
2. Fetch the JWKS (or use a cached copy).
3. Find the key in "keys" whose "kid" matches.
4. Convert that JWK into a usable public key.
5. Verify the JWT's signature against that specific key.Because both the current and recently-retired keys stay in the JWKS for an overlap window, tokens signed just before a rotation still verify correctly — this is the entire reason JWKS is an array, not a single key.
Verifying a token against a JWKS in code
Most JWT libraries handle the fetch-and-match dance for you if you hand them a JWKS URL instead of a static key:
// Node.js — jwks-rsa + jsonwebtoken
import jwksClient from "jwks-rsa";
import jwt from "jsonwebtoken";
const client = jwksClient({ jwksUri: "https://your-tenant.auth0.com/.well-known/jwks.json" });
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
callback(err, key?.getPublicKey());
});
}
jwt.verify(token, getKey, { algorithms: ["RS256"] }, (err, decoded) => {
if (err) return console.error("Invalid token:", err.message);
console.log(decoded);
});# Python — PyJWT + PyJWKClient
import jwt
from jwt import PyJWKClient
url = "https://your-tenant.auth0.com/.well-known/jwks.json"
signing_key = PyJWKClient(url).get_signing_key_from_jwt(token)
decoded = jwt.decode(token, signing_key.key, algorithms=["RS256"], audience="your-api")Why asymmetric verification changes the security model
With a symmetric algorithm (HS256), the same secret both signs and verifies — meaning every service that needs to *check* a token also has the power to *forge* one. With RS256/ES256 and a JWKS, the identity provider keeps the private key entirely to itself and publishes only the public half. Any number of microservices can verify tokens independently, with no shared secret to leak, rotate carefully, or accidentally commit to a repo.
Common JWKS mistakes
- Not caching the JWKS. Fetching it on every single request adds latency and load on the identity provider; cache it (most libraries do this for you) and refresh on a
kidcache-miss or a TTL. - Ignoring `kid` and just trying every key. Works for a tiny JWKS, but is fragile and slow to scale — always match on
kidfirst. - Not verifying `alg`. A well-known JWT attack swaps the header's
algtononeor toHS256(tricking a naive verifier into treating the RSA public key as an HMAC secret). Always pin the expected algorithm(s) explicitly, as shown in thealgorithms: ["RS256"]option above — never let the token itself dictate which algorithm to verify with. - Trusting an unpinned JWKS URL. Fetch it only from the identity provider's real, documented
.well-knownendpoint — never one that arrives as user input.