Skip to content
ecdsa.com

Verify a JWT with JWKS · Python

How to verify a JWT with a JWKS in Python

To verify a JWT against a JWKS in Python, PyJWT's PyJWKClient fetches the issuer's key set and returns the key named by the token's kid, and jwt.decode checks the ES256 signature, iss, aud and exp. The run serves a real JWKS over HTTPS inside the container and checks five tokens: one valid and four rejected, each for its own reason.

Tested with PyJWT 2.15.1 (Python 3.14.8, cryptography 50.0.2).

verify.py — PyJWKClient for the key, jwt.decode for everything else
import jwt # PyJWT: pip install "pyjwt[crypto]==2.15.1"
# Fetches the key set over HTTPS, caches it, and fetches again when a kid is unknown
jwks_client = jwt.PyJWKClient("https://issuer.example.com/.well-known/jwks.json")
def verify_access_token(token: str) -> dict:
signing_key = jwks_client.get_signing_key_from_jwt(token) # selected by the token's kid
return jwt.decode(
token,
signing_key.key,
algorithms=["ES256"], # never take the algorithm from the token
issuer="https://issuer.example.com",
audience="api://orders",
options={"require": ["exp", "iss", "aud"]},
)
check every token in tokens.txt
with open("tokens.txt") as f:
for line in f:
label, token = line.split()
try:
claims = verify_access_token(token)
print(f"{label}: accepted, sub={claims['sub']}")
except jwt.PyJWTError as e: # also covers PyJWKClientError
print(f"{label}: rejected: {type(e).__name__}: {e}")

How it works

  1. PyJWKClient(url) downloads the JWKS on first use and caches it for 300 seconds (lifespan).
  2. get_signing_key_from_jwt(token) reads the unverified header, finds the key with that kid, and refetches the set once if the kid is unknown and the 30-second cooldown has passed.
  3. jwt.decode verifies the signature with signing_key.key, then exp, iss and aud; options={"require": […]} also rejects tokens that lack those claims.
  4. Every error derives from jwt.PyJWTError: InvalidAudienceError, InvalidIssuerError, ExpiredSignatureError and, for an unknown key, PyJWKClientError.

Gotchas

  • algorithms=["ES256"] is mandatory in PyJWT 2 — and it must come from your configuration, not from the token header, or the token chooses how it is verified.
  • If the token has an aud but decode is called without audience=, PyJWT raises InvalidAudienceError: Invalid audience. That is deliberate: pass the audience your service expects instead of disabling the check.
  • The client is meant to live as long as the process. Creating a PyJWKClient per request discards the cache and fetches the JWKS for every token.
  • Fetch errors surface as PyJWKClientConnectionError (a subclass of PyJWKClientError), so a down JWKS endpoint looks like an invalid token unless you catch it separately and answer with a 5xx.

Related recipes