How JWKS and JWT Validation Work
When a service receives a JSON Web Token (JWT), it faces a fundamental question: is this token genuine? The answer lies not in the token itself but in a public key infrastructure purpose-built for the web — JWKS, the JSON Web Key Set. While JWTs define the token format and OAuth 2.0 defines the authorization flows, JWKS is the mechanism that connects the two: it tells relying parties where to find the cryptographic keys needed to verify a token's signature. This article dives deep into that key infrastructure — how keys are published, discovered, rotated, and used to validate tokens at scale.
JWT Structure Recap
A JWT is a compact, URL-safe string composed of three Base64url-encoded segments separated by dots:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYzEyMyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUuY29tIiwiYXVkIjoiYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzE0MjAwMDAwfQ.signature_bytes_here
The three parts are:
- Header — declares the signing algorithm (
alg) and, critically, the Key ID (kid) that identifies which key was used to sign the token - Payload — contains the claims (issuer, audience, expiration, subject, custom data)
- Signature — the cryptographic proof that the header and payload have not been tampered with, produced using the signer's private key
The header and payload are simply JSON objects, Base64url-encoded. Anyone can decode and read them. The signature is what makes a JWT trustworthy — and verifying that signature requires the signer's public key. That is where JWKS comes in.
Signing Algorithms: Asymmetric vs Symmetric
JWTs can be signed using symmetric or asymmetric algorithms. The choice determines everything about how keys are managed and how validation works.
Symmetric: HMAC (HS256 / HS384 / HS512)
HMAC algorithms use a single shared secret for both signing and verification. The issuer and the verifier must both possess the same key. This works when the signer and verifier are the same service (or fully trust each other), but it breaks down in distributed systems: you cannot share a secret with every microservice and every third party that needs to verify tokens without creating a massive secret management burden and expanding your attack surface.
Asymmetric: RSA, ECDSA, EdDSA
Asymmetric algorithms use a key pair — a private key (kept secret by the issuer) and a public key (published openly). Anyone with the public key can verify a token's signature, but only the holder of the private key can produce valid signatures. This is the foundation of JWKS: the identity provider publishes its public keys, and any service in any network can validate tokens without ever needing access to the private key.
The commonly used asymmetric algorithms for JWTs are:
- RS256 / RS384 / RS512 — RSA with PKCS#1 v1.5 padding and SHA-256/384/512 hashes. RS256 is by far the most common algorithm in production identity providers. RSA keys are typically 2048 or 4096 bits. Signatures are large (256 bytes for 2048-bit keys) but verification is fast.
- PS256 / PS384 / PS512 — RSA with PSS (Probabilistic Signature Scheme) padding. PSS is considered more robust than PKCS#1 v1.5 because it is provably secure under the RSA assumption and produces randomized signatures (the same input produces different signatures each time). Recommended for new systems that need RSA compatibility.
- ES256 / ES384 / ES512 — ECDSA (Elliptic Curve Digital Signature Algorithm) using the P-256, P-384, or P-521 curves respectively. ECDSA provides equivalent security to RSA with dramatically smaller keys: a 256-bit EC key provides roughly the same security as a 3072-bit RSA key. Signatures are also much smaller (64 bytes for ES256 vs 256 bytes for RS256), which matters when tokens are passed in HTTP headers.
- EdDSA — Edwards-curve Digital Signature Algorithm, typically using the Ed25519 curve. EdDSA is the newest option, offering the best performance characteristics: fast signing, fast verification, small signatures (64 bytes), and deterministic output (no random nonce needed, eliminating an entire class of implementation bugs). Ed25519 is increasingly adopted but not yet universally supported by all JWT libraries.
The algorithm choice is encoded in the JWT header's alg field and in the JWK's alg field. A verifier must ensure these match and must never accept an algorithm the verifier does not explicitly expect — a point we will return to when discussing security pitfalls.
JWKS: JSON Web Key Sets
A JSON Web Key Set (JWKS) is a JSON document containing an array of public keys. It is defined in RFC 7517. Identity providers publish their JWKS at a well-known URL so that any service can fetch the keys needed to verify tokens. This is the key distribution mechanism that makes JWT-based authentication work across organizational boundaries.
The .well-known/jwks.json Endpoint
By convention, identity providers expose their public keys at a URL like:
https://auth.example.com/.well-known/jwks.json
Real-world examples:
- Auth0:
https://{tenant}.auth0.com/.well-known/jwks.json - Okta:
https://{domain}.okta.com/oauth2/default/v1/keys - Google:
https://www.googleapis.com/oauth2/v3/certs - Microsoft Azure AD:
https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys - Keycloak:
https://{host}/realms/{realm}/protocol/openid-connect/certs - AWS Cognito:
https://cognito-idp.{region}.amazonaws.com/{poolId}/.well-known/jwks.json
An HTTP GET to this URL returns a JSON object with a keys array, each entry being a JWK (JSON Web Key).
JWK Format
Each key in the JWKS is a JSON object with fields that describe the key's type, its intended use, its identity, and the actual key material. Here is an example RSA public key in JWK format:
{
"keys": [
{
"kty": "RSA",
"kid": "abc123",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbf...",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "def456",
"use": "sig",
"alg": "RS256",
"n": "nzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s...",
"e": "AQAB"
}
]
}
The critical fields in a JWK:
kty(Key Type) — the cryptographic family:"RSA","EC"(elliptic curve), or"OKP"(octet key pair, used for EdDSA). This determines which other fields are present.kid(Key ID) — a unique identifier for this specific key. This is what the JWT header'skidfield references. Without it, a verifier could not determine which key from the set to use for verification.use— the intended use:"sig"for signature verification,"enc"for encryption. Most JWKS for JWT validation contain only"sig"keys.alg— the specific algorithm this key is intended for (e.g.,"RS256","ES256"). This lets verifiers reject tokens whosealgheader does not match the key's declared algorithm.nande— for RSA keys, the modulus and exponent, Base64url-encoded. These are the mathematical components of the RSA public key.xandy— for EC keys, the x and y coordinates on the curve, Base64url-encoded.crv— for EC and OKP keys, the curve name ("P-256","P-384","Ed25519").x— for OKP (EdDSA) keys, the public key value.
Notice that a JWKS typically contains multiple keys. This is by design — it enables key rotation without downtime, which we will cover in depth shortly.
The JWT Validation Flow
When a service receives a JWT (typically in the Authorization: Bearer <token> header), it must perform a precise sequence of checks before trusting the token's claims. Every step is mandatory — skipping any one can create a security vulnerability.
Step 1: Decode the Header
The verifier Base64url-decodes the first segment to read the JWT header. The two critical fields are alg (the algorithm) and kid (the Key ID). The verifier must not blindly trust the alg field — it should only accept algorithms from an explicitly configured allowlist.
Step 2: Fetch the JWKS
Using the known issuer URL (configured on the verifier, never derived from the token), the verifier fetches the JWKS. In practice, the JWKS is cached locally and refreshed periodically (typically every 5-15 minutes) or when an unknown kid is encountered. The fetch must use HTTPS/TLS — fetching keys over plain HTTP would let an attacker substitute their own keys.
Step 3: Find the Matching Key
The verifier iterates through the keys array in the JWKS, looking for a JWK whose kid matches the JWT header's kid. If no match is found, the verifier may attempt to re-fetch the JWKS (in case the issuer rotated keys since the last cache refresh). If there is still no match, the token is rejected.
Step 4: Verify the Cryptographic Signature
With the public key in hand, the verifier computes the signature over the header and payload (the first two Base64url-encoded segments joined by a dot) using the declared algorithm and compares it to the signature in the token. If they do not match, the token has been tampered with or was signed by a different key.
Step 5-8: Validate Claims
After the signature is verified, the verifier decodes the payload and checks the registered claims:
exp(Expiration Time) — the token must not be used after this Unix timestamp. A small clock skew tolerance (typically 30-60 seconds) is common.nbf(Not Before) — the token must not be used before this time. Protects against tokens that are issued in advance.iat(Issued At) — the time the token was created. Some systems reject tokens older than a certain threshold regardless of theirexp.iss(Issuer) — must exactly match the expected issuer URL. This prevents a token issued byevil.example.comfrom being accepted by a service expecting tokens fromauth.example.com.aud(Audience) — must contain the identifier of the service performing the validation. This prevents a token intended forapi-a.example.comfrom being replayed againstapi-b.example.com.
Key Rotation and Why kid Matters
Cryptographic keys should not live forever. Private keys may be compromised. Algorithms weaken over time. Compliance policies mandate periodic rotation. But rotating signing keys for a JWT issuer serving millions of tokens is operationally complex: at the moment of rotation, there are tokens in flight signed with the old key that have not yet expired. If you remove the old public key from the JWKS immediately, all those tokens become unverifiable.
The Rotation Process
The standard key rotation process uses the JWKS's ability to hold multiple keys simultaneously:
- T0 — Normal operation. The JWKS contains one key (Key A). All tokens are signed with Key A.
- T1 — Publish the new key. Generate a new key pair (Key B). Add Key B's public key to the JWKS. Continue signing tokens with Key A. This gives relying parties time to cache the updated JWKS containing both keys.
- T2 — Switch signing to the new key. Start signing all new tokens with Key B. Key A remains in the JWKS so that tokens signed before the switch (which may still be within their
expwindow) can still be verified. - T3 — Remove the old key. After enough time has passed for all Key A-signed tokens to expire (typically the maximum token lifetime plus a buffer), remove Key A from the JWKS.
The grace period between T2 and T3 must be at least as long as the maximum token lifetime. If your access tokens have a 1-hour expiration, you should keep the old key in the JWKS for at least 1 hour after switching. Many providers keep it for 24 hours or more for safety.
Caching JWKS with TTL
Relying parties should cache the JWKS rather than fetching it for every token validation. A typical strategy:
- Cache the JWKS with a TTL of 5-15 minutes (many providers set
Cache-Controlheaders suggesting this). - If a token arrives with a
kidnot found in the cached JWKS, fetch the JWKS again immediately (the issuer may have rotated keys). - Rate-limit JWKS re-fetches to prevent a denial-of-service attack where an adversary sends tokens with unknown
kidvalues to force constant outbound HTTP requests. - If the re-fetch still does not contain the
kid, reject the token.
This caching strategy means that during key rotation, there is a brief window (up to the cache TTL) where some relying parties may not yet have the new key. This is why the T1 phase (publishing the new key before using it to sign) should be at least as long as the longest expected cache TTL across all relying parties.
OpenID Connect Discovery
The .well-known/jwks.json URL can be discovered automatically via OpenID Connect Discovery. An OpenID Connect provider publishes a metadata document at:
https://auth.example.com/.well-known/openid-configuration
This JSON document contains, among other things, a jwks_uri field that points to the JWKS endpoint:
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"response_types_supported": ["code", "token", "id_token"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"],
"subject_types_supported": ["public"]
}
The discovery document also tells you which algorithms the provider supports (id_token_signing_alg_values_supported), which is essential for configuring your verifier's allowlist. By using OpenID Connect Discovery, a relying party only needs to know the issuer URL — everything else (JWKS location, supported algorithms, endpoints) is discovered automatically.
This is analogous to how TLS/HTTPS uses certificate authorities to establish trust — OpenID Connect Discovery provides a standardized way to locate the trust anchors (public keys) for a given identity provider.
How Identity Providers Publish Keys
Each major identity provider has its own conventions, but the core mechanism is the same: an HTTPS endpoint serving a JWKS document. Here is how the major providers handle key publication and rotation:
Auth0
Auth0 uses RS256 by default and publishes keys at https://{tenant}.auth0.com/.well-known/jwks.json. Keys are rotated automatically, and the JWKS always contains both the current and the previous signing key. Auth0 also supports configuring custom signing keys and algorithms per application.
Okta
Okta exposes keys at /oauth2/{authServerId}/v1/keys. It supports automatic key rotation on a configurable schedule. Okta's JWKS typically contains 2-3 keys at any time to facilitate graceful rotation. The Cache-Control header on the JWKS response is set to allow caching for a few minutes.
Google publishes its keys at https://www.googleapis.com/oauth2/v3/certs and rotates them roughly every few weeks. The JWKS response includes explicit Cache-Control and Expires headers, and Google recommends caching based on those headers rather than using a fixed TTL.
Microsoft Azure AD / Entra ID
Azure AD uses the OpenID Connect discovery endpoint at https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration to advertise its JWKS URI. Keys are rotated on an unpredictable schedule (sometimes weekly, sometimes less frequently). Azure AD can publish 3-5 keys simultaneously in the JWKS. In multi-tenant scenarios, the common tenant JWKS aggregates keys from all Azure AD tenants.
Keycloak
Keycloak publishes per-realm keys at /realms/{realm}/protocol/openid-connect/certs. Administrators can configure key rotation policies, generate new keys, and control which algorithms are supported. Keycloak's admin console provides explicit key management, making it suitable for organizations that need fine-grained control over their key lifecycle.
AWS Cognito
Cognito publishes keys at https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json. Each user pool has its own key set. Cognito uses RS256 and rotates keys periodically, always ensuring at least two keys are present in the JWKS to allow overlap during rotation.
Federated Identity and Multi-Tenant Validation
In modern architectures, a service often needs to accept tokens from multiple identity providers. A B2B SaaS platform might accept tokens from its own Auth0 tenant for direct users, from a customer's Azure AD for enterprise SSO, and from Google Workspace for another customer. Each provider has its own JWKS endpoint, its own keys, and its own issuer.
Multi-Tenant Validation Strategy
The verifier must maintain a mapping of trusted issuers to their JWKS endpoints:
Trusted issuers:
"https://login.microsoftonline.com/{tenant-a}/v2.0"
-> JWKS: https://login.microsoftonline.com/{tenant-a}/discovery/v2.0/keys
"https://accounts.google.com"
-> JWKS: https://www.googleapis.com/oauth2/v3/certs
"https://myapp.auth0.com/"
-> JWKS: https://myapp.auth0.com/.well-known/jwks.json
When a token arrives, the verifier:
- Decodes the header and payload without verifying the signature
- Reads the
issclaim from the payload - Looks up the issuer in its trusted issuers list — if the issuer is not recognized, the token is rejected immediately
- Fetches (or uses cached) JWKS from the endpoint associated with that issuer
- Proceeds with the normal validation flow (kid match, signature verification, claims validation)
The critical security requirement here is that the issuer-to-JWKS mapping must be configured server-side, never derived from the token itself. If a verifier fetches the JWKS from a URL found in the token, an attacker can point it to a JWKS they control and sign tokens with their own keys.
Dynamic Tenant Registration
Some platforms support dynamic tenant onboarding where new identity providers are added at runtime. In these cases, the platform typically:
- Requires an administrator to register the new tenant's issuer URL and configure trust
- Validates that the issuer's OpenID Connect discovery document is well-formed and accessible over HTTPS
- Fetches and caches the JWKS from the discovered
jwks_uri - Stores the issuer-JWKS mapping in a database or configuration store
This pattern is common in identity federation platforms where the set of trusted providers changes over time, but each addition is still an explicit trust decision made by an administrator.
Security Pitfalls
JWT validation with JWKS has a well-documented list of attacks that exploit implementation mistakes. Understanding these is essential for any developer building JWT validation.
The alg=none Attack
The JWT specification defines an "alg": "none" option for unsecured JWTs — tokens that carry no signature at all. A vulnerable implementation that trusts the alg field from the token header might see "alg": "none", skip signature verification entirely, and accept the token. An attacker crafts a JWT with arbitrary claims, sets alg to none, removes the signature, and gains unauthorized access.
Defense: Never accept the alg from the token at face value. Maintain a server-side allowlist of accepted algorithms. Most JWT libraries default to rejecting none, but the allowlist approach is the correct defense-in-depth measure.
RSA/HMAC Key Confusion (RS256 vs HS256)
This is one of the most subtle JWT attacks. Consider a service that verifies RS256-signed tokens using a public key. A vulnerable library that trusts the token's alg header might accept a token that declares "alg": "HS256". HMAC (HS256) uses a symmetric secret — and the library might use the RSA public key (which is public, known to everyone) as the HMAC secret. The attacker signs a forged token using HMAC with the publicly available RSA public key, sets alg to HS256, and the vulnerable library validates it successfully.
Defense: Configure the verifier to accept only the specific algorithms you expect. If your issuer uses RS256, your verifier should reject tokens with any other alg value. The JWK's alg field provides an additional check: if the key declares "alg": "RS256", it should never be used for HMAC verification.
JWKS URL Injection
Some JWT headers include a jku (JWK Set URL) field that tells the verifier where to fetch the public keys. If a verifier fetches keys from the URL specified in the token itself, an attacker can:
- Generate their own key pair
- Host a JWKS containing their public key at
https://evil.example.com/.well-known/jwks.json - Forge a JWT signed with their private key, setting
jkuto point to their JWKS - The verifier fetches the attacker's JWKS, finds the matching key, verifies the signature (which is valid, because the attacker signed it), and accepts the forged token
Defense: Never fetch JWKS from a URL provided in the token. Always use a server-side configured JWKS URL. If jku support is needed, validate it against a strict allowlist of trusted URLs. Most production libraries and frameworks do not use jku at all.
Missing Audience Validation
If a verifier does not check the aud claim, a token issued for service-A can be replayed against service-B — as long as both services trust the same identity provider (which is common in microservice architectures). The token's signature is valid, the issuer is correct, but the token was not meant for this service.
Defense: Always validate the aud claim. Each service should have its own unique audience identifier and must reject tokens that do not include it.
No Expiration or Overly Long Expiration
Access tokens should have short lifetimes — typically 5 to 60 minutes. A token with a 24-hour or infinite expiration gives an attacker a long window to exploit a leaked token. Some implementations fail to check exp at all, treating tokens as valid indefinitely.
Defense: Always check exp. Configure a maximum acceptable token lifetime on the verifier. Use refresh tokens (which are not JWTs and are validated server-side) for long-lived sessions.
Accepting Untrusted Issuers
If a service derives the JWKS endpoint from the iss claim in the token without checking against a list of trusted issuers, an attacker can set iss to their own identity provider, sign the token with their own keys, and the verifier will happily validate it by fetching the attacker's JWKS.
Defense: Validate iss against an explicitly configured list of trusted issuers before fetching keys or verifying signatures.
JWKS and TLS: The Trust Chain
JWKS relies on TLS/HTTPS for transport security. When a verifier fetches the JWKS, it relies on the TLS certificate chain to authenticate the server. If the JWKS is fetched over plain HTTP, or if TLS certificate validation is disabled, an attacker performing a man-in-the-middle attack can substitute their own JWKS and sign tokens with their own keys.
This creates an interesting trust dependency: JWT verification trusts the JWKS endpoint, which trusts TLS certificates, which trust certificate authorities, which are trusted by the operating system's root certificate store. Each layer in this chain must hold for JWT validation to be meaningful — much like how RPKI relies on a chain of cryptographic trust from Regional Internet Registries down to individual route announcements.
Implementation Checklist
If you are implementing JWT validation with JWKS, here is what you need to get right:
- Configure a strict algorithm allowlist. If your issuer uses RS256, accept only RS256. Never accept
none. - Hard-code the JWKS URL or derive it from a configured issuer. Never use the token's
jkuheader. - Fetch JWKS over HTTPS only. Validate the TLS certificate.
- Cache the JWKS with a TTL. Re-fetch on unknown
kid, but rate-limit re-fetches. - Match the
kidfrom the token header to the JWKS. Reject tokens with unknownkidvalues (after one re-fetch attempt). - Verify the signature using the public key from the matched JWK.
- Validate
issagainst a trusted issuers list. - Validate
audagainst this service's expected audience. - Validate
exp,nbf, and optionallyiat. Allow a small clock skew (30-60 seconds). - Log validation failures with enough detail (issuer, kid, reason) to diagnose issues without logging the full token (which contains sensitive claims).
JWKS in Practice: A Complete Example
Here is how a full validation round-trip works in practice:
# 1. Application starts — discover JWKS URI
GET https://auth.example.com/.well-known/openid-configuration
Response: { "jwks_uri": "https://auth.example.com/.well-known/jwks.json", ... }
# 2. Fetch and cache JWKS
GET https://auth.example.com/.well-known/jwks.json
Response: { "keys": [{ "kid": "key-2024-q1", "kty": "RSA", "alg": "RS256", ... }] }
# 3. Request arrives with JWT
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0yMDI0LXExIn0.eyJpc3MiOi...
# 4. Decode header: { "alg": "RS256", "kid": "key-2024-q1" }
# Check: RS256 is in our allowlist? Yes.
# 5. Find kid "key-2024-q1" in cached JWKS? Yes.
# 6. Verify RSA-SHA256 signature with the public key. Valid.
# 7. Decode payload:
# { "iss": "https://auth.example.com", "aud": "api.myapp.com",
# "sub": "user|42", "exp": 1714203600, "iat": 1714200000 }
# 8. Check iss == "https://auth.example.com"? Yes.
# 9. Check "api.myapp.com" in aud? Yes.
# 10. Check current_time < exp? Yes (token not expired).
# 11. Token is valid. Extract sub = "user|42" for authorization.
JWKS Beyond JWT: Other Uses
While JWT validation is the most common use of JWKS, the JWK specification is used more broadly:
- JWE (JSON Web Encryption) — JWKS can publish encryption keys for encrypting tokens or payloads, where
"use": "enc"distinguishes them from signing keys. - DPoP (Demonstration of Proof-of-Possession) — a newer OAuth extension where clients prove possession of a key by including a JWK in a signed proof, binding access tokens to the client's key.
- FAPI (Financial-grade API) — high-security API profiles that require mutual TLS or DPoP alongside JWT validation, with stricter requirements on key sizes and algorithms.
- Verifiable Credentials — the W3C Verifiable Credentials standard uses JWKs and JWTs as one of its proof formats for digital identity credentials.
Further Reading
For more context on the protocols and infrastructure that JWKS integrates with:
- How JWT Works — the token format, claim semantics, and when to use JWTs
- How OAuth 2.0 Works — the authorization framework that issues JWTs
- How TLS/HTTPS Works — the transport security that JWKS depends on for safe key distribution