jwtjwksoauthsecurityjson

JSON Web Key Sets (JWKS): How OAuth Actually Verifies Tokens

·9 min read·Security & Auth

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:

json
{
  "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:

text
GET https://your-tenant.auth0.com/.well-known/jwks.json
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:

json
// JWT header — decoded from the first segment
{ "alg": "RS256", "typ": "JWT", "kid": "2026-01-key-0" }

The verification flow:

text
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:

javascript
// 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
# 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 kid cache-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 kid first.
  • Not verifying `alg`. A well-known JWT attack swaps the header's alg to none or to HS256 (tricking a naive verifier into treating the RSA public key as an HMAC secret). Always pin the expected algorithm(s) explicitly, as shown in the algorithms: ["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-known endpoint — never one that arrives as user input.

Frequently asked questions

No. A JWT is a signed token carrying claims (like a user ID). A JWK is a JSON representation of a single cryptographic key. A JWKS bundles multiple JWKs so a verifier can find the right public key for a given token.

It's almost always at /.well-known/jwks.json relative to the identity provider's issuer URL, and for OpenID Connect providers it's also listed in the jwks_uri field of /.well-known/openid-configuration.

Yes — fetch the URL and paste the JSON into JSONKit's JSON Formatter to read the keys array, or decode a token itself with the JWT Decoder to read its kid and compare it against the set.

Identity providers keep recently-rotated keys around for an overlap window so in-flight tokens don't suddenly fail — an unfamiliar kid is usually just a previous signing key still valid for tokens issued before the last rotation.

Try JSONKit — JSON Developer Tools

Format, validate, convert and diff JSON. Instant, accurate and 100% private.