Describe the bug
JwksInfoView in oauth2_provider/views/oidc.py sets the Cache-Control response header with the header name literally included as a prefix inside the value. The response sent on the wire contains:
Cache-Control: Cache-Control: public, max-age=86400, stale-while-revalidate=86400, stale-if-error=86400
instead of the intended:
Cache-Control: public, max-age=86400, stale-while-revalidate=86400, stale-if-error=86400
The offending code (present in 3.2.0 and on current master):
# oauth2_provider/views/oidc.py — JwksInfoView.get()
response["Cache-Control"] = (
"Cache-Control: public, " # <-- header name leaks into the value
+ f"max-age={oauth2_settings.OIDC_JWKS_MAX_AGE_SECONDS}, "
+ f"stale-while-revalidate={oauth2_settings.OIDC_JWKS_MAX_AGE_SECONDS}, "
+ f"stale-if-error={oauth2_settings.OIDC_JWKS_MAX_AGE_SECONDS}"
)
HttpResponse.__setitem__ expects the header value only — it does not parse/strip a <name>: prefix — so the literal string Cache-Control: becomes the first "directive" in the emitted header value. Per RFC 9111 §5.2.3, caches must ignore unknown directives, so downstream caches/CDNs drop max-age, stale-while-revalidate, and stale-if-error and fall back to their defaults. The JWKS caching behavior configured by OIDC_JWKS_MAX_AGE_SECONDS is effectively disabled.
To Reproduce
- Minimal project with
django-oauth-toolkit installed and OIDC enabled:
# settings.py
OAUTH2_PROVIDER = {
"OIDC_ENABLED": True,
"OIDC_RSA_PRIVATE_KEY": "<PEM>",
"OIDC_JWKS_MAX_AGE_SECONDS": 86400,
}
- Include the default URLs (
path("o/", include("oauth2_provider.urls"))) and start the server.
- Request the JWKS endpoint:
curl -I http://localhost:8000/o/.well-known/jwks.json
- Observe the malformed response header:
cache-control: Cache-Control: public, max-age=86400, stale-while-revalidate=86400, stale-if-error=86400
Expected behavior
The response should contain a well-formed Cache-Control header without the duplicated header name:
cache-control: public, max-age=86400, stale-while-revalidate=86400, stale-if-error=86400
Proposed fix — remove the "Cache-Control: " prefix from the value:
response["Cache-Control"] = (
"public, "
+ f"max-age={oauth2_settings.OIDC_JWKS_MAX_AGE_SECONDS}, "
+ f"stale-while-revalidate={oauth2_settings.OIDC_JWKS_MAX_AGE_SECONDS}, "
+ f"stale-if-error={oauth2_settings.OIDC_JWKS_MAX_AGE_SECONDS}"
)
Version
django-oauth-toolkit 3.2.0 (also reproduces against current master).
Additional context
n/a
Describe the bug
JwksInfoViewinoauth2_provider/views/oidc.pysets theCache-Controlresponse header with the header name literally included as a prefix inside the value. The response sent on the wire contains:instead of the intended:
The offending code (present in
3.2.0and on currentmaster):HttpResponse.__setitem__expects the header value only — it does not parse/strip a<name>:prefix — so the literal stringCache-Control:becomes the first "directive" in the emitted header value. Per RFC 9111 §5.2.3, caches must ignore unknown directives, so downstream caches/CDNs dropmax-age,stale-while-revalidate, andstale-if-errorand fall back to their defaults. The JWKS caching behavior configured byOIDC_JWKS_MAX_AGE_SECONDSis effectively disabled.To Reproduce
django-oauth-toolkitinstalled and OIDC enabled:path("o/", include("oauth2_provider.urls"))) and start the server.Expected behavior
The response should contain a well-formed
Cache-Controlheader without the duplicated header name:Proposed fix — remove the
"Cache-Control: "prefix from the value:Version
django-oauth-toolkit
3.2.0(also reproduces against currentmaster).Additional context
n/a