Validate OIDC Tokens with JWKS
Fetch the issuer JWKS, verify the signature, check iss, aud, exp and nbf, cache keys by kid, and refresh the key set on rotation.
Task
Verify an OpenID Connect ID token correctly instead of trusting decoded claims.
When to use this
A service decodes the JWT payload without verifying the signature, so any attacker can forge a token with an admin claim.
Prerequisites
- A JWT library
- The issuer discovery document or JWKS URL
Steps
1. Fetch signing keys from the issuer JWKS and cache them by kid.
Expected output: Signature verification uses the right public key.
2. Verify the signature and reject any token that fails.
Expected output: Forged tokens are rejected.
3. Validate iss, aud, exp, and nbf against the expected values.
Expected output: Tokens for other audiences or expired tokens are rejected.
4. Refresh the JWKS when a kid is unknown, and rate-limit refreshes.
curl -s https://issuer.example.com/.well-known/jwks.json | jq '.keys[].kid'Expected output: Key rotation is handled without a fetch per request.
Troubleshooting
Problem
Unknown kid causes fetch storms
Solution
Cache the JWKS with a TTL and a single-flight refresh so one rotation does not trigger many fetches.
Problem
Alg none or HS256 confusion
Solution
Pin allowed algorithms to the asymmetric ones you expect; never accept alg none.
Limitations
- JWKS validation proves the token is authentic; it does not by itself authorize the caller, which still needs scope checks.
- Clock skew between issuer and verifier can cause valid tokens to fail unless small leeway is allowed.
Context
Verify an OpenID Connect ID token correctly instead of trusting decoded claims. A service decodes the JWT payload without verifying the signature, so any attacker can forge a token with an admin claim.
Steps
- Fetch signing keys from the issuer JWKS and cache them by kid. Expected: Signature verification uses the right public key.
- Verify the signature and reject any token that fails. Expected: Forged tokens are rejected.
- Validate iss, aud, exp, and nbf against the expected values. Expected: Tokens for other audiences or expired tokens are rejected.
- Refresh the JWKS when a kid is unknown, and rate-limit refreshes. Expected: Key rotation is handled without a fetch per request.
const { payload } = await jwtVerify(token, getKey, {
issuer: 'https://issuer.example.com',
audience: 'my-api',
algorithms: ['RS256'],
});
Verify
- Signature verification uses the right public key.
- Forged tokens are rejected.
- Tokens for other audiences or expired tokens are rejected.
- Key rotation is handled without a fetch per request.
Limits
- JWKS validation proves the token is authentic; it does not by itself authorize the caller, which still needs scope checks.
- Clock skew between issuer and verifier can cause valid tokens to fail unless small leeway is allowed.