The developer’s guide to JWKS
A complete reference to JSON Web Key Sets: structure, algorithms, endpoints, and key rotation.
If you've ever implemented authentication using JWTs, you've probably seen references to JWKS endpoints scattered across OAuth 2.0 and OpenID Connect documentation.
But what exactly is a JWKS, how does it relate to JWTs, and why should you care about it beyond just copying a config value into your app?
This guide answers all of that. We'll cover the full picture: what JWKS is, how it differs from individual JWKs, how keys are structured for different cryptographic algorithms, what a JWKS endpoint does, how signing algorithms work, and when and why you'd use JWKS in production. At the end, we'll look at how WorkOS handles JWKS for you so you don't have to manage this complexity yourself.
Let’s dive right in.
What is a JWK?
Before defining a JWK, it helps to understand what problem it solves.
A quick primer on signing keys
When an authorization server issues a JWT, it needs a way to prove that the token is genuine, i.e., that it was actually issued by that server and hasn't been tampered with. It does this by digitally signing the token.
Digital signing works with a key pair: two mathematically related keys, a private key and a public key.
- The private key is kept secret on the server. It's used to produce the signature.
- The public key is shared openly. Anyone with it can verify that a signature was produced by the corresponding private key, but they can't use it to create new signatures.
This asymmetry is what makes distributed authentication practical. The authorization server signs tokens with its private key, and every other service in your system can verify those tokens independently using only the public key; no secrets to distribute, no need to call back to the authorization server.
The problem with raw key formats
Cryptographic public keys aren't natively JSON-friendly. Historically, they've been stored and transmitted in formats like:
- PEM: a base64-encoded text format you've probably seen, starting with
-----BEGIN PUBLIC KEY----- - DER: a binary encoding of the same data.
- PKCS#8, PKCS#12: container formats for keys and certificates.
These formats work, but they weren't designed for web APIs. They're hard to embed in JSON, don't carry metadata about how the key should be used, and have no standard way to identify a specific key when you have several.
Enter JWK
A JSON Web Key (JWK) is a standardized JSON representation of a cryptographic key. It encodes the key material itself alongside metadata (like what algorithm the key is for, whether it's meant for signing or encryption, and a unique identifier) all in a single, self-describing JSON object.
JWKs are defined in RFC 7517 and are part of the broader JOSE (JSON Object Signing and Encryption) family of standards, which also includes JWTs (RFC 7519), JWS (RFC 7515), and JWE (RFC 7516).
A minimal JWK for an RSA public key looks like this:
The large opaque string in n is the RSA public key's modulus (the actual key material) encoded in base64url format.
e is the public exponent.
Together, they define the public key.
The other fields (alg, use, kid) are metadata we'll cover in detail below.
The advantage over a PEM file is that a JWK is just JSON: easy to include in an API response, easy to parse in any language, and self-describing enough that a recipient knows exactly how to use it without out-of-band documentation.
What is a JWKS?
A JSON Web Key Set (JWKS) is a collection of JWKs grouped under a single keys array. Where a JWK represents one key, a JWKS represents a set of keys.
Why a collection, not just one key?
In an ideal world, an authorization server would have exactly one signing key, forever. In practice, several real-world situations require more than one key to be active at the same time.
- Key rotation. Signing keys should be rotated periodically, both as good security hygiene and in response to incidents. But you can't just swap the key instantaneously: tokens signed with the old key may still be valid and in active use (they haven't expired yet). During the rotation window, the server needs to keep the old public key available so those tokens can still be verified, while simultaneously publishing the new public key for freshly issued tokens. A JWKS holds both, and clients use the
kid(key ID) field to know which key to use for a given token. - Multiple algorithms. An authorization server may need to support clients that use different signing algorithms, e.g., RS256 for legacy integrations and ES256 for newer ones. Rather than running separate endpoints per algorithm, a single JWKS can contain keys of different types and let each client pick the one it needs.
- Separation of concerns. Some servers use different keys for different purposes: one for signing tokens, another for encrypting them. The
usefield on each JWK (sigvsenc) distinguishes them, and they can all coexist in the same JWKS.
The result is that a JWKS endpoint is the single source of truth for all the public key material a client might need, regardless of how many keys are currently active or why.
In this example, two keys coexist: an RSA key and an EC key. A verifier receiving a JWT looks at the token's kid header, finds the matching key in the JWKS, and uses that key, ignoring the others entirely.
The key distinction:
In practice, you'll almost always work with a JWKS rather than individual JWKs, because authorization servers expose a JWKS endpoint rather than individual key endpoints.
What is a JWKS endpoint?
A JWKS endpoint is a publicly accessible URL that serves a JWKS document over HTTPS. It's the mechanism by which an authorization server publishes its public keys so that any relying party can fetch and use them for JWT verification without needing to be pre-configured with each key.
Discovery
JWKS endpoints are typically discoverable via the OpenID Connect discovery document, served at:
That document contains a jwks_uri field pointing to the JWKS endpoint:
Response
A GET request to the JWKS endpoint returns the JWKS document with Content-Type: application/json. The response should only contain public key material.
Caching JWKS responses
Fetching the JWKS on every token verification request would be both slow and unnecessary. Best practices:
- Cache the JWKS according to the
Cache-Controlheaders returned by the endpoint. - Refresh on
kidmiss: If a JWT arrives with akidnot present in your cached JWKS, refetch the JWKS before rejecting the token (the authorization server may have rotated keys). - Cap the refresh rate: To prevent abuse (e.g., an attacker flooding your service with tokens signed by unknown keys), implement a minimum refresh interval (typically 5–10 minutes).
Most JWKS client libraries handle this caching behavior automatically.
JWK vs JWT: Understanding the relationship
These two are often confused, so let's be precise.
- A JWT (JSON Web Token) is a token: a compact, URL-safe string used to transmit claims between parties. It has a header, payload, and signature. The signature is what makes it trustworthy: it proves the token was issued by someone holding the private key.
- A JWK is a key representation: specifically the public key that can be used to verify that signature.
The relationship flows like this:
- An authorization server holds a private key (kept secret).
- It uses that private key to sign JWTs it issues.
- It publishes the corresponding public key as a JWKS (a collection of JWK) at a well-known endpoint.
- Resource servers (your API, your app) fetch that public key and use it to verify incoming JWTs.
The private key never leaves the authorization server.
The public key in the JWKS is what makes distributed, stateless JWT verification possible. Your service can verify a token without calling back to the authorization server on every request.
JWK structure
Every JWK contains a set of members (fields). Some are universal; others are algorithm-specific.
The universal parameters are:
The kid parameter deserves special attention. When a JWT is issued, its header typically includes a kid claim:
When verifying the token, your library looks up the matching key from the JWKS by kid, rather than trying every key in the set. This makes key rotation efficient: you can publish multiple keys simultaneously and clients can always find the right one.
kty: JWK key types
The kty (key type) parameter is the most fundamental field in a JWK. It defines the cryptographic family the key belongs to, which in turn determines what other fields the JWK contains and what mathematical operations are used for signing and verification.
Choosing a key type isn't just a formatting decision. Each family represents a different underlying cryptographic algorithm with different tradeoffs around security, performance, key size, and ecosystem support. In most cases, your choice will be shaped by what your authorization server supports and what your clients can verify, but understanding the differences helps you make an informed decision and evaluate the security posture of systems you integrate with.
There are four key types defined in the JOSE standards:
- RSA: The most widely supported, based on the difficulty of factoring large integers. Larger keys, larger signatures, but near-universal library support.
- EC (Elliptic Curve): Based on elliptic curve discrete logarithm problems. Smaller keys and signatures than RSA at equivalent security levels, with strong library support across modern stacks.
- OKP (Octet Key Pair): Uses modern curves like Ed25519, which were designed from scratch with security and performance in mind. Excellent properties but less universally supported.
- oct (Octet Sequence). Symmetric keys for HMAC-based algorithms. Simple but unsuitable for distributed systems where the key would need to be shared with every verifier.
The kty parameter determines which additional fields are present in the JWK. Let's look at each.
RSA keys (kty: "RSA")
RSA keys are the most common in production JWKS deployments, largely due to their long track record and broad library support.
Public key parameters:
Private key parameters (additional):
!!Important: A JWKS endpoint for token verification should only ever expose public key parameters (n and e). Never publish a JWK containing d or other private parameters.!!
Example RSA public key JWK:
RSA keys should be at least 2048 bits in length (the n modulus). 4096-bit keys provide a larger security margin at the cost of performance.
Elliptic Curve keys (kty: "EC")
EC keys are smaller and faster than RSA for equivalent security levels, making them increasingly popular — especially in latency-sensitive environments.
Parameters:
Example EC public key JWK:
Curve comparison:
P-256 is the most widely deployed and is a solid default for new systems.
Octet Key Pair keys (kty: "OKP")
OKP keys use modern elliptic curves defined outside the NIST family, specifically Ed25519 and X25519 (from the crv parameter). These are used with the EdDSA algorithm.
EdDSA with Ed25519 is fast, has small key sizes, and has a clean security proof, but library support is less universal than RSA or P-256 EC. Check your ecosystem before adopting it.
Symmetric Keys (kty: "oct")
Symmetric (octet sequence) keys are used with HMAC-based algorithms like HS256, HS384, and HS512. They have a single parameter:
!!Critical: Symmetric keys should never appear in a public JWKS endpoint. If you're using HMAC-signed JWTs, the key must remain secret on both the issuer and verifier sides. Symmetric keys in a JWKS are only appropriate for private, internal distribution.!!
alg: Signing algorithms
The alg field in a JWK (and a JWT header) specifies the signing algorithm. It ties the key to a specific cryptographic operation, so there's no ambiguity about how the key should be used.
It's worth understanding what a signing algorithm actually does before comparing them. At a high level, signing works in two steps:
- The token payload is passed through a hash function (like SHA-256), which produces a short, fixed-length digest of the content.
- That digest is then processed with the private key using the algorithm's signing operation, producing the signature.
Verification reverses this: the verifier recomputes the digest from the token payload, then uses the public key to confirm that the signature matches. If even a single byte of the payload was changed after signing, the digest won't match and verification fails.
The signing algorithm therefore has two moving parts:
- the cryptographic scheme (RSA, ECDSA, EdDSA, HMAC) and
- the hash function (SHA-256, SHA-384, SHA-512).
The alg identifier encodes both: RS256 means RSA + SHA-256, ES384 means ECDSA + SHA-384, and so on.
RSA-based algorithms (RSASSA-PKCS1-v1_5)
RSA signing works by raising the hash digest to the power of the private exponent modulo a large number; an operation that's easy to perform with the private key, easy to verify with the public key, but computationally infeasible to reverse without it.
RSASSA-PKCS1-v1_5 (the "RS" family) is deterministic, i.e., signing the same payload twice with the same key always produces the same signature. It's the most mature and widely implemented RSA signing scheme, which is why RS256 remains the most common algorithm you'll encounter in the wild. The tradeoff is that PKCS1-v1_5 has a less tight formal security proof than its successor, RSA-PSS.
On hash choice: the difference between RS256, RS384, and RS512 is only the hash function applied to the payload before signing. SHA-256 is already considered cryptographically strong for this purpose. Moving to SHA-384 or SHA-512 provides a marginally larger security margin with no meaningful real-world benefit. RS256 is the right default unless a compliance requirement mandates a stronger hash.
RSA-PSS algorithms
RSA-PSS (Probabilistic Signature Scheme) is the modern replacement for PKCS1-v1_5. The key difference is that PSS incorporates a random salt into the signing operation, making it non-deterministic, i.e., the same payload signed twice will produce different signatures, both equally valid. This isn't a bug; it's a deliberate design choice that contributes to a tighter formal security proof.
In practical terms, PSS is harder to attack with certain theoretical techniques that can be applied to PKCS1-v1_5. RFC 7518 explicitly recommends PS256 over RS256 for new applications, and cryptographers generally agree with this guidance. The catch is library support: while most modern JWT libraries handle PSS correctly, older or more minimal implementations may only support the RS family. Verify your full stack before committing to PS256 in a new system, but default to it when you can.
EC-based algorithms (ECDSA)
ECDSA takes a fundamentally different mathematical approach than RSA. Rather than operating on large integers, it performs arithmetic on points along an elliptic curve, a structure that's much harder to attack at shorter key lengths. A 256-bit EC key provides roughly the same security as a 3072-bit RSA key, which translates directly into smaller key material, smaller signatures, and faster operations.
For JWTs specifically, smaller signatures mean smaller tokens. An RS256-signed JWT header+signature typically runs around 342 bytes of base64url; an ES256 signature is around 86 bytes. At high request volumes or in constrained environments (mobile, edge), this adds up.
One important behavior to be aware of: ECDSA signing is non-deterministic by default. The algorithm incorporates a random nonce (k value) into each signature, so signing the same payload twice produces two different but equally valid signatures. This is expected and doesn't affect verification correctness, but it does mean you can't use signature equality as a cache key or deduplication mechanism. If determinism matters, look at Ed25519 (via EdDSA), which achieves determinism through a different construction.
EdDSA
EdDSA with Ed25519 is arguably the best-designed signing algorithm available today. It addresses several subtle weaknesses found in older schemes:
- Deterministic signing. Unlike ECDSA, EdDSA doesn't rely on a random nonce during signing. It derives a deterministic nonce from the private key and message using a hash function. This eliminates an entire class of vulnerability: weak or repeated nonces in ECDSA have historically led to private key extraction in high-profile incidents.
- Resistance to side-channel attacks. Ed25519 was designed with constant-time implementations in mind, making it easier to implement correctly without leaking timing information.
- Clean security proof. The security of Ed25519 is based on well-understood hardness assumptions with a tight reduction.
- Fast. Ed25519 signing and verification are significantly faster than RSA or NIST EC operations at equivalent security levels.
The main barrier to adoption is library support. While Ed25519 is widely available in modern crypto libraries (OpenSSL, libsodium, Go's standard library, Node.js crypto), some JWT-specific libraries and older platforms don't yet support it. Always verify your full stack (authorization server, JWT library, and any middleware) before adopting EdDSA. It's the right long-term direction, but RS256 or ES256 remain safer choices in environments where compatibility is uncertain.
HMAC algorithms
HMAC works differently from all the asymmetric algorithms above. Instead of a key pair, it uses a single shared secret key for both signing and verification. Signing produces an HMAC tag by running the payload and the key together through a hash function; verification recomputes the tag and compares.
This simplicity is HMAC's only real advantage for JWT use cases. The fundamental problem is key distribution: every service that needs to verify a token must possess the same secret key. In a monolithic application with a single verifier, this is manageable. In any distributed system (multiple microservices, third-party integrations, mobile clients) it quickly becomes a liability. Sharing a secret with many parties means the secret is only as secure as its most exposed holder, and rotating it requires coordinated updates across every service simultaneously.
There is also no meaningful difference in token security between HS256, HS384, and HS512 in practice. The bottleneck is almost always key management, not hash strength. If you're using HMAC today, the right migration path is to move to RS256 or ES256, not to upgrade from HS256 to HS512.
Algorithm comparison summary
Why use JWKS?
1. Stateless, distributed token verification
Without JWKS, verifying a JWT typically requires calling back to the authorization server to validate the token. With JWKS, you fetch the public key once and verify tokens locally, at any scale, with no network round-trip per request.
2. Key rotation without downtime
Cryptographic keys should be rotated regularly. JWKS makes this seamless: the authorization server publishes both the old and new key in the JWKS during a transition period. Verifiers cache both keys and can verify tokens signed by either. Once old tokens have expired, the old key can be removed.
Without JWKS (for example, if you had hard-coded a public key) key rotation would require a coordinated deployment of every service that verifies tokens.
3. Multi-tenant and multi-algorithm support
A single JWKS can contain keys of different types (RSA, EC) and for different purposes (signing vs. encryption). This lets an authorization server support multiple client configurations from one endpoint.
4. Standardization and interoperability
JWKS is a well-defined open standard (RFC 7517). Every major identity provider (WorkOS, Google, Microsoft) exposes a JWKS endpoint. Any JWT library worth using has built-in support for fetching and using JWKS. This interoperability means you can swap identity providers without rewriting your token verification logic.
5. Zero secret distribution
Because JWKS only exposes public keys, there's nothing sensitive to protect at the endpoint. You can cache it in a CDN, embed it in client-side code, and expose it publicly without any security risk. The private key never leaves the authorization server.
When to use JWKS
JWKS is the right approach in the following scenarios:
- You're implementing an OAuth 2.0 / OpenID Connect flow. JWKS is part of the OIDC specification. If you're using access tokens or ID tokens from an OIDC-compliant provider, JWKS is how you verify them.
- You have multiple services verifying the same tokens. Distributing a symmetric secret to every service is operationally painful and a security liability. JWKS lets every service independently verify tokens using only the public key.
- You need to support key rotation. If you want to rotate signing keys without breaking existing tokens mid-flight, JWKS with kid-based key selection is the standard solution.
- You're building an authorization server or identity provider. If you're issuing JWTs yourself, exposing a JWKS endpoint is the standard way to allow downstream services to verify them.
- You're integrating with a third-party identity provider. Almost every major IdP exposes a JWKS endpoint. Your integration should fetch and cache the JWKS rather than hard-coding public keys.
How to verify a JWT against a JWKS
Node.js example
Using jose, a well-maintained JOSE/JWT library, to verify a WorkOS session token:
createRemoteJWKSet handles caching and cache invalidation automatically.
Python example
Using python-jose or PyJWT with the cryptography backend, to verify a WorkOS session token:
Signing keys rotation
Key rotation is one of the core reasons JWKS exists as a format. Because a JWKS can hold multiple keys simultaneously, you can introduce a new signing key and retire the old one without any hard cutover that would invalidate tokens already in the wild. Done correctly, rotation is invisible to users.
Choosing a kid convention
Before you rotate anything, it's worth thinking about how you name your keys. The kid value is arbitrary (any unique string works) but a consistent naming convention makes debugging significantly easier. Two common approaches:
- Timestamp-based:
key-2024-01-15orkey-1705276800. Makes it immediately obvious when a key was generated, which is useful when diagnosing token verification failures across log files. - Random identifiers: A UUID or random hex string. Avoids leaking key generation timing but harder to reason about at a glance.
Whichever you choose, make sure kid values are unique across your entire key history, not just currently active keys. Reusing a kid for a new key will cause clients with a cached JWKS to attempt verification with the wrong key material.
Sizing the overlap window
The overlap window (the period where both old and new keys are present in the JWKS) needs to be long enough that no valid token signed by the old key is still in circulation when you remove it. The minimum safe window is your maximum token TTL, but in practice you should account for:
- Client-side JWKS caching. If your clients cache the JWKS with a long
max-age, they may not pick up the new key immediately. Your overlap window needs to exceed both the token TTL and the maximum cache duration you allow. - Clock skew. Tokens validated close to their expiry may succeed on one node and fail on another if clocks diverge. A small buffer (5–10 minutes) on top of your TTL is prudent.
- Token caching in your application. If your API caches decoded token payloads rather than re-verifying on every request, tokens may remain active in your system longer than their nominal TTL.
A common safe default is: overlap window = token TTL + JWKS cache TTL + 10 minutes.
Scheduled rotation: step by step
- Generate the new key pair. Store the private key securely (HSM, KMS, or encrypted secrets store). Never in source control or application config files.
- Pre-publish the new public key. Add the new key to your JWKS before you start signing with it. This gives all clients time to fetch and cache the new key before they start receiving tokens that use it. Wait at least one full JWKS cache TTL before proceeding. Do not remove the old key yet.
- Switch signing to the new private key. Update your authorization server to sign new tokens with the new private key, with the
kidheader pointing to the new key's identifier. Old tokens remain valid; they still have theirkidpointing to the old key, which is still present in the JWKS. - Wait out the overlap window. Let all tokens signed by the old key expire naturally. Monitor your logs for any verification failures that reference the old
kid; these indicate tokens still in circulation. - Verify the new key is working correctly. Before removing the old key, confirm that new tokens are being issued with the correct
kid, and that verification is succeeding end-to-end across all your services. - Remove the old public key from the JWKS. Once you're confident that no live tokens reference the old
kid, remove it. Any token that still carries the oldkidwill now fail verification, which is intentional, since they should have expired. - Retire the old private key. Delete or archive it from your secrets store. If you're using a KMS, disable the key version rather than deleting it immediately, in case you need to audit past signatures.
Emergency rotation: key compromise
If a private key is compromised or suspected to be compromised, the scheduled rotation process is too slow; you can't wait for tokens to expire naturally. Emergency rotation involves a harder tradeoff between security and user disruption.
Your options, in order of severity:
- Immediate key removal with forced re-authentication. Remove the compromised key from the JWKS immediately. All tokens signed by it will become unverifiable at once, forcing every active user to re-authenticate. This is disruptive but closes the window immediately.
- Short-TTL bridge tokens. If your infrastructure supports it, issue a round of very short-lived tokens (minutes, not hours) signed by a new key before pulling the old one. Users get a brief grace period to transparently receive new tokens before the old key is removed.
- Token revocation list. If you maintain a token revocation list or blocklist, you can invalidate tokens by
jti(JWT ID) orsub(subject) without touching the JWKS at all, but this reintroduces the statefulness that JWKS was meant to eliminate, and only works if your verifiers check the revocation list.
In any compromise scenario, rotate immediately and treat the investigation as a separate track from the recovery.
Security considerations
- Always validate claims, not just signatures. A valid signature means the token was issued by the expected party, it doesn't mean the token is meant for you. Always validate
iss(issuer),aud(audience),exp(expiration), andnbf(not before) claims. For more, see JWT validation: how-to and best libraries to use. - Don't accept
alg: none. The JWT spec allows an unsigned token withalg: none. Any library or implementation that accepts this is critically vulnerable. Explicitly specify which algorithms you'll accept. - Don't trust the
algheader blindly. An attacker can forge a JWT header that specifies a weak or unexpected algorithm. Always configure your verifier with an explicit allowlist of accepted algorithms. - Use asymmetric algorithms for distributed verification. As discussed, symmetric HMAC algorithms require sharing the secret key with every verifier. For anything beyond a single-service deployment, use RSA or EC keys.
- Validate the JWKS endpoint over HTTPS. Never fetch a JWKS over plain HTTP. The public key material itself isn't secret, but a MITM attacker substituting a different JWKS could lead to forged token acceptance.
- Rate-limit JWKS refresh. If you refetch the JWKS every time you see an unknown
kid, an attacker can flood your service with forged tokens and cause repeated JWKS fetches. Implement a minimum cooldown between refreshes.
Managing JWKS with WorkOS
Managing JWKS infrastructure yourself means handling key generation, secure storage, rotation schedules, endpoint hosting, caching logic, and monitoring, none of which is your core product.
WorkOS handles all of this as part of its authentication platform. What you get:
- Automatic key management. WorkOS generates and manages RSA signing keys on your behalf. Key generation, storage, and rotation happen without any action required from you.
- A hosted JWKS endpoint. Every WorkOS environment exposes a JWKS endpoint at
https://api.workos.com/sso/jwks/{client_id}. This endpoint returns the current public keys used to sign WorkOS-issued JWTs (session tokens), so your backend services can verify them locally without calling back to WorkOS on every request. - Automatic key rotation. WorkOS rotates signing keys on a regular schedule. During rotation, both the outgoing and incoming keys are present in the JWKS simultaneously, so in-flight tokens continue to validate without interruption.
- Standard-compliant tokens. WorkOS-issued tokens are standard JWTs with a
kidheader that matches a key in the JWKS. Any JWT library with JWKS support will work out of the box.
Offloading JWKS management to WorkOS means:
- No key material to store or protect in your own infrastructure.
- No rotation logic to implement or schedule.
- No JWKS endpoint to host, monitor, or scale.
- Consistent, standards-compliant behavior across all your environments.
You get the full benefits of asymmetric JWT verification (distributed, stateless, scalable) without the operational overhead of running your own PKI.
Summary
JWKS sits at the foundation of modern token-based authentication. Here's the short version:
- A JWK is a JSON representation of a cryptographic key. A JWKS is a set of JWKs, wrapped in a
keysarray. - JWKs are the public keys used to verify JWTs. The issuer signs with a private key; verifiers check with the corresponding public key from the JWKS.
- JWKs carry metadata:
kty(key type),kid(key ID),use(purpose),alg(algorithm), and algorithm-specific parameters. - The main key types are RSA (
n,e), EC (crv,x,y), OKP (crv,x), and oct (symmetric; don't publish publicly). - Common signing algorithms include
RS256,PS256,ES256, andEdDSA. For new systems, preferPS256orES256overRS256. - A JWKS endpoint is a public HTTPS URL that serves the JWKS. Clients cache it and use the
kidto look up the right key per token. - JWKS enables stateless, distributed JWT verification, seamless key rotation, and standards-based interoperability.
- WorkOS manages JWKS infrastructure for you (key generation, rotation, and endpoint hosting) so you can focus on building your product.