RFC 9700 Security Best Current Practice

RFC 9700 (“Best Current Practice for OAuth 2.0 Security”, BCP 240) updates and extends the security advice in RFC 6749, RFC 6750, and RFC 6819. This page maps each relevant recommendation to django-oauth-toolkit (DOT) behavior and the setting that controls it.

Gated behaviors and the 3.x → 4.0 transition

Every RFC 9700 recommendation is covered by a COMPLIANT_BCP_RFC9700_<topic> boolean gate: True means the recommendation is enforced, False (the current default) preserves the legacy behavior. There are two kinds:

Behavior gates control whether DOT itself performs a discouraged behavior:

  • False (the current default) — the insecure/legacy behavior is allowed. The request-time gates (implicit grant, password grant, plain PKCE, access token in the query string) emit a DeprecationWarning each time the behavior is exercised. The two ambient gates (COMPLIANT_BCP_RFC9700_AUTHZ_RESPONSE_ISS and COMPLIANT_BCP_RFC9700_TOKEN_STORAGE) would fire on every request, so they are surfaced by manage.py check --deploy (W005/W006) instead of a per-request/per-token warning.

  • True — the behavior is enforced: the insecure request is rejected, or the secure behavior is performed instead.

Config-validation gates cover recommendations that are expressed through existing settings (REFRESH_TOKEN_REUSE_PROTECTION, ALLOWED_REDIRECT_URI_SCHEMES, ALLOW_URI_WILDCARDS, PKCE_REQUIRED). The gate does not replace the setting — the setting stays canonical and in control of runtime behavior. Instead the gate sets the severity of the manage.py check --deploy message when the setting is on a non-compliant value:

  • False (the current default) — an insecure value produces a check Warning.

  • True — an insecure value produces a check Error, so a non-compliant configuration cannot pass deploy checks. (A compliant value produces nothing in either position.)

The config-validation gates are COMPLIANT_BCP_RFC9700_REFRESH_TOKEN (§4.14.2), COMPLIANT_BCP_RFC9700_REDIRECT_URI_SCHEME (§2.1), COMPLIANT_BCP_RFC9700_REDIRECT_URI_MATCHING (§4.1.1), and COMPLIANT_BCP_RFC9700_PKCE_REQUIRED (§2.1.1).

These defaults are scheduled to flip to True in the 4.0 release. Set them to True now to adopt the compliant behavior early and silence the warnings.

Run python manage.py check --deploy to get a checklist of every recommendation that is currently on a non-compliant value (warnings while the gates are False, errors once they are True).

Compliant settings block

To adopt the RFC 9700 recommendations today, add the following to your OAUTH2_PROVIDER setting. Hashed token storage is left commented out because it is opt-in and incompatible with a non-zero refresh-token grace period (see the caveat below); enable it once you have confirmed REFRESH_TOKEN_GRACE_PERIOD_SECONDS is 0:

OAUTH2_PROVIDER = {
    # ... your existing settings ...

    # RFC 9700 gates (default False today; will default True in 4.0)
    "COMPLIANT_BCP_RFC9700_IMPLICIT_GRANT": True,
    "COMPLIANT_BCP_RFC9700_PASSWORD_GRANT": True,
    "COMPLIANT_BCP_RFC9700_PKCE_METHOD": True,
    "COMPLIANT_BCP_RFC9700_ACCESS_TOKEN_TRANSPORT": True,
    "COMPLIANT_BCP_RFC9700_AUTHZ_RESPONSE_ISS": True,

    # Canonical settings whose defaults also change in 4.0
    "REFRESH_TOKEN_REUSE_PROTECTION": True,
    "ALLOWED_REDIRECT_URI_SCHEMES": ["https"],

    # Config-validation gates: turn any remaining insecure value of the
    # settings above (plus ALLOW_URI_WILDCARDS / PKCE_REQUIRED) into a
    # `check --deploy` error instead of a warning
    "COMPLIANT_BCP_RFC9700_REFRESH_TOKEN": True,
    "COMPLIANT_BCP_RFC9700_REDIRECT_URI_SCHEME": True,
    "COMPLIANT_BCP_RFC9700_REDIRECT_URI_MATCHING": True,
    "COMPLIANT_BCP_RFC9700_PKCE_REQUIRED": True,

    # Optional, opt-in hardening (see the caveat below)
    # "COMPLIANT_BCP_RFC9700_TOKEN_STORAGE": True,
}

Recommendation-by-recommendation

PKCE (§2.1.1)

PKCE is required by default (PKCE_REQUIRED is True). RFC 9700 also discourages the plain code_challenge_method in favor of S256; set COMPLIANT_BCP_RFC9700_PKCE_METHOD = True to reject plain challenges and drop it from the authorization-server metadata. A deployment that sets PKCE_REQUIRED = False is flagged by the COMPLIANT_BCP_RFC9700_PKCE_REQUIRED validation gate (W010 while the gate is False, E005 once it is True; a callable PKCE_REQUIRED is a per-client policy and is not flagged).

Redirect URI matching (§2.1)

DOT already performs exact redirect-URI matching (scheme, host, port, and path), with wildcards off (ALLOW_URI_WILDCARDS defaults to False). Set ALLOWED_REDIRECT_URI_SCHEMES = ["https"] to disallow registering plaintext http redirect URIs. Two validation gates cover these settings: COMPLIANT_BCP_RFC9700_REDIRECT_URI_SCHEME flags http in the scheme list (W008/E003) and COMPLIANT_BCP_RFC9700_REDIRECT_URI_MATCHING flags ALLOW_URI_WILDCARDS = True (W009/E004).

Note

Requiring https also disallows native-app loopback callbacks (http://127.0.0.1/[::1], RFC 8252), because redirect URIs are validated against ALLOWED_REDIRECT_URI_SCHEMES by scheme. Keep "http" in the list if you must support them. ALLOW_LOCALHOST_LOOPBACK only extends the any-port loopback exemption to http://localhost — it does not re-enable the http scheme.

Implicit grant (§2.1.2)

The implicit grant MUST NOT be used. Set COMPLIANT_BCP_RFC9700_IMPLICIT_GRANT = True to reject the token / id_token response types and stop advertising implicit in the authorization-server metadata.

Resource owner password credentials grant (§2.4)

The password grant MUST NOT be used. Set COMPLIANT_BCP_RFC9700_PASSWORD_GRANT = True to reject grant_type=password and stop advertising it.

Access tokens in the query string (§4.3.2)

Access tokens MUST NOT be transmitted in the URI query string. Set COMPLIANT_BCP_RFC9700_ACCESS_TOKEN_TRANSPORT = True to reject requests that present an access_token query parameter at the resource server. The Authorization header (and form-encoded body per RFC 6750) are unaffected.

Mix-up attacks / issuer identification (§4.4, RFC 9207)

Set COMPLIANT_BCP_RFC9700_AUTHZ_RESPONSE_ISS = True to include the iss parameter (RFC 9207) in the authorization response and advertise authorization_response_iss_parameter_supported in the metadata. The iss value matches the metadata issuer (OIDC_ISS_ENDPOINT when configured, otherwise derived from the request).

Note

Multi-tenant deployments that use the RFC 8414 path-component issuer form (/.well-known/oauth-authorization-server/<issuer_path>) MUST set OIDC_ISS_ENDPOINT (per issuer) so the iss parameter matches the published metadata issuer; the issuer suffix cannot be derived from the authorization request itself.

Refresh-token rotation and replay detection (§4.14)

Rotation is on by default (ROTATE_REFRESH_TOKEN). Set REFRESH_TOKEN_REUSE_PROTECTION to True to revoke the entire token family when a refresh token is replayed (§4.14.2). Note that reuse detection only treats a replay as an attack after REFRESH_TOKEN_GRACE_PERIOD_SECONDS. The COMPLIANT_BCP_RFC9700_REFRESH_TOKEN validation gate flags REFRESH_TOKEN_REUSE_PROTECTION = False (W007 while the gate is False, E002 once it is True).

Token storage at rest (§4)

By default DOT stores access and refresh tokens in cleartext (alongside a SHA-256 token_checksum used for lookup). Set COMPLIANT_BCP_RFC9700_TOKEN_STORAGE = True to store only the token hash, so a database read no longer discloses usable tokens. Existing cleartext tokens are left in place and age out as they expire or rotate.

Warning

Hashed token storage is incompatible with the refresh-token grace period, which must return a previously issued (cleartext) token from the database. When COMPLIANT_BCP_RFC9700_TOKEN_STORAGE is True you must set REFRESH_TOKEN_GRACE_PERIOD_SECONDS = 0 (the default); manage.py check --deploy raises oauth2_provider.E001 otherwise.

Out of scope

Sender-constrained access tokens (DPoP, RFC 9449, and mutual-TLS, RFC 8705; RFC 9700 §2.2/§4.13) are not implemented. DOT issues bearer tokens only.