Verify a JWT Signature Against a JWKS Without a Backend

Verify a JWT Signature Against a JWKS Without a Backend

Paste a token and a JWKS to check an RS256, ES256 or HS256 signature in your browser. How kid picks the key, measured errors and the claims to check.

26.10.2026
13 min read
Share this article:
JWTjwksoidcSecurityTutorial

A signature check needs the token and the right public key

Decoding a JWT shows what it claims. Verifying it shows whether the key you expect signed those claims. With an OpenID Connect provider such as Auth0, Keycloak or Cognito, that key sits in a JWKS, a JSON document of public keys. This guide explains how the verifier picks a key and how to read its answer when an integration fails. It uses the JWT verify tool, which takes the JWKS as pasted text and never fetches a URL, so no server is needed. On 26 October 2026 we ran 31 test cases through the verifier code and report what each returned. They cover RS256, ES256 and HS256 tokens, plus tampered, expired and mismatched ones. The rules come from RFC 7515, 7517, 7518, 7519 and 8725. To read a token before verifying it, start with our JWT encode and decode guide.

Decoding reads a token. Verifying checks its signature against a public key from the JWKS.
The kid header picks the key. It is a hint, and it is optional (RFC 7515, section 4.1.4).
A token without kid verified against a one-key JWKS and failed against a three-key JWKS.
An RS256 signature from a 2048-bit key is 256 bytes long, and an ES256 signature is 64 bytes.
A valid signature does not say the token is meant for your API. Check iss, aud and exp as well.
Paste the body of the JWKS, not its address. The tool fetches nothing.

How a JWKS works and how the key is chosen

A JWKS is a JSON list of public keys

RFC 7517 defines a JWK Set as an object with a keys array. The order of the keys carries no preference unless an application decides otherwise. Each key states its type in kty and carries the numbers needed to check a signature.

Improvements

RSA keys carry n and e (modulus and exponent). In our test, the n value of a 2048-bit key was 342 Base64URL characters.
EC keys carry crv, x and y. A P-256 key produces ES256 signatures.
kid, alg and use are optional labels. use says sig for signing or enc for encryption.
Our test set (two RSA keys and one EC key, each with kid, alg and use) was 997 characters, so a pasted JWKS stays small.
A key of type oct holds a shared secret in k. That is private material, even inside a document called a key set.
The kid chooses the key, and it is only a hint

RFC 7515 calls kid a hint about which key secured the token, and the header is optional. The verifier compares it with the kid of each JWK. What happens without a match depends on the tool, so we tested it.

Improvements

Token with kid k1 and a set of three keys that includes k1: verified, and the result named k1.
Token without kid and a set of three keys: no matching key, because the verifier cannot guess.
Token without kid and a set of one key, or a bare JWK: verified.
Token with kid a and a single key that has no kid: no matching key. A label on one side only is not enough.
Token with an unknown kid: no matching key. In production this usually means the provider rotated its keys.
Signature sizes and formats

The third segment of the token is the signature. Its length and encoding depend on the algorithm, which explains some failures that look like corruption.

Improvements

RS256 with a 2048-bit key produced a 256-byte signature, which is why the third segment of an RSA token is long.
ES256 produced 64 bytes. RFC 7518 defines it as R and S, 32 bytes each, joined end to end.
RFC 7518 requires 64 octets for ES256. A DER signature as OpenSSL prints it is therefore not a valid JWS signature until converted.
HS256 signatures are 32 bytes, and verifying one needs the same shared secret that signed it.
What a valid signature proves, and what it does not

A valid signature shows that the holder of the private key signed this header and this payload. It does not show that the token was meant for your service or that it is still current.

Improvements

We changed sub to admin in a valid token. The verdict was signature invalid.
RFC 8725, section 3.8, requires that the keys used belong to the issuer named in iss.
In our test an aud of other and an iss of https://evil.example both failed as claim errors, while the signature itself was fine.
A token expired by 30 seconds passed with a clock tolerance of 60 seconds and failed with a tolerance of 0.

Debugging an OIDC token without a server

Collect the token and the JWKS body

Copy the access or ID token from the failing request, without the Bearer prefix. Then get the JWKS from the provider. Its address is the jwks_uri value of the discovery document, served at the issuer address followed by /.well-known/openid-configuration.

Auth0 publishes the set at https://YOUR_DOMAIN/.well-known/jwks.json.
Cognito uses https://cognito-idp.REGION.amazonaws.com/USER_POOL_ID/.well-known/jwks.json and signs its tokens with RS256.
Keycloak uses /realms/REALM/protocol/openid-connect/certs. Recent versions no longer add the old /auth prefix.
Open the address in a browser or with curl, copy the JSON body and paste that body, not the address.
Read the issuer from the iss claim of the token and take the address from its discovery document instead of writing it into your code.
Pin what your server must check

The same decisions apply in the tool and in production code. Set them once, in your configuration, and never read them from the token.

Accept only the algorithms you expect, for example RS256. RFC 8725, section 3.1, says the algorithm list belongs to the application.
Compare iss with the exact issuer address.
Compare aud with the identifier of your API.
Allow a small clock tolerance. RFC 7519 says leeway should stay under a few minutes, and the tool offers 0, 60 or 300 seconds.
Refresh the cached JWKS when a token carries a kid you have not seen.

Reading the verifier answer

No matching key

The tool answers key_not_found when the token has no kid and the set holds several keys, or when its kid is absent from the set.

Decode the token in the decoder and compare the kid in its header with the kid values of the pasted set. Case counts.
With several keys and no kid in the token, the verifier cannot choose. Paste only the key you expect, or ask the issuer to set kid.
A kid you have never seen suggests that the provider rotated its keys. The Cognito documentation says to refresh the cached keys in that case.
A set copied before the rotation lacks the new key. Copy the body again from the provider.
Invalid signature

The tool found a key and the check failed. Several causes produce the same message.

A changed payload fails. Editing sub in a valid token gave signature invalid.
A token whose kid names key B but was signed by key A fails the same way. Look for two environments, such as staging and production, with different sets.
A private JWK gave signature invalid with a message that the key must be public. Paste the public part, and check the set in the inspector first.
A key of another type than the algorithm, for example an EC key with an RS256 token, also ends here.
The tool refuses the input

Some answers come before any signature check.

Invalid JWT structure: the token still has its Bearer prefix. In our test the tool did not strip it. A trailing newline did no harm.
Invalid JWK or JWKS JSON: an address pasted instead of the body, a truncated copy, or an empty keys array.
Unsupported algorithm for alg none: an unsigned token cannot be verified, and your server should reject it too.
Secret required: the token says HS256 and the set holds only public RSA keys. HMAC needs a shared secret or an oct key.

Four mistakes in JWKS verification

Treating a valid signature as permission

The signature proves who issued the token. It says nothing about what the holder may do on your service.

Accepting any token signed by the provider, whatever its audience.
Skipping the iss check because the JWKS already comes from the issuer.
Reading the green result as authorization.
A token issued for another API of the same provider passes the signature check and is accepted.
Verify the signature, iss, aud and exp together, then apply your own access rules.
Pasting private key material

A JWK with d, or an oct key with k, is a secret. A public key set never needs it.

Pasting a JWK that still contains d into a chat, a ticket or an online tool.
Copying a set from a secrets store instead of from the public endpoint.
Committing a test JWKS that still has its private fields.
Our test with a private RSA JWK failed verification only because the library wanted a public key. The same paste elsewhere could leak d.
Open the set in the JWKS inspector first. Its inventory view lists hasPrivate for each key, and the pretty view removes d, the RSA factors and oct k.
Letting the token choose the algorithm

The alg header belongs to the sender. If the verifier follows it, an attacker chooses the check.

Passing no algorithms option and trusting the alg header.
Using an RSA public key as the secret of an HS256 token.
Accepting alg none on a test route that reaches production.
The FastMinify verifier uses the token alg for a one-key paste. In our test a key labeled RS512 verified an RS256 token, and a key marked use enc verified it too. Production code should be stricter.
Fix the algorithm list in your configuration and use a library that matches alg, use and key_ops, as jose does.
Fetching the JWKS on every request, or never again

Both extremes fail. One adds a network call to every request, the other locks users out at the first rotation.

Calling the JWKS endpoint for each token.
Caching the set forever.
Writing one key into the code instead of using the set.
jose keeps keys for 10 minutes by default and refetches for an unknown kid only after a 30-second cooldown.
Use a library that caches and refreshes on an unknown kid, and set its limits on purpose.

Four claims about JWKS and signatures

The kid proves which key signed the token

The label is written by the sender, so it proves nothing by itself.

False. RFC 7515 calls kid a hint. In our test a token with kid k2 that key k1 had signed returned signature invalid.
A JWKS is a secret document

People hesitate to paste one into a tool.

A set of public keys is public by design and anyone can fetch it. The exception is an oct key, whose k value is a shared secret that you must never publish.
ES256 signatures are ASN.1 DER, like those of OpenSSL

This belief sends people into conversion code that is not needed in a JWT.

False. RFC 7518 requires a 64-octet value made of R and S. Our ES256 test token had a 64-byte signature.
Verifying a JWT requires a backend

The idea comes from libraries that fetch keys and cache them on a server.

No. A JWKS is public data and verification is a local computation. The verify page works on pasted text in your browser and does not fetch keys.

Verify and inspect in the browser

Verify a token against a JWKS

The JWT verify tool checks the signature and, if you ask, the audience, the issuer and the time claims. It supports 14 algorithms, among them RS256, ES256 and HS256.

1

Paste the token

Paste the compact JWT first. The key tabs follow the alg read from its header.

2

Paste the JWKS body

Paste the JSON of the key set, a single JWK or a public PEM. To turn a PEM key into a JWK first, use the PEM and JWK converter. For an HS256 token, use the HMAC secret tab.

3

Set the checks

Enter the audience and the issuer if you want them compared, and choose a clock tolerance of 0, 60 or 300 seconds. The Ignore expiration option still checks the signature.

4

Read the verdict

The result shows valid with the algorithm and the kid, or an error code described in the troubleshooting section above.

Check the key set first

The JWKS inspector reads a JWK or a JWKS without verifying anything. Use it before the verifier when a set comes from someone else.

1

Paste the JWKS

Paste a key set or a single key. Private fields are removed from the output and from the copy.

2

Switch to the Inventory view

The inventory lists kid, kty, use, alg, crv and hasPrivate for each key, without printing any key material.

3

Sort the keys

The Sort keys option orders the JSON so that two environments can be compared in a diff.

What the tools leave out

Both pages work on pasted text. A few limits are worth knowing.

No fetch of a JWKS address: you paste the body.
No encrypted (JWE) tokens: only signed ones with three parts.
The tool reads the algorithm from the token and does not compare it with the alg of a one-key paste.
Verified is not trusted: the tool does not know whether the issuer deserves your trust.
Each field is limited to 512 KB.

The same checks in a terminal and in your code

List the keys of a provider with curl and jq

Replace the address with your issuer. The last command reads the kid from the first segment of a token held in the TOKEN variable.

Basic example

# Read the jwks_uri of the discovery document curl -s https://auth.example.com/.well-known/openid-configuration | jq -r .jwks_uri # List the keys: kid, type, algorithm and use curl -s https://auth.example.com/.well-known/jwks.json | jq -c '.keys[] | {kid, kty, alg, use}' # Read the kid in the token header echo "$TOKEN" | jq -R 'split(".")[0] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson | .kid'
Verify with jose and a remote JWKS

createRemoteJWKSet downloads the set, caches it and picks the key by kid and alg. The algorithm, issuer and audience come from your configuration.

Basic example

import { createRemoteJWKSet, jwtVerify } from 'jose' const jwks = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json')) const { payload, protectedHeader } = await jwtVerify(token, jwks, { algorithms: ['RS256'], // fixed by you, never read from the token issuer: 'https://auth.example.com', audience: 'orders-api', clockTolerance: 60, // tolerance, as seconds }) console.log(protectedHeader.kid, payload.sub)
Verify against a pasted JWKS, as the tool does

createLocalJWKSet takes the set you already hold. In our tests it threw ERR_JWKS_NO_MATCHING_KEY for an unknown kid. It threw ERR_JWKS_MULTIPLE_MATCHING_KEYS for a token without kid and two RSA keys, and ERR_JOSE_ALG_NOT_ALLOWED when the algorithm list excluded the token alg.

Basic example

import { createLocalJWKSet, jwtVerify } from 'jose' // jwksText is the JSON body you copied from the provider const jwks = createLocalJWKSet(JSON.parse(jwksText)) try { const { payload, protectedHeader } = await jwtVerify(token, jwks, { algorithms: ['RS256'], issuer: 'https://auth.example.com', audience: 'orders-api', clockTolerance: 60, }) console.log(protectedHeader.kid, payload.sub) } catch (err) { // ERR_JWKS_NO_MATCHING_KEY, ERR_JWS_SIGNATURE_VERIFICATION_FAILED, ERR_JWT_CLAIM_VALIDATION_FAILED, ERR_JWT_EXPIRED console.error(err.code) }

Conclusion

Verifying against a JWKS comes down to three decisions. Pick the key by kid and alg. Check iss, aud and exp. When the key is missing, refresh the set instead of trusting the token. The browser tools let you reproduce each case with pasted text before you touch a server. Inspect the set, verify the token, then copy the same pins into your code.

Decode the token first and compare its kid with the set before you blame the signature.
Inspect any JWKS you did not generate, and check hasPrivate before pasting it anywhere.
Fix the algorithm, the issuer and the audience in your configuration.
Cache the JWKS, and refresh it when a token carries an unknown kid.
Treat a valid signature as the first check, not as authorization.
Share this article
Share this article: