JKU and JWK — Dynamic Key Resolution for JWT Verification
In this tutorial, you will learn about JKU and JWK. We cover key concepts, practical examples, and best practices to help you master this topic.
JKU (JWK Set URL) and JWK (JSON Web Key) are JWT header parameters that enable dynamic resolution of signing keys, supporting key rotation without reconfiguring verifiers.
What You'll Learn
How JKU and JWK headers work, JWK Set format, when to use dynamic key resolution, and the security risks (JKU header injection).
Why It Matters
Static signing keys require reconfiguration when rotated. JKU/JWK allow servers to publish their current keys at a well-known URL. Verifiers fetch the latest keys automatically. This is essential for multi-service architectures.
Real-World Use
Google's OAuth2 endpoints publish JWKs at https://www.googleapis.com/oauth2/v3/certs. Auth0, Firebase, and most OIDC providers use JWK Sets. Clients fetch the appropriate key using the kid header.
flowchart LR
A["Token with\nkid header"] --> B["Verifier"]
B -->|"Fetch JWKS"| C["https://auth.example.com/.well-known/jwks.json"]
C -->|"Return keys"| B
B -->|"Select key by kid"| D["Public Key"]
D -->|"Verify signature"| E["Valid or Invalid"]
style A fill:#dbeafe,stroke:#2563eb
style B fill:#fef3c7,stroke:#d97706
style C fill:#dcfce7,stroke:#16a34a
style E fill:#fef3c7,stroke:#d97706
JWK Set Format
A JWK Set is a JSON object with a keys array:
{
"keys": [
{
"kty": "RSA",
"kid": "key-id-1",
"n": "0vx7ago...",
"e": "AQAB",
"alg": "RS256",
"use": "sig"
},
{
"kty": "RSA",
"kid": "key-id-2",
"n": "sX7p...",
"e": "AQAB",
"alg": "RS256",
"use": "sig"
}
]
}
Code Example: JWK Verification with Python
import jwt
import requests
from jwt import PyJWKClient
# JWKS endpoint
jwks_url = "https://auth.example.com/.well-known/jwks.json"
# Create a JWK client (caches keys)
jwks_client = PyJWKClient(jwks_url)
token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS1pZC0xIn0..."
# Get the signing key from the JWKS using the kid in the header
signing_key = jwks_client.get_signing_key_from_jwt(token)
# Verify using the fetched key
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="https://api.example.com",
options={"verify_exp": True}
)
print(f"Verified: {payload['sub']}")
Code Example: Hosting a JWK Set
from flask import Flask, jsonify
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import base64, json
app = Flask(__name__)
# Generate an RSA key pair
private_key = rsa.generate_private_key(
public_exponent=65537, key_size=2048
)
public_key = private_key.public_key()
# Extract public key components
public_numbers = public_key.public_numbers()
n = base64.urlsafe_b64encode(
public_numbers.n.to_bytes(256, 'big')
).rstrip(b'=').decode()
e = base64.urlsafe_b64encode(
public_numbers.e.to_bytes(3, 'big')
).rstrip(b'=').decode()
@app.route("/.well-known/jwks.json")
def jwks():
return jsonify({
"keys": [{
"kty": "RSA",
"kid": "my-key-1",
"n": n,
"e": e,
"alg": "RS256",
"use": "sig"
}]
})
# Sign a token using the private key
def sign_jwt(payload):
header = {"kid": "my-key-1"}
return jwt.encode(
payload, private_key,
algorithm="RS256",
headers=header
)
JKU Header Injection Attack
The jku header tells the verifier where to fetch the JWK Set. An attacker can set jku to their own server hosting a key they control:
{
"alg": "RS256",
"jku": "https://attacker.com/keys.json",
"kid": "malicious-key"
}
If the verifier fetches keys from the attacker-controlled URL, it will accept tokens signed with the attacker's key.
Common Mistakes
1. Not Validating the JKU URL
Always validate that the JKU URL points to a trusted domain (e.g., https://auth.yourdomain.com/.well-known/jwks.json).
2. Not Using kid with Multiple Keys
When you have multiple keys (common during rotation), the kid header tells the verifier which key to use. Without it, the verifier must try all keys.
3. Fetching JWKS on Every Request
JWK Sets change infrequently. Cache the response (with reasonable TTL) to avoid unnecessary network calls.
4. Not Handling JWKS Fetch Failures
If the JWKS endpoint is down, new verifications fail. Cache previous keys and fall back to them if fetch fails.
5. Including Private Keys in JWKS
The JWKS endpoint must only contain public keys. Never expose private keys.
Practice Questions
- What does JKU stand for and what does it do?
- What is the difference between JWK and JWK Set?
- How does the
kidheader help with key rotation? - What is the JKU header injection attack?
- How should JWKS responses be cached?
Answers:
- JWK Set URL — a header parameter that tells the verifier where to fetch the signing keys.
- A JWK is a single key. A JWK Set (JWKS) is a JSON object containing an array of JWKs, typically served at a well-known URL.
- The
kidin the JWT header matches thekidin the JWKS, selecting the specific key used for signing. During rotation, both old and new keys are in the JWKS. - An attacker sets the
jkuheader to their own server hosting a malicious key. If the verifier trusts the JKU without validation, it accepts forged tokens. - Cache with a reasonable TTL (e.g., 1 hour). Cache-Control headers from the JWKS endpoint can guide this. Have a fallback to stale cache if fetch fails.
Challenge: Build a JWT verification system that fetches keys from a JWKS endpoint, caches them, handles key rotation (multiple keys), and validates the JKU URL against a whitelist.
FAQ
Mini Project
Create a Flask application that both issues JWTs (using RS256 with JWKS endpoint) and verifies JWTs (fetching keys from the JWKS). Include key rotation by publishing two keys simultaneously.
What's Next
Now learn about JWT Algorithm Confusion Attack — a critical vulnerability that can bypass JWT verification.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro