← Back to Blog Security

JWKS Explained (JSON Web Key Sets): A Complete Guide to OIDC Public Keys

If you are building modern applications with Single Sign-On (SSO) using Auth0, AWS Cognito, or Okta, you have certainly encountered the acronym JWKS. But what is a JSON Web Key Set? In this comprehensive guide, we will break down JWKS explained in simple terms. Need to create keys right now? Use our offline JWK Generator to instantly generate RSA and EC key pairs securely in your browser.

A JSON Web Key Set (JWKS) is a standardized format used to distribute public cryptographic keys. When an Identity Provider (IdP) signs a JSON Web Token (JWT) using an asymmetric algorithm (like RS256), it keeps the private key hidden and publishes the public keys at a well-known URL. Your backend application needs to fetch these public keys to verify the authenticity of the JWT.

By the end of this article, you will understand the anatomy of a JWK, how the /.well-known/jwks.json endpoint works, the difference between symmetric and asymmetric signing, and how to securely verify JWTs in your API using caching best practices. If you need to instantly create your own key pairs for testing, you can use our JWK Generator tool to generate RSA and EC keys completely in your browser, with zero data leaving your machine.

🔐 Quick Developer Tools for JWT & JWKS
  • JWK Generator — Instantly create secure RSA and EC JSON Web Keys (completely offline).
  • JWK to PEM Converter — Convert complex JWK JSON objects back into standard PEM certificate formats.
  • JWT Debugger — Inspect tokens without uploading sensitive claims to external servers.

1. What is a JWKS (JSON Web Key Set)?

To understand JWKS explained properly, we must first break down the acronym. JWKS stands for JSON Web Key Set. It is part of the JOSE (JSON Object Signing and Encryption) suite of standards, explicitly defined in RFC 7517.

In essence, a JWKS is a JSON object containing an array of cryptographic keys. These keys are formatted as JSON Web Keys (JWKs). While a JWKS can technically hold symmetric keys, its primary use case on the modern web is to expose a set of public keys associated with private keys held securely by an authorization server.

When an application receives a JWT (such as an access token or an ID token), it must verify the token's digital signature to ensure that the token was genuinely issued by the trusted authority and hasn't been tampered with. To verify an asymmetrically signed token, the receiving application needs the corresponding public key. The JWKS acts as a decentralized phonebook, providing applications with the exact public keys needed to validate incoming JWTs.

By formatting cryptographic keys as JSON, JWKS makes it trivial for modern programming languages and web frameworks to consume, parse, and utilize public keys via a simple HTTP GET request.

2. The Role of Identity Providers (Auth0, Cognito, Okta)

In a standard OAuth 2.0 or OpenID Connect (OIDC) architecture, you typically rely on an Identity Provider (IdP) to handle user authentication. Popular IdPs include Auth0, Amazon Cognito, Okta, and Azure Active Directory.

When a user successfully logs in, the IdP mints a JWT. Before sending this token back to the client application, the IdP signs it using its highly guarded private key. The token is then sent to the browser or mobile app, which subsequently passes it along to your backend API in the Authorization: Bearer header.

Your backend API is now faced with a problem: how does it know the token is valid?

Instead of calling the IdP on every single request to validate the token (which would create a massive bottleneck and a single point of failure), your backend relies on the IdP's JWKS endpoint. The IdP exposes the public key portion of its key pair publicly. Your backend downloads this JWKS, finds the correct public key, and cryptographically verifies the token locally. This architecture ensures high performance, minimal latency, and zero shared secrets.

3. The /.well-known/jwks.json Endpoint Standard

If you look at the OpenID Connect Discovery specification, you will notice a standard mechanism for authorization servers to publish their configuration metadata. This is typically located at the URL path /.well-known/openid-configuration.

Within this discovery document, there is a field called jwks_uri. This URI points directly to the JSON Web Key Set endpoint. The widely adopted convention is to host this file at /.well-known/jwks.json.

For example, if your Auth0 tenant is example-tenant.us.auth0.com, your JWKS URL will be:
https://example-tenant.us.auth0.com/.well-known/jwks.json

Similarly, for AWS Cognito, the URL follows this structure:
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json

Making a GET request to this endpoint requires no authentication. The public keys are intentionally public. Here is what a typical response looks like:

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "87c4b693-0210-4fa3-9cc1-db6715f333eb",
      "alg": "RS256",
      "n": "vX-1t...",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "another-key-id-2026",
      "alg": "RS256",
      "n": "mP_9q...",
      "e": "AQAB"
    }
  ]
}

The keys array can contain one or multiple keys. The ability to host multiple keys is what enables seamless key rotation—a concept we will explore later.

4. Symmetric vs. Asymmetric Signing: Why JWKS is Necessary

To truly appreciate JWKS, you must understand the difference between symmetric and asymmetric JWT signing algorithms.

HS256 (HMAC with SHA-256): This is a symmetric algorithm. It uses a single shared secret string to both sign and verify the token. If an Auth0 server issues a token using HS256, your backend API must possess the exact same secret string to verify it. This creates a severe security risk in microservices architectures: if one minor service is compromised and the secret is leaked, the attacker can mint completely valid JWTs for any user.

RS256 (RSA Signature with SHA-256): This is an asymmetric algorithm. It uses a key pair consisting of a private key and a public key. The IdP keeps the private key secure and uses it exclusively to sign tokens. Your backend services retrieve the public key (via the JWKS endpoint) and use it only to verify the tokens. Even if your backend is compromised and the public key is leaked, the attacker cannot mint new tokens.

If you want a deeper dive into token security, signature algorithms, and the dangers of algorithm confusion attacks, read our JWT Security Complete Guide.

JWKS was fundamentally designed to solve the distribution problem of asymmetric public keys. Without JWKS, you would be manually copying and pasting PEM certificates across all your microservices every time a key rotates.

5. Deep Dive into JWK Structure: Anatomy of a Key

When you inspect a JSON Web Key from a JWKS endpoint, you'll notice several properties. Let's break down the exact anatomy of an RSA JWK:

{
  "kty": "RSA",         // Key Type (Required)
  "use": "sig",         // Intended use (sig = signature, enc = encryption)
  "kid": "key-id-123",  // Key ID (Matches the kid in JWT header)
  "alg": "RS256",       // Algorithm
  "n": "base64url...",  // RSA Modulus
  "e": "AQAB"           // RSA Exponent
}

Here is what each parameter means:

  • kty (Key Type): Identifies the cryptographic algorithm family used with the key. For RSA keys, this is RSA. For Elliptic Curve keys, this would be EC. This field is strictly required.
  • use (Public Key Use): Indicates how the public key is meant to be used. The value is either sig (for signature verification) or enc (for encryption). IdPs setting up JWT signing will use sig.
  • kid (Key ID): A unique identifier for the key. When an IdP mints a JWT, it includes a kid claim in the header of the JWT. The backend looks at the JWT header, extracts the kid, and searches the JWKS array for a key with the matching kid.
  • alg (Algorithm): Identifies the specific cryptographic algorithm intended for use with the key. For OIDC tokens, this is commonly RS256.
  • n (Modulus) and e (Exponent): These parameters are specific to the RSA key type. They mathematically define the RSA public key. The values are base64url-encoded integers. If you need to convert these complex JSON fields back into a traditional PEM certificate string, you can use our JWK to PEM Converter.
  • x5c (X.509 Certificate Chain): Optional field containing the certificate chain. Some older enterprise systems rely on this rather than n and e.

By structuring keys this way, JWKS provides all the metadata necessary for a cryptographic library to reconstruct the public key mathematically and securely verify the incoming token.

6. Code Example: Fetching & Parsing JWKS in Node.js

In a Node.js backend using Express, verifying RS256 JWTs against a JWKS endpoint is highly streamlined using libraries like jsonwebtoken and jwks-rsa. The jwks-rsa library is specifically designed to fetch, cache, and rotate keys dynamically.

Here is a complete, production-ready example:

const express = require('express');
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const app = express();

// Configure the JWKS client with caching
const client = jwksClient({
  jwksUri: 'https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json',
  cache: true,
  cacheMaxEntries: 5, // Default is 5
  cacheMaxAge: 3600000 // Cache for 1 hour
});

// Function to get the signing key based on kid
function getKey(header, callback){
  client.getSigningKey(header.kid, function(err, key) {
    if (err) return callback(err);
    const signingKey = key.publicKey || key.rsaPublicKey;
    callback(null, signingKey);
  });
}

// Verification Middleware
app.get('/api/protected', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  
  jwt.verify(token, getKey, { algorithms: ['RS256'] }, (err, decoded) => {
    if (err) {
      return res.status(401).json({ error: 'Invalid Token' });
    }
    res.json({ message: 'Success', user: decoded });
  });
});

In this flow, the middleware automatically decodes the incoming JWT header, extracts the kid, and passes it to the getKey function. The jwks-rsa client first checks its local cache. If the key is not in the cache, it reaches out to the JWKS URI, downloads the keys, caches the matching public key, and passes it back to jsonwebtoken to complete the cryptographic validation.

7. Code Example: Verifying Tokens in Python / FastAPI

If you are building an API in Python using FastAPI, Django, or Flask, you can achieve the exact same flow using the standard PyJWT library.

Here is how you can implement JWKS fetching and token verification in Python, utilizing functools.lru_cache for optimal performance:

import jwt
import requests
from jwt.algorithms import RSAAlgorithm
from functools import lru_cache

JWKS_URL = "https://cognito-idp.us-east-1.amazonaws.com/USER_POOL_ID/.well-known/jwks.json"

@lru_cache(maxsize=1)
def get_jwks():
    response = requests.get(JWKS_URL)
    response.raise_for_status()
    return response.json()

def verify_token(token: str):
    # Get unverified header to extract 'kid'
    unverified_header = jwt.get_unverified_header(token)
    kid = unverified_header.get("kid")
    
    jwks = get_jwks()
    
    # Find the correct key
    public_key = None
    for key in jwks["keys"]:
        if key["kid"] == kid:
            public_key = RSAAlgorithm.from_jwk(key)
            break
            
    if not public_key:
        raise Exception("Public key not found in JWKS.")
        
    # Verify the token using the found public key
    decoded = jwt.decode(
        token,
        public_key,
        algorithms=["RS256"],
        audience="your_client_id"
    )
    return decoded

Notice how we use jwt.get_unverified_header(token) first. This is a critical step: we must peek into the unverified header to discover the kid so we know which public key to load, but we do not trust the payload until jwt.decode() successfully validates the signature using the correct RSA public key.

8. Key Rotation and Caching Best Practices

Security best practices dictate that cryptographic keys should not live forever. If a private key is compromised, changing the key limits the attacker's window of opportunity. This process is called Key Rotation.

Identity providers handle key rotation seamlessly using the JWKS endpoint. Here is the lifecycle of a key rotation:

  1. The IdP generates a new RSA key pair with a brand new kid.
  2. The IdP adds the new public key to the keys array at the /.well-known/jwks.json endpoint. The old key remains in the array.
  3. The IdP begins signing all new JWTs using the new private key.
  4. Your backend API receives a new token, sees a new kid in the header, realizes it's not in the local cache, and fetches the updated JWKS.
  5. Tokens signed by the old key continue to validate correctly because the old public key is still in the JWKS array (and in your cache).
  6. Once all tokens signed by the old key have naturally expired, the IdP removes the old key from the JWKS endpoint entirely.

Mandatory Caching: You must cache the JWKS response. Do not perform an HTTP GET request to the JWKS endpoint on every incoming API request. If you do, you will add 100ms+ of latency to every request, severely degrade your application's performance, and rapidly hit rate limits on your IdP (such as Auth0's strict rate limits on the well-known endpoint).

Use a cache with an appropriate Time-To-Live (TTL). A caching period of 1 hour (3600 seconds) to 24 hours is standard. Furthermore, your caching logic should automatically flush or aggressively fetch when it encounters a JWT with a kid that is not currently in the cache, as this is the primary signal that a key rotation has occurred.

9. Security Considerations & Common Pitfalls

While JWKS is inherently designed for security, implementation flaws in your backend can introduce massive vulnerabilities. Pay close attention to these common pitfalls:

  • Trusting the Issuer Unconditionally: Never dynamically determine the JWKS URL based solely on the iss (issuer) claim in an unverified token. An attacker can craft a JWT, set the issuer to an attacker-controlled server (e.g., https://hacker.com), and host their own JWKS file with their own public key. Your API must hardcode or firmly configure the trusted IdP domain.
  • The "None" Algorithm Attack: Ensure your JWT verification library strictly enforces the RS256 algorithm. If you fail to specify allowed algorithms, attackers might strip the signature and pass an alg: "none" header, which poorly configured libraries might accept as valid.
  • Algorithm Confusion: This occurs when a server expects RS256 but allows HS256, and uses the RSA public key string as the HMAC secret. Always explicitly pin algorithms: ['RS256'] in your backend logic.
  • Ignoring the Audience (aud): Verifying the signature proves the token was issued by the IdP, but you must also verify the aud claim. The token must be intended for your specific API. Otherwise, a token generated for a different application (within the same tenant) could be used maliciously against yours.

10. Debugging JWKS Issues

When JWKS validation fails, backend APIs typically return a generic 401 Unauthorized response. This makes debugging incredibly frustrating. If you are facing signature validation errors, run through this checklist:

1. Check the 'kid' mismatch: Decode your JWT header locally (you can use our offline JWT Debugger) and note the kid. Then, manually navigate to your IdP's JWKS endpoint in your browser. Does the kid in your token exist in the JSON array? If not, the token is obsolete, or you are pointing to the wrong IdP tenant/environment.

2. Check the Algorithm: Ensure your token is actually using an asymmetric algorithm like RS256. If your IdP is configured to issue HS256 tokens, JWKS verification will fundamentally fail. You must configure your Auth0 or Cognito application settings to use RS256.

3. Certificate Formatting Issues: Some older libraries cannot parse raw n and e parameters and strictly require PEM-formatted strings. If your library throws format errors, use a converter to map the JWK into a standard -----BEGIN PUBLIC KEY----- PEM string before passing it to the validation function.

4. Caching stale keys: If your IdP just performed a key rotation, but your backend cache is set to never expire, your backend will continually reject new tokens. Restart your backend server to clear the in-memory cache and force a fresh fetch from the JWKS URL.

11. Frequently Asked Questions (FAQs)

What does JWKS stand for?
JWKS stands for JSON Web Key Set. It is a set of cryptographic public keys represented in JSON format, used to verify the signatures of JSON Web Tokens (JWTs) issued by an Identity Provider (IdP).
How does the /.well-known/jwks.json endpoint work?
The /.well-known/jwks.json endpoint is a standard OpenID Connect (OIDC) discovery URL hosted by an Authorization Server (like Auth0 or AWS Cognito). It returns the public keys needed by clients and APIs to cryptographically verify the RS256 signatures of JWTs without needing to share a secret.
What is the kid claim in a JWT header?
The kid (Key ID) claim in the header of a JWT tells the receiving application which specific public key from the JWKS endpoint was used to sign the token. The application looks up the matching kid in the JWKS array to find the correct public key for verification.
Why use RS256 instead of HS256?
RS256 uses asymmetric cryptography (a public/private key pair), while HS256 uses symmetric cryptography (a single shared secret). RS256 is much safer for microservices or third-party integrations because you only share the public key via JWKS, whereas HS256 requires distributing the highly sensitive secret key to every verifying service.
What are the components of an RSA JWK?
An RSA JWK contains attributes like kty (Key Type, which is RSA), use (Public Key Use, usually sig for signature), kid (Key ID), alg (Algorithm, like RS256), n (Modulus), and e (Exponent). The 'n' and 'e' values mathematically construct the RSA public key.
How often should JWKS keys be rotated?
Key rotation frequency depends on security policies, but rolling keys every 30 to 90 days is common. Identity providers automate this process. When keys rotate, new keys are added to the JWKS endpoint, and old keys are retained briefly to allow existing unexpired tokens to still be validated.
Should I cache the JWKS response?
Yes, you must cache the JWKS response. Fetching the keys on every API request will cause massive latency and likely trigger rate limits on your Identity Provider. Caching keys with mechanisms like jwks-rsa for Node.js is considered a mandatory best practice.
Can I convert a JWK to a PEM file?
Yes. Since many backend libraries prefer standard PEM formats, you can extract the modulus (n) and exponent (e) from the JWK and mathematically reconstruct the X.509 public key in PEM format using a JWK to PEM converter tool.