Well Known JSON Web Key Sets (JWKS)
Published: Apr 9, 2025
Last updated: Apr 9, 2025
When working across multiple services, being able to verify the source of a JSON Web Token (JWT) plays a vital role in proving token integrity.
In this blog post, we'll be working through the process of distributing JWTs and verifying them in another service by making use of a "well-known" public endpoint.
You can find the demo code examples on my GitHub.
Well-known endpoints
Well-known URIs are public endpoints prefixed with /.well-known/
.
Defined in RFC 8615, the purpose of well known endpoints is to add a consistent resolution to web applications that require the discovery of information about an origin (also known as "site-wide metadata").
Well-Known Endpoint | Purpose | Example URL | |
---|---|---|---|
openid-configuration | Provides OpenID Connect provider configuration details, including endpoints and supported capabilities. | https://example.com/.well-known/openid-configuration | |
jwks.json | Supplies the JSON Web Key Set (JWKS), containing public keys used to verify JSON Web Tokens (JWTs) issued by the authorization server. | https://example.com/.well-known/jwks.json | |
webfinger | Enables discovery of information about entities (e.g., users) identified by a URI, often used in federated systems. | https://example.com/.well-known/webfinger | |
oauth-authorization-server | Provides metadata about the OAuth 2.0 authorization server, detailing its capabilities and endpoints. | https://example.com/.well-known/oauth-authorization-server | |
security.txt | Specifies security policies and contact information for reporting vulnerabilities. | https://example.com/.well-known/security.txt | |
Well-known endpoints standardize the process of discovering essential service information and ease integration across various services.
As noted in the above table, we can use this supply the JSON Web Key Set that contains the public keys that help us with the JWT verification process.
Verifying JWTS without well-known endpoints
Before demonstrating an example of a well-known endpoint assisting with JWT verification, we can take a look at how verification works.
In order to do this, we need to generate a key-pair to use. For the sake of mixing things up compared to my previous posts, I'll generate this key pair using node-forge:
import forge from "node-forge"; import fs from "node:fs"; const { pki } = forge; // 1. Generate RSA keypair const keypair = pki.rsa.generateKeyPair(2048); // 2. Convert keys to PEM format const privateKeyPem = pki.privateKeyToPem(keypair.privateKey); const publicKeyPem = pki.publicKeyToPem(keypair.publicKey); // 3. Print or save them console.log("🔐 Private Key:\n", privateKeyPem); console.log("🔓 Public Key:\n", publicKeyPem); // Optional: save to files fs.writeFileSync("keys/private.pem", privateKeyPem); fs.writeFileSync("keys/public.pem", publicKeyPem);
Running this script will generate my key-pair for me in a keys
folder.
Once completed, we can use another script to generate a JSON Web Token (JWT) and then run verification across it.
Here is a simple script doing just that:
import jwt from "jsonwebtoken"; import fs from "node:fs"; const privateKey = fs.readFileSync("./keys/private.pem"); const issuer = "your-issuer"; const audience = "your-audience"; const algorithm = "RS256"; const token = jwt.sign({ sub: "user_123", role: "admin" }, privateKey, { algorithm, keyid: "test-key", expiresIn: "1h", issuer, audience, }); const publicKey = fs.readFileSync("./keys/public.pem"); try { const decoded = jwt.verify(token, publicKey, { algorithms: [algorithm], issuer, // optional, but recommended audience, // optional }); console.log("✅ Token is valid!"); console.log("Decoded payload:", decoded); } catch (err) { console.error("❌ Token verification failed:", (err as Error).message); }
If we run this script, we can validate that everything is verified as expected:
npx tsx ./simple-jwt-verification.ts ✅ Token is valid! Decoded payload: { sub: 'user_123', role: 'admin', iat: 1744184754, exp: 1744188354, aud: 'your-audience', iss: 'your-issuer' }
As you might expect, if you update any of the verification values to expect something different i.e. a different issuer or audience, then re-running the script will fail the validation.
Verification with a well-known JWKS endpoint
Now that we understand how verification can work for JWTs, let's use a local server with a well-known endpoint to perform the verification this time.
For our server file, we can add the following:
import { Hono } from "hono"; import { exportJWK, importSPKI } from "jose"; import fs from "node:fs"; import { serve } from "@hono/node-server"; const publicKey = fs.readFileSync("./keys/public.pem", "utf8"); // Convert PEM to KeyLike (SPKI = public key in PEM format) const key = await importSPKI(publicKey, "RS256"); // Convert the public key to JWK format const jwk = await exportJWK(key); jwk.kid = "test-key"; // must match JWT `kid` const app = new Hono(); // Serve JWKS app.get("/.well-known/jwks.json", (c) => { return c.json({ keys: [jwk] }); }); serve(app, (info) => { console.log(`Listening on http://localhost:${info.port}`); });
We import helper functions from the widely used jose package for converting our public key string into a format that can be exported as a JSON Web Key.
JOSE stands for JSON Object Signing and Encryption. SPKI stands for Subject Public Key Info.
The SPKI format is used for representing public keys, and it's this format that we need to pass as an argument for exportJWK
.
We start up a simple Hono web server and serve our JSON Web Key as part of an array of keys from the endpoint /.well-known/jwks.json
.
As for the verification script, we can follow a similar script to before with some helper functions to derive our public key used for the verification:
import jwt from "jsonwebtoken"; import axios from "axios"; import jwkToPem from "jwk-to-pem"; import fs from "node:fs"; const issuer = "http://localhost:3000"; // your issuer that serves the .well-known/jwks.json const audience = "your-audience"; const algorithm = "RS256"; const kid = "test-key"; const jwksUrl = `${issuer}/.well-known/jwks.json`; const privateKey = fs.readFileSync("./keys/private.pem"); const token = jwt.sign({ sub: "user_123", role: "admin" }, privateKey, { algorithm, keyid: kid, expiresIn: "1h", issuer, audience, }); async function getPublicKeyFromJwks(kid: string): Promise<string> { const { data } = await axios.get(jwksUrl); const jwk = data.keys.find((key: any) => key.kid === kid); if (!jwk) { throw new Error(`Key with kid "${kid}" not found in JWKS`); } return jwkToPem(jwk); } async function verifyToken(token: string) { const decodedHeader = jwt.decode(token, { complete: true })?.header; if (!decodedHeader || !decodedHeader.kid) { throw new Error("Missing or invalid token header"); } const publicKey = await getPublicKeyFromJwks(decodedHeader.kid); const payload = jwt.verify(token, publicKey, { algorithms: [algorithm], issuer, audience, }); return payload; } verifyToken(token) .then((decoded) => { console.log("✅ Token is valid!"); console.log("Decoded payload:", decoded); }) .catch((err) => { console.error("❌ Token verification failed:", (err as Error).message); });
In this script, we make use of the jwk-to-pem library to convert the JWK that we fetch into the PEM format. We also make use of an HTTP client Axios in order to make our GET request.
It's worth also calling out here that we could have used the jose
library as well. It has some helper functions for verifying JWTs and importing JWKs. I opted not to use it in order to keep our script that previously verified with the jsonwebtoken
library aligned to make it easier to see the differences.
Another potentially confusing point to clarify: I am creating the JWT within the script with the private key that we generated previously, but using the well-known endpoint to verify it with the public key information that we also generated earlier in this blog post.
If we run our updated script, it will give us our desired verification:
$ npx tsx server-jwt-verification.ts ✅ Token is valid! Decoded payload: { sub: 'user_123', role: 'admin', iat: 1744191716, exp: 1744195316, aud: 'your-audience', iss: 'http://localhost:3000' }
Great! We successfully verified our JSON Web Token using the well-known endpoint that shared our JSON Web Keys.
Conclusion
We explored the use of Well-Known JSON Web Key Sets (JWKS) for dynamically verifying JSON Web Tokens (JWTs) across distributed services.
This becomes useful in your own services if you have a centralized service delegating JWTs that you need extra verification of that are durable against key-rotations.
This was done using well-known endpoints, which are worth knowing as these endpoints have many use cases for distributing service metadata. You are likely to require doing this yourself with other 3rd parties at some stage.
Links and Further Reading
Photo credit: alisonstardust
Well Known JSON Web Key Sets (JWKS)
Introduction