Alex Leko
All content on this blog was fully or partially created using local AI (Apple MLX).

Cloudflare blocked Keycloak JWKS requests from Python

  • cloudflare
  • keycloak
  • jwks
  • oidc
  • python

A protected API request kept returning 401 Invalid or missing bearer token. We fixed an audience mismatch in the Keycloak token, but the 401 persisted. The API was also unable to fetch the public signing keys it needed from Keycloak because Cloudflare blocked its Python request.

The setup

The Bible Quiz API accepts Keycloak access tokens. PyJWT's PyJWKClient fetches the realm's public signing keys, then the API checks each token's signature, issuer, audience, and expiration. It gets those keys from the realm's JWKS endpoint:

<keycloak-realm>/protocol/openid-connect/certs

The JWKS document contains public keys. The API must be able to fetch it without a user token or client secret.

The token was missing its audience

The first decoded access token had the expected issuer:

"iss": "<keycloak-realm>"

Its aud array did not include <client_id>. The token did have "azp": "<client_id>", but the API checks aud, not azp.

We added an Audience protocol mapper to the <client_id> client's dedicated client scope. The mapper's Included Client Audience was set to <client_id>, and Add to access token was enabled. A fresh token then included <client_id> in aud.

The audience mismatch was fixed, but the API still returned 401. That led us to check how it fetched the signing key.

Cloudflare blocked Python's JWKS request

The API returns the same generic 401 when a token is missing or validation fails, so the response did not identify the cause. Running the validator directly exposed it:

PyJWKClientConnectionError: Fail to fetch data from the url, err: "HTTP Error 403: Forbidden"

A regular curl request to the JWKS URL returned 200. That did not match what the API saw. PyJWKClient uses Python's urllib request behavior, so we repeated the request with Python's user agent:

curl -i -A 'Python-urllib/3.13' \
  '<keycloak-realm>/protocol/openid-connect/certs'

Cloudflare returned 403 with error code 1010. It allowed the ordinary curl request and blocked the Python request. Without the JWKS key, PyJWKClient could not verify the token's signature, and the API returned its generic 401.

The Cloudflare fix

We searched Cloudflare Security Events for the blocked request, identified the feature responsible, and scoped an exception to this JWKS request. The rest of the auth hostname kept its existing protections.

The matching conditions were:

(http.host eq "auth.ldw.solutions"
 and http.request.method eq "GET"
 and http.request.uri.path eq "/realms/<keycloak-realm>/protocol/openid-connect/certs"
 and http.user_agent eq "Python-urllib/3.13")

We created a Cloudflare custom rule with the Skip action and selected the feature identified in Security Events. Depending on the event, that may be Browser Integrity Check or User Agent Blocking. A stable public egress IP can further narrow the match. The Python user-agent string can change between versions, so update that condition when upgrading Python.

After deployment, the Python-user-agent request returned 200 with a keys array. PyJWKClient fetched the signing key, and the API accepted a fresh token.

What to check when this happens

For a similar 401:

  1. Inspect the token locally. Check iss, aud, and exp, and keep the token private. azp does not satisfy the aud check.
  2. Run the same PyJWKClient validation as the API. A PyJWKClientConnectionError means the client could not fetch the signing key.
  3. Test the configured JWKS URL from the API's environment with ordinary curl and with Python's user agent. A 200 from curl alone does not prove the Python request can get through.
  4. If Cloudflare blocks it, find the event in Security Events and scope the exception to anonymous GET requests for the JWKS path.
  5. Retest key retrieval, restart the API if needed, and sign in again to get a fresh access token.

The API checks no Keycloak user or client roles. The role mappings were unrelated to this 401; token validation stopped when the API could not fetch the signing key.

Keep the exception narrow

Keep the exception limited to the JWKS path and GET method. The endpoint is public by design and serves public verification keys. Do not send a client secret to it or turn off Cloudflare protection for the whole auth host. Swagger's Authorization Code + PKCE flow uses a public client, so leave its client secret blank.

Cloudflare references: