A JWKS (JSON Web Key Set) is a JSON document, defined by RFC 7517, that publishes an identity provider’s public keys so services can verify JSON Web Token (JWT) signatures without manual key exchange. Keycloak serves one per realm at /realms/{realm}/protocol/openid-connect/certs, and the realm’s OpenID Connect discovery document lists it under the jwks_uri member that the Discovery specification requires of every provider.
When your API rejects tokens after a rotation, the first useful comparison is between the failing token’s key ID (kid), the live key set and your verifier’s cached set. We will make that comparison below, then walk through a rotation that keeps older tokens verifiable while the new key starts signing.
Key Takeaways
- A JWKS contains a
keysarray, andkidhelps you select a verification key. - Your backend must verify the signature, accepted algorithm, issuer, audience and timestamps.
- During a routine rotation, retain the old verification key until the tokens and cookies that depend on it have moved over.
JWK, JWKS, JWS, JWE and jwks_uri explained
These names describe different parts of the same process. The token carries claims about a request, while the key set supplies the public material needed to check its signature. The surrounding standards are commonly called JOSE, short for JSON Object Signing and Encryption.
| Term | Meaning | Source |
|---|---|---|
| JWK | A cryptographic key represented as JSON. | RFC 7517 |
| JWKS | An object with a keys array containing JWKs. |
RFC 7517 |
| JWS | A signed or message-authenticated payload. Its compact form has three dot-separated segments. | RFC 7515 |
| JWE | An encrypted payload. Its compact form has five dot-separated segments. | RFC 7516 |
| JWT | A claims object carried in a JWS or JWE. | RFC 7519 |
jwks_uri |
The HTTPS URL for the provider’s key set, supplied in discovery metadata. | OpenID Connect Discovery |
A signed JWT is not encrypted, so checking its signature does not hide its contents. Conversely, finding a matching kid does not prove that a token is valid because it is only a hint for selecting a key. Your verifier still needs to check the signature and claims before your API trusts anything in the payload.
What does a JWKS document look like?
The keys array contains individual key objects, and a set can include other top-level members too. This abbreviated illustration shows overlapping RSA signing keys. The modulus values are placeholders, so it is a format example rather than usable cryptographic material.
{
"keys": [
{
"kid": "signing-key-old",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "REPLACE_WITH_OLD_PUBLIC_MODULUS",
"e": "AQAB"
},
{
"kid": "signing-key-new",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "REPLACE_WITH_NEW_PUBLIC_MODULUS",
"e": "AQAB"
}
]
}
JWKS fields and how to use them
| Field | Meaning | Source |
|---|---|---|
kty |
Required key type, such as RSA or EC. |
RFC 7517, key type |
kid |
Key identifier used during selection and rollover. | RFC 7517, key ID |
use |
Intended use: sig for signatures, enc for encryption. |
RFC 7517, public key use |
key_ops |
Allowed operations, such as verify. |
RFC 7517, key operations |
alg |
Intended algorithm, such as RS256. |
RFC 7517, algorithm |
n, e |
Base64url-encoded RSA modulus and public exponent. | RFC 7518, RSA public keys |
crv, x, y |
Elliptic curve name and public point coordinates. | RFC 7518, EC public keys |
x5c |
Base64-encoded DER certificate chain. | RFC 7517, certificate chain |
x5t, x5t#S256 |
Certificate thumbprints using SHA-1 or SHA-256. | SHA-1 thumbprint, SHA-256 thumbprint |
The alg member describes the key’s intended use, but your application must independently decide which algorithms it accepts. Following JWT algorithm verification requirements, your verifier checks the header against that configuration rather than letting the token choose a different algorithm.
The general JWK format can represent private or symmetric keys. OpenID Connect Discovery requires the provider’s public set to exclude that material. In particular, d represents private key material for RSA and elliptic curve keys, and must not appear in the public set.
Finding the Keycloak JWKS endpoint
Start with the discovery document for the realm your API trusts, which Keycloak serves at /realms/{realm}/.well-known/openid-configuration. For an example realm named myrealm, you can inspect the advertised values like this:
curl --fail --silent --show-error \
'https://auth.example.com/realms/myrealm/.well-known/openid-configuration' \
| jq '{issuer, jwks_uri}'
Then fetch the advertised HTTPS URL. This example uses the standard certificate path and prints the fields useful during rotation:
curl --fail --silent --show-error \
'https://auth.example.com/realms/myrealm/protocol/openid-connect/certs' \
| jq '.keys[] | {kid, kty, alg, use}'
Read discovery from a configured, trusted issuer, never from an arbitrary URL supplied by an incoming token. OpenID Connect Discovery requires HTTPS for jwks_uri, and RFC 8725 warns against blindly following token-supplied key URLs. Otherwise an attacker can point your verifier at keys they control.
If you are following an older tutorial that includes /auth before /realms, check the context path in your deployment before trying its URL. Keycloak’s Quarkus migration guide documents why that prefix may differ, and we will cover the resulting 404 below. Our OpenID Connect guide for developers explains how discovery fits into the protocol.
Verifying JWTs safely with a remote JWKS
A maintained JWT library can resolve the public key and validate the token in the same call. The Node.js jose library’s createRemoteJWKSet handles retrieval and selection, while jwtVerify checks signatures and claims. Keeping one resolver for the process lets requests share its cache instead of fetching a new set for each token.
import { createRemoteJWKSet, jwtVerify } from "jose";
// Configure these from your trusted realm's discovery document.
const issuer = "https://auth.example.com/realms/myrealm";
const JWKS = createRemoteJWKSet(
new URL("https://auth.example.com/realms/myrealm/protocol/openid-connect/certs")
);
export async function verifyAccessToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
issuer,
audience: "my-api",
algorithms: ["RS256"],
requiredClaims: ["exp"],
});
return payload;
}
Replace the example audience with your API’s configured identifier and choose the signing algorithm your deployment intentionally accepts. The jose claim options require iss and aud when their expected values are configured, and requiredClaims makes expiration mandatory too. Your request handler must reject verification errors before running authorization checks on the returned claims.
For a complete backend integration, follow how to verify a Keycloak-issued access token. Our Keycloak token validation guide for APIs covers framework integrations. Signature verification alone also cannot tell you that a session was revoked after issuance; compare the available checks in token introspection vs local validation.
Cache refresh and unknown kid handling
The jose remote resolver fetches when its cache is missing or stale, and selects using alg and kid while respecting use and key_ops. If it cannot find a matching key, it can fetch again once the cooldown, the wait between successful fetches, has elapsed. Its documented defaults are a 10-minute cache age, 30-second cooldown and 5-second request timeout.
That cooldown matters during a rotation: an unknown key immediately after a successful fetch does not guarantee an immediate refetch. It also limits repeated requests caused by tokens with arbitrary key IDs. When diagnosing failures, inspect the library’s refresh behavior and network errors before reducing cache lifetimes or restarting every service.
If your verifier cannot obtain a usable key or validate the signature, your request handler needs to reject the token. During an outage, you can investigate connectivity while keeping that rejection behavior in place, because accepting unverified claims would bypass authentication. Our JWT best practices guide covers the surrounding validation and storage decisions.
Rotating Keycloak signing keys with overlap
Keycloak keeps a selected active key pair alongside passive ones: the active pair creates new signatures and the passive pairs can still verify earlier ones. The same realm keys overview explains that the pair used for signing comes from the first key provider, sorted by priority, that can supply an active key pair, and the generated key procedure adds that “changing the priority for a provider will not cause the keys to be re-generated”. That distinction matters when you rehearse a rotation: you need a new provider, rather than a priority edit on the existing one.
Use a development realm that already signs with RS256. The generated key procedure and passive key procedure give us the console steps:
- Obtain a test token through your application and record its header’s
kid, then confirm that your API accepts it. - Follow the generated key procedure: select the realm in the Admin Console, click Realm settings, open the Keys tab and then the Providers tab, click Add provider and select rsa-generated. Enter a Priority higher than the current RSA signing provider, because the procedure says “the highest number makes the key pair active”, then click Save.
- Open the old provider and follow the making keys passive procedure: toggle Active to Off and click Save, leaving Enabled on. The separate disabling keys procedure toggles Enabled to Off instead, which removes the key from verification and is not what you want during overlap.
- Fetch the JWKS and obtain a fresh token. Compare the token headers and run both tokens through your API while they are otherwise valid.
- Remove the old provider only after the retirement window described below.
You should see the new token’s kid change while both public keys remain in the set, so your verifier can select and cache the public key from the certificate endpoint using the token’s key ID, as Keycloak’s access token validation guidance describes. If the older token fails during the rehearsal, its error category tells you whether to check publication of the old key, cache refresh or token claims.

The diagram follows Keycloak’s active and passive key model: during overlap, the new key signs while the old public key remains available for verification. Retirement also needs the realm-wide token and cookie checks described below.
Choosing the overlap window
For local access-token verification, the earliest retirement time is the last issuance with the old key plus the longest applicable access-token lifetime plus the verifier’s allowed clock skew. This follows the JWT expiration rule, which lets a verifier allow a small tolerance around expiration. We measure from the final old-key issuance because a token issued just before the cutover still needs its full validity window.
For a worked example, suppose the last old-key token was issued at 12:00:00 UTC on October 7, 2026, your configured lifetime is 15 minutes, and every verifier allows at most 30 seconds of skew. These are example settings, not Keycloak defaults. Applying the expiration rule, the earliest access-token-only retirement deadline is 12:15:30 UTC that day. A longer client-specific lifetime or verifier tolerance moves the deadline later, so use the largest values your deployment accepts.
The whole realm needs a broader plan. Keycloak recommends creating new keys every three to six months and deleting the old ones one to two months after that, so that SSO cookies and tokens have time to move over. It adds that “rotating keys also applies to offline tokens”, so applications “need to refresh the tokens before the old keys are removed”, and that a user who “was inactive in the period between the new keys being added and the old keys being removed” will have to re-authenticate. The short access-token example above does not replace that realm-wide guidance. Our JWT token lifecycle guide connects expiration with refresh and revocation.
When the window has passed, test with a fresh verifier cache as well as your running API instances. A warm cache may still contain the removed public key, so it cannot show you what a newly started verifier will fetch. Confirm that current tokens work and that retired tokens are rejected before considering the rehearsal complete.
Rehearsing the rotation on Skycloak
On a Skycloak-managed cluster, you can use the same Keycloak procedure through Admin Console SSO. Our cluster documentation describes it this way: “Admin Console SSO lets you access the Keycloak Admin Console directly using your Skycloak account, with no separate admin password to manage.” Open the cluster in the dashboard and use Admin Console SSO to reach the Keycloak admin console, select your development realm and use its discovery document for the JWKS URL, then run the before-and-after token checks through your application. We run the managed Keycloak hosting, while your verifier configuration and chosen overlap window still need to match your application’s token policy.
Troubleshooting JWKS verification failures
Start with the error category before changing configuration, because a missing key and a rejected claim need different fixes. The examples below use jose errors where possible, so you can compare them with what your API logs.
Unknown kid after rotation
In jose, JWKSNoMatchingKey has code ERR_JWKS_NO_MATCHING_KEY. Its message reads as follows. It means no key met the selection requirements, which can involve algorithm and intended use as well as kid.
JWKSNoMatchingKey: no applicable key found in the JSON Web Key Set
For example, compare this decoded token header with a simplified live set. These are illustrative IDs and placeholder public moduli, not captured tokens or usable keys. The JWK set format lets the old and new public keys appear together:
{
"decodedTokenHeader": {
"alg": "RS256",
"kid": "signing-key-new",
"typ": "JWT"
},
"liveJWKS": {
"keys": [
{
"kid": "signing-key-old",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "REPLACE_WITH_OLD_PUBLIC_MODULUS",
"e": "AQAB"
},
{
"kid": "signing-key-new",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "REPLACE_WITH_NEW_PUBLIC_MODULUS",
"e": "AQAB"
}
]
}
}
Here the token’s signing-key-new is present in the live set. If your cached set contains only signing-key-old, investigate the resolver’s cooldown and network access before changing the realm. If the new ID is absent from both sets, check whether you fetched the correct realm and whether its provider is enabled. Decoding the header only helps you diagnose selection, so the token remains untrusted until verification succeeds.
Old tokens fail while new tokens work
This pattern is consistent with an old key being removed before its tokens expire, especially when failures start after a service restart or cache refresh. Compare the old token’s kid with the live set and inspect whether the old provider was disabled prematurely. The certificate endpoint returns the public keys enabled by the realm, so a key you deleted is no longer published for verifiers to fetch. Restoring a key suspected of compromise would also restore trust in forged tokens, so follow the compromised-key procedure if exposure is suspected.
If only some API instances fail, compare what each instance has cached and when it last fetched the set. That can explain why a service with the old public key still verifies a signature while another cannot resolve it.
The certificate endpoint returns 404
If the certificate endpoint returns 404, inspect the HTTP response body and compare the realm name and base path with your deployment’s discovery URL. A proxy or server not-found page can help you identify which component handled the request. The Quarkus migration guide states that “by default, the new Quarkus distribution removes /auth from the context-path” and shows --http-relative-path /auth as the build option that reintroduces it, so a path copied from an older tutorial needs checking against your actual deployment.
HS256 is accepted where RS256 is expected
RS256 and HS256 use different key material: RS256 uses an RSA pair and HS256 uses a shared HMAC secret. If your verifier lets the token choose between them, it can mistake an RSA public key for an HMAC secret. The attacker can then sign a token using public material, which is the algorithm confusion attack described in RFC 8725.
Your accepted algorithms belong in trusted application configuration, as in the algorithms option above, and a signature failure must stop the request. Our WSO2 JWT bypass analysis explains related failure handling and distinguishes that bypass from classic RS256-to-HS256 confusion.
A public key set contains d
The RSA and elliptic curve private-key definitions use d for private material. If you see it in a public set, treat the key as exposed and follow your incident procedure to replace it and remove the exposed material. Routine overlap is unsuitable for a compromised signing key because it would preserve acceptance of signatures made by whoever obtained it. For a compromised realm signing key, Keycloak’s advice is to generate a new key pair and then immediately remove the compromised one.
The signature passes but the token is not active or expired
The jose claim validator produces these messages for not-before and expiration failures:
JWTClaimValidationFailed: "nbf" claim timestamp check failed
JWTExpired: "exp" claim timestamp check failed
These errors point you toward token timestamps and the verifier’s clock rather than the signing key. Compare nbf (not before) and exp (expiration) with the current time, using the JWT time-claim rules. The iat claim records issuance time, and additional checks on it depend on your verifier’s policy, such as jose’s maximum token age.
Once you have checked time synchronization, compare any clock tolerance with your intended policy. Our clock skew guide and expired-token guide explain the next checks. Changing keys cannot fix timestamps already embedded in a token.
The signature passes but issuer or audience fails
The same claim validator reports issuer and audience mismatches with these messages:
JWTClaimValidationFailed: unexpected "iss" claim value
JWTClaimValidationFailed: unexpected "aud" claim value
Compare the configured issuer with discovery’s issuer, including its hostname and path, and compare your API’s expected audience with the token’s aud. These checks keep a correctly signed token from another issuer or intended recipient out of your API, as RFC 8725 asks in its sections on issuer validation and audience validation. Correcting the configuration or token issuance preserves that protection, whereas disabling the checks removes it.
Check your realm and rehearse the next rotation
Paste your realm’s jwks_uri and a development token into the Skycloak JWKS verifier to see which key matches the token’s kid and whether the signature verifies. Enforce the expected issuer and audience in your backend, as in the jose example above, because its configuration determines which requests your API accepts.
For your next rehearsal, keep a token from before the signing switch and obtain another after it. Verify both during the overlap, observe your resolver’s refresh behavior, and test retirement with a fresh cache. You can follow the same steps on a Skycloak-managed realm through the Admin Console, using Admin Console SSO. If you need a managed cluster for that rehearsal, you can book a demo.

