Skip to content

JWKS endpoint emits malformed Cache-Control header (header name duplicated in value) #1689

@cubricmms

Description

@cubricmms

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

  1. 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,
    }
  2. Include the default URLs (path("o/", include("oauth2_provider.urls"))) and start the server.
  3. Request the JWKS endpoint:
    curl -I http://localhost:8000/o/.well-known/jwks.json
  4. 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).

  • I have tested with the latest published release and it's still a problem.
  • I have tested with the master branch and it's still a problem.

Additional context

n/a

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions