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:

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.

Header {"alg": "RS256", "typ": "JWT", "kid": "abc123"} . Payload (Claims) {"sub": "user42", "iss": "https://auth...", "exp": 1714200000} . Signature RSASSA-PKCS1-v1_5( base64(header) + "." + base64(payload), privkey) kid selects the key from JWKS

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:

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:

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:

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.

JWT Validation Flow 1. Decode JWT Header Extract alg and kid fields 2. Fetch JWKS from Issuer GET /.well-known/jwks.json (cached) 3. Find Key by kid Match JWT header kid to JWK kid 4. Verify Signature Cryptographically verify with public key 5. Check exp (Expiration) Reject if current time > exp 6. Check nbf / iat Reject if current time < nbf 7. Validate iss (Issuer) Must match expected issuer URL 8. Validate aud (Audience) Must contain this service's identifier TOKEN VALID REJECT: bad signature REJECT: token expired REJECT: wrong issuer REJECT: wrong audience REJECT: kid not found

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:

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:

JWKS Key Rotation Timeline T0 T1: Add new key T2: Switch signing T3: Remove old key Key A (kid: abc123) Signing + Verification Verify only (grace) Key B (kid: def456) Published Signing + Verification JWKS at T0: [Key A] JWKS at T1: [Key A] [Key B] JWKS at T2: [Key A] [Key B]* JWKS at T3: [Key B] * = active signing key
  1. T0 — Normal operation. The JWKS contains one key (Key A). All tokens are signed with Key A.
  2. 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.
  3. 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 exp window) can still be verified.
  4. 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:

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

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:

  1. Decodes the header and payload without verifying the signature
  2. Reads the iss claim from the payload
  3. Looks up the issuer in its trusted issuers list — if the issuer is not recognized, the token is rejected immediately
  4. Fetches (or uses cached) JWKS from the endpoint associated with that issuer
  5. 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:

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:

  1. Generate their own key pair
  2. Host a JWKS containing their public key at https://evil.example.com/.well-known/jwks.json
  3. Forge a JWT signed with their private key, setting jku to point to their JWKS
  4. 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:

  1. Configure a strict algorithm allowlist. If your issuer uses RS256, accept only RS256. Never accept none.
  2. Hard-code the JWKS URL or derive it from a configured issuer. Never use the token's jku header.
  3. Fetch JWKS over HTTPS only. Validate the TLS certificate.
  4. Cache the JWKS with a TTL. Re-fetch on unknown kid, but rate-limit re-fetches.
  5. Match the kid from the token header to the JWKS. Reject tokens with unknown kid values (after one re-fetch attempt).
  6. Verify the signature using the public key from the matched JWK.
  7. Validate iss against a trusted issuers list.
  8. Validate aud against this service's expected audience.
  9. Validate exp, nbf, and optionally iat. Allow a small clock skew (30-60 seconds).
  10. 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:

Further Reading

For more context on the protocols and infrastructure that JWKS integrates with:

See BGP routing data in real time

Open Looking Glass
More Articles
How OAuth 2.0 Works: Delegated Authorization Explained
How JWT Works: JSON Web Tokens Explained
What is BGP? The Internet's Routing Protocol Explained
What is an Autonomous System (AS)?
What is a BGP Looking Glass?
How to Look Up an IP Address's BGP Route