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 unknownjwks_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"]}, )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
PyJWKClient(url)downloads the JWKS on first use and caches it for 300 seconds (lifespan).get_signing_key_from_jwt(token)reads the unverified header, finds the key with thatkid, and refetches the set once if thekidis unknown and the 30-second cooldown has passed.jwt.decodeverifies the signature withsigning_key.key, thenexp,issandaud;options={"require": […]}also rejects tokens that lack those claims.- Every error derives from
jwt.PyJWTError:InvalidAudienceError,InvalidIssuerError,ExpiredSignatureErrorand, 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
audbutdecodeis called withoutaudience=, PyJWT raisesInvalidAudienceError: 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
PyJWKClientper request discards the cache and fetches the JWKS for every token. - Fetch errors surface as
PyJWKClientConnectionError(a subclass ofPyJWKClientError), so a down JWKS endpoint looks like an invalid token unless you catch it separately and answer with a 5xx.