Update dependency django-oauth-toolkit to v3.4.0 #27

Open
bot.renovate wants to merge 1 commit from renovate/django-oauth-toolkit-3.x-lockfile into main
Member

This PR contains the following updates:

Package Type Update Change
django-oauth-toolkit project.dependencies minor 3.3.03.4.0

Release Notes

django-oauth/django-oauth-toolkit (django-oauth-toolkit)

v3.4.0

Compare Source

The headline of this release is first-class support for the Model Context Protocol (MCP)
authorization server role.
MCP's authorization spec is built on a stack of modern OAuth RFCs,
and this cycle landed the whole stack: Authorization Server Metadata (RFC 8414) and Protected
Resource Metadata (RFC 9728) for discovery, Dynamic Client Registration (RFC 7591 / RFC 7592) and
OAuth Client ID Metadata Documents (CIMD) so clients can register themselves, Resource Indicators
(RFC 8707) for audience-bound access tokens, and the OAuth 2.0 Security Best Current Practice
(RFC 9700) together with the RFC 9207 iss parameter. The RFC 9700 compliance gates double as a
configurable OAuth 2.1 security posture — they can reject the implicit and password grants and
enforce S256-only PKCE (legacy behavior by default in 3.4, scheduled to flip to compliant in 4.0).
The new ALLOW_LOCALHOST_LOOPBACK setting smooths the ephemeral-port loopback callback used by
native clients such as Claude Code, MCP Inspector, and mcp-remote.

Beyond MCP, the release adds a Django Ninja integration alongside the existing DRF support and
support for RP-Initiated Registration, lifts the 255-character cap on refresh tokens (mirroring the
access-token checksum scheme), makes cleartokens reclaim revoked refresh tokens sooner, and
harmonizes Bearer Authorization header parsing across the middleware.

It also carries a batch of security fixes: an unauthenticated open redirect from the
authorization endpoint (prompt=none), HS256 ID tokens being signed with the hashed client
secret, cleartext tokens and codes exposed in the Django admin, client secrets written to debug
logs, and predictable device-flow user_code generation. Longstanding operational bugs are fixed
too, including a multi-database migrate deadlock (#​1591) and duplicate unique indexes that broke
fresh installs on Oracle and strict MySQL (#​1656).

Before upgrading, read the breaking-changes section below: most items are makemigrations
steps for swapped models, but applications using the HS256 signing algorithm now require
hash_client_secret=False.

WARNING - POTENTIAL BREAKING CHANGES
  • Applications using the HS256 signing algorithm must now be configured with
    hash_client_secret=False. Previously such applications signed ID tokens with the hashed client
    secret, producing tokens that relying parties could not verify. Application.clean() now raises a
    ValidationError for HS256 + hash_client_secret=True, and Application.jwk_key raises
    ImproperlyConfigured at signing time if the secret is hashed. To migrate an affected
    application, recreate it (or reset its secret) with hash_client_secret=False so the plaintext
    secret is stored and can be used as the shared HMAC key.
  • Changes to the AbstractRefreshToken model require doing a manage.py migrate after upgrading.
  • If you use a swapped refresh token model (OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL) you will need to
    update your custom model with manage.py makemigrations. If your table already contains refresh
    tokens you must also backfill token_checksum with a data migration — adapt the batched backfill
    loop from forwards_func in
    oauth2_provider/migrations/0015_refreshtoken_token_checksum.py (dropping its swapped-model
    guard, the early return, and resolving your own model instead) and keep the same operation order:
    add nullable checksum → drop the old ("token", "revoked") unique constraint → widen token to
    TextField → backfill → make checksum non-nullable → add the ("token_checksum", "revoked")
    unique constraint.
  • If you use a swapped application model (OAUTH2_PROVIDER_APPLICATION_MODEL), run
    manage.py makemigrations after upgrading: AbstractApplication gained a
    registration_source CharField (choices manual/dcr/cimd, default manual) to mark
    how an application was registered — for example via Dynamic Client Registration (#​670). This
    replaces the never-released dcr_created BooleanField. Installs using the built-in Application
    model just need manage.py migrate (migration 0019).
  • If you use a swapped application model (OAUTH2_PROVIDER_APPLICATION_MODEL), run
    manage.py makemigrations after upgrading: for CIMD (#​1742) AbstractApplication gained a
    nullable cimd_expires_at DateTimeField, and client_id widened from max_length=100 to
    255 so a metadata-document URL fits. Installs using the built-in Application model just need
    manage.py migrate (migration 0020).
  • If you use a swapped device grant model (OAUTH2_PROVIDER_DEVICE_GRANT_MODEL), run
    manage.py makemigrations after upgrading: the redundant field-level unique=True was removed
    from AbstractDeviceGrant.device_code (#​1656), and AbstractDeviceGrant.scope changed from
    CharField(max_length=64, null=True) to a non-nullable TextField(blank=True) (#​1693). When
    prompted for a default for existing NULL scope rows, provide the one-off default ""
    matching oauth2_provider/migrations/0016_alter_devicegrant_scope.py. Uniqueness remains enforced by the
    <app_label>_<class>_unique_device_code constraint. If you are doing a fresh install on
    Oracle (or a MySQL backend that raises warnings as errors), you must also regenerate — or
    hand-edit — your existing CreateModel migration for the swapped model, since it still declares
    both uniqueness rules and will fail the same way migration 0013 did.
  • If you use a swapped access token model (OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL) and have not
    yet applied
    the 0012_add_token_checksum migration (i.e. you are upgrading from a version
    below 3.0), its token_checksum backfill now deterministically skips the swapped model — the
    schema operations in that migration never applied to swapped models, and the old backfill only
    worked when the ordering of your app's migrations happened to allow it. migrate logs a warning
    when the backfill is skipped and your table contains access tokens. Until token_checksum is
    backfilled those tokens will not validate; no data is lost, and tokens work again as soon as the
    checksum is populated. To backfill, add a data migration to your app (ordered after your
    migration that adds token_checksum): adapt the batched backfill loop from forwards_func in
    oauth2_provider/migrations/0012_add_token_checksum.py, dropping its swapped-model guard (the
    early return) and resolving your own model instead. You can check for affected rows with
    YourAccessToken.objects.filter(token_checksum__isnull=True).exists(). Installs that already
    applied 0012 (any 3.x deployment) are unaffected.
Added
  • #​1373 Integration and docs for Django Ninja authentication
  • #​1546 Support for RP-Initiated Registration
  • #​1099 Add RFC 8414 OAuth 2.0 Authorization Server Metadata endpoint (/.well-known/oauth-authorization-server)
  • #​1743 Add RFC 9728 OAuth 2.0 Protected Resource Metadata endpoint (/.well-known/oauth-protected-resource), plus opt-in
    mixins/decorators (ProtectedResourceMetadataMixin, protected_resource_metadata) and a DRF authenticator
    (OAuth2ProtectedResourceAuthentication) that advertise it via the resource_metadata WWW-Authenticate challenge parameter
  • #​1635 Dynamic help text on the application client_secret field, warning users to copy the
    secret on creation and explaining it is hashed and unrecoverable when editing. The help text is
    shared by both the Django admin application form and the front-end register/edit views: the
    ApplicationAdmin uses ApplicationForm, and a shared oauth2_provider/js/application_form.js
    updates the text live as the hash_client_secret checkbox is toggled on either surface. The
    form also warns immediately when the HS256 algorithm is selected while the client secret is —
    or will be — hashed, instead of only surfacing the error on save. (#​1697, #​1740)
  • #​670 Dynamic Client Registration Protocol (RFC 7591 / RFC 7592) — DynamicClientRegistrationView and
    DynamicClientRegistrationManagementView with configurable permission classes and registration access
    tokens. Dynamically registered applications are flagged with AbstractApplication.registration_source
    set to "dcr" and can be filtered in the Django admin.
  • #​1739 ALLOW_LOCALHOST_LOOPBACK setting to extend the RFC 8252 §7.3 any-port loopback exemption to http://localhost redirect URIs (opt-in, default False)
  • #​1742 Support for OAuth Client ID Metadata Documents (CIMD,
    draft-ietf-oauth-client-id-metadata-document). A client may present an https URL as its
    client_id; when CIMD_ENABLED is on the server fetches, validates and persists the metadata
    document as a public application (SSRF-hardened fetch, failure backoff and an in-flight fetch cap).
    Applications resolved this way carry AbstractApplication.registration_source set to "cimd".
    Registration can be gated with CIMD_REGISTRATION_PERMISSION_CLASSES (default allow-all;
    HostAllowlistCIMDPermission restricts it to CIMD_ALLOWED_HOSTS), and the
    clearcimdapplications management command prunes expired CIMD applications that hold no live
    tokens. See docs/cimd.rst.
  • #​1751 Advertise the Dynamic Client Registration endpoint as registration_endpoint in the RFC 8414
    authorization server metadata document when DCR_ENABLED is on
  • #​1626 RFC 8707 "Resource Indicators" support
    • clients can optionally specify resource parameter during authorization or access token requests
    • Resource binding stored in Grant, AccessToken and RefreshToken models
    • Token introspection endpoint returns aud claim for tokens with resource indicators
  • #​1749 RFC 9700 (OAuth 2.0 Security Best Current Practice) compliance
    gates, each controlled by a COMPLIANT_BCP_RFC9700_<topic> setting that defaults to False (current behavior,
    warns when the discouraged behavior is used) and is scheduled to default to True in 4.0 (enforces the
    compliant behavior): COMPLIANT_BCP_RFC9700_IMPLICIT_GRANT (§2.1.2),
    COMPLIANT_BCP_RFC9700_PASSWORD_GRANT (§2.4), COMPLIANT_BCP_RFC9700_PKCE_METHOD (§2.1.1),
    COMPLIANT_BCP_RFC9700_ACCESS_TOKEN_TRANSPORT (§4.3.2), COMPLIANT_BCP_RFC9700_AUTHZ_RESPONSE_ISS (§4.4),
    and COMPLIANT_BCP_RFC9700_TOKEN_STORAGE (§4). Enforced behaviors are also removed from the RFC 8414
    authorization-server metadata and the OIDC discovery document, so both stay consistent with what the
    server accepts.
  • #​1749 RFC 9207 iss authorization-response parameter and the
    authorization_response_iss_parameter_supported metadata field (mix-up defense), gated by
    COMPLIANT_BCP_RFC9700_AUTHZ_RESPONSE_ISS.
  • #​1749 Config-validation gates for the RFC 9700 recommendations expressed through existing settings (the settings stay
    canonical; the gate only sets validation severity — insecure value → check Warning while the gate is False,
    check Error once it is True): COMPLIANT_BCP_RFC9700_REFRESH_TOKEN
    (REFRESH_TOKEN_REUSE_PROTECTION, §4.14.2), COMPLIANT_BCP_RFC9700_REDIRECT_URI_SCHEME
    (ALLOWED_REDIRECT_URI_SCHEMES, §2.1), COMPLIANT_BCP_RFC9700_REDIRECT_URI_MATCHING
    (ALLOW_URI_WILDCARDS, §4.1.1), and COMPLIANT_BCP_RFC9700_PKCE_REQUIRED (PKCE_REQUIRED, §2.1.1).
  • #​1749 A --deploy security system check that flags every RFC 9700 recommendation currently on a non-compliant value
    (warnings oauth2_provider.W001W010, errors oauth2_provider.E002E005 when the corresponding
    config-validation gate is enabled), plus an error (oauth2_provider.E001) for the incompatible combination of
    hashed token storage and a non-zero REFRESH_TOKEN_GRACE_PERIOD_SECONDS.
  • #​1749 New docs/security.rst page mapping each RFC 9700 recommendation to the corresponding setting. The demo IdP
    exposes every gate as an OAUTH2_PROVIDER_COMPLIANT_BCP_RFC9700_* environment variable so the Docker image and the
    e2e suite can exercise both gate positions.
  • #​1660 Extract the HttpRequest creation in OAuth2Validator.validate_user into an overridable
    build_http_request method, so subclasses can pass extra attributes through to their authentication backends.
Changed
  • #​1732 Bearer Authorization header parsing is now harmonized across the codebase via a shared
    oauth2_provider.utils.parse_bearer_token helper implementing RFC 7235 / RFC 6750 semantics.
    As a result, OAuth2TokenMiddleware and OAuth2ExtraTokenMiddleware now accept the scheme
    case-insensitively (e.g. a lowercase bearer header, which is RFC-correct, is no longer
    ignored) and no longer mis-parse non-Bearer schemes that merely start with Bearer
    (e.g. BearerX token was previously treated as a Bearer token and is now rejected).
  • #​1688 cleartokens now removes revoked refresh tokens once REFRESH_TOKEN_GRACE_PERIOD_SECONDS
    has passed, instead of keeping them until REFRESH_TOKEN_EXPIRE_SECONDS. When
    REFRESH_TOKEN_REUSE_PROTECTION is enabled, revoked tokens are still kept until they expire so
    that token reuse can be detected.
  • #​1601 RefreshToken.token is now a TextField and lookups use a new SHA-256 token_checksum
    field, removing the 255 character limit so long refresh tokens (e.g. Microsoft's JWT refresh
    tokens) are supported. This mirrors the AccessToken.token_checksum approach introduced in 3.0.0
    (#​1447). The revocation endpoint also looks up access tokens by checksum now, restoring an indexed
    lookup there.
  • #​1652 The 0012_add_token_checksum backfill now computes checksums in batched bulk_update
    calls (1000 rows per statement) instead of saving each access token individually, sharply
    reducing how long the migration locks the access token table on large installations. Running
    cleartokens before upgrading is still the best preparation for tables with many expired
    tokens. See the warning above if you use a swapped access token model.
Deprecated
  • #​1749 Using the OAuth 2.0 implicit grant, the resource owner password credentials grant, the PKCE plain
    code_challenge_method, or an access token in the URI query string now emits a DeprecationWarning, per
    RFC 9700. Each is gated by the corresponding
    COMPLIANT_BCP_RFC9700_* setting, whose default is scheduled to flip to True (enforcing rejection) in 4.0.
  • #​1700 Deprecate the AUTHENTICATION_SERVER_EXP_TIME_ZONE setting. Token introspection exp values are
    Unix timestamps and are always interpreted as UTC per RFC 7662/RFC 7519. The setting still works
    for backwards compatibility but now emits a DeprecationWarning and will be removed in a future
    release.
Fixed
  • #​1619 Accept wildcard redirect_uris whose hostname uses the double-dash form required for
    Netlify deploy-preview URLs (https://*--sitename.netlify.app). The validator previously stripped
    only a single leading hyphen after removing the *, leaving a hostname that began with - and was
    rejected by URIValidator; it now strips up to two leading hyphens while rejecting longer runs.
  • #​694 ReadWriteScopedResourceMixin.__new__() no longer forwards positional/keyword arguments to
    object.__new__(), which raised TypeError: object.__new__() takes exactly one argument when
    instantiating any view mixing this in with any argument at all — notably breaking Django REST
    Framework's cls(**initkwargs) view instantiation.
  • #​1006 A client_id or username containing a NUL (\x00) byte no longer causes a 500 error
    on database backends (e.g. PostgreSQL) that raise ValueError instead of executing the query;
    such values are now correctly treated as not matching any client/user.
  • #​1738 Fix the rw_protected_resource decorator accumulating the read/write scope on a shared list
    across requests. The required-scope list was built once at decoration time and appended to on
    every request, so after a write (POST) request the write scope stayed in the list and a
    subsequent read (GET) request with a read-only token was wrongly rejected. The behaviour was
    request-order dependent, not thread-safe, and also mutated a caller-supplied scopes list. The
    read/write scope is now added to a fresh per-request list.
  • #​1693 AbstractDeviceGrant.scope is now a TextField(blank=True) like the other grant and token
    models, instead of CharField(max_length=64, null=True). 64 characters is well below the limits
    common in the broader OAuth ecosystem (Okta allows 1024, Google 2048), so longer scope strings
    no longer fail or get truncated in the device authorization flow. Existing rows with a NULL
    scope are backfilled to an empty string by migration 0016.
  • #​1593 Use pk instead of id in clear_expired() and RefreshToken.revoke() so token models with a custom primary key field are supported.
  • #​1594 Fix introspection token expiry handling to consistently use UTC and avoid the deprecated
    datetime.utcfromtimestamp.
  • #​1696 Fix auth_time in oauth2 validator when user has never logged in.
  • #​1603 Honor user-overridden OIDC_SERVER_CLASS when OIDC_ENABLED is True and OAUTH2_SERVER_CLASS is not explicitly set; previously only the default was used in this fallback path.
  • #​1591 Fix migrate hanging on 0012_add_token_checksum when a database router or
    multi-database configuration is in use. The RunPython data migrations in 0006 and 0012 now
    pin their queries to schema_editor.connection.alias, so the backfill runs on the connection
    performing the migration instead of being routed to a second connection that deadlocks against
    the migration transaction's own locks. This also makes both migrations backfill the correct
    database when migrating a non-default alias (migrate --database=...). Thanks to Igor Petrik for
    the diagnosis and fix.
  • #​1656 Remove the redundant unique=True on DeviceGrant.device_code, which duplicated the
    unique_device_code UniqueConstraint and created two identical unique indexes on the same
    column. Fresh installs failed on Oracle (ORA-02261) and on MySQL backends that raise database
    warnings as errors (ER_DUP_INDEX, 1831). Migration 0013 is fixed in place because the
    duplicate was created inside CREATE TABLE, so a follow-up migration could never fix fresh
    installs. Databases that already applied the old 0013 keep one harmless extra unique index;
    it can optionally be dropped by hand. Thanks to Febin Micheal Antony (#​1659) and
    moscowmule2240 (#​1718) for the fixes.
Security
  • #​1734 Generate device-flow user_code values with the cryptographically secure secrets module
    instead of the predictable random module (Mersenne Twister). The user_code is a device
    authorization credential and must be unguessable per
    RFC 8628 sections 5.1 and 5.2.
  • #​1735 Stop writing client secrets to the logs. On a failed client authentication, the OAuth2Validator
    logged the submitted client_secret (and, for Basic auth, the base64 client_id:client_secret
    credential string) at DEBUG level. These messages now log at most the client_id (when it is
    available; the base64/unicode decode-failure paths log a generic message with no credential), so
    password-equivalent client secrets and raw credential strings no longer leak into log files or
    aggregators.
  • #​1736 Stop exposing cleartext access tokens, refresh tokens, and authorization codes in the Django
    admin. The default AccessTokenAdmin, RefreshTokenAdmin, and GrantAdmin classes listed the
    raw token/code in list_display and included them in search_fields. Because these values
    are stored in cleartext, any staff user with view access saw replayable credentials, and
    searching placed them in the ?q= query string (captured by access logs and browser history).
    The columns are now masked (last characters only) and are no longer searchable (search is
    available by application and user instead). The raw token/code field is also excluded from
    the admin change/view form, which showed the editable cleartext field to any staff user with view
    access; a masked read-only value is shown instead. Adding tokens/codes through the admin is now
    disabled (has_add_permission returns False on the AccessToken, RefreshToken, Grant, and
    IDToken admins) — these are issued by the OAuth flows and are not meant to be hand-created, and
    the add form would otherwise present an editable cleartext field. Relatedly, the AccessToken,
    RefreshToken, and Grant model __str__ methods no longer return the raw token/code (which the
    admin renders in a row's change-page title and breadcrumbs, and which also appears in repr() and
    logs); they now return a "<Model> #<pk>" identifier.
  • #​1737 Fix HS256-signed ID tokens being signed with the hashed client secret. When an application
    used the HS256 algorithm with hash_client_secret=True (the default), the ID token was signed
    with the stored password-hash string as the HMAC key instead of the shared client secret, so a
    relying party holding the real (plaintext) secret could never verify the signature — and a
    password hash was misused as a MAC key. HS256 now requires hash_client_secret=False:
    Application.clean() rejects the combination, and jwk_key raises ImproperlyConfigured
    rather than emit an unverifiable token. HS256 with an empty client secret is likewise rejected
    (an empty HMAC key would make ID tokens trivially forgeable). See the breaking-changes note above.
  • #​1719 Fix an unauthenticated open redirect from the authorization endpoint. A prompt=none request from
    an unauthenticated user was redirected to the supplied redirect_uri with a login_required error
    before the client and redirect_uri were validated, allowing an attacker to redirect a victim's
    browser to an arbitrary origin. The request is now validated against a registered client before any
    redirect, per OpenID Connect Core 1.0 section 3.1.2.6.
    Reported by Brian Lee (SSLab, Georgia Tech).

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR has been generated by Mend Renovate.

This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [django-oauth-toolkit](https://github.com/django-oauth/django-oauth-toolkit) | project.dependencies | minor | `3.3.0` → `3.4.0` | --- ### Release Notes <details> <summary>django-oauth/django-oauth-toolkit (django-oauth-toolkit)</summary> ### [`v3.4.0`](https://github.com/django-oauth/django-oauth-toolkit/blob/HEAD/CHANGELOG.md#340---2026-07-23) [Compare Source](https://github.com/django-oauth/django-oauth-toolkit/compare/3.3.0...3.4.0) The headline of this release is first-class support for the **Model Context Protocol (MCP)** authorization server role. MCP's authorization spec is built on a stack of modern OAuth RFCs, and this cycle landed the whole stack: Authorization Server Metadata (RFC 8414) and Protected Resource Metadata (RFC 9728) for discovery, Dynamic Client Registration (RFC 7591 / RFC 7592) and OAuth Client ID Metadata Documents (CIMD) so clients can register themselves, Resource Indicators (RFC 8707) for audience-bound access tokens, and the OAuth 2.0 Security Best Current Practice (RFC 9700) together with the RFC 9207 `iss` parameter. The RFC 9700 compliance gates double as a configurable **OAuth 2.1 security posture** — they can reject the implicit and password grants and enforce S256-only PKCE (legacy behavior by default in 3.4, scheduled to flip to compliant in 4.0). The new `ALLOW_LOCALHOST_LOOPBACK` setting smooths the ephemeral-port loopback callback used by native clients such as Claude Code, MCP Inspector, and mcp-remote. Beyond MCP, the release adds a **Django Ninja** integration alongside the existing DRF support and support for RP-Initiated Registration, lifts the 255-character cap on refresh tokens (mirroring the access-token checksum scheme), makes `cleartokens` reclaim revoked refresh tokens sooner, and harmonizes Bearer `Authorization` header parsing across the middleware. It also carries a **batch of security fixes**: an unauthenticated open redirect from the authorization endpoint (`prompt=none`), HS256 ID tokens being signed with the *hashed* client secret, cleartext tokens and codes exposed in the Django admin, client secrets written to debug logs, and predictable device-flow `user_code` generation. Longstanding operational bugs are fixed too, including a multi-database `migrate` deadlock ([#&#8203;1591](https://github.com/django-oauth/django-oauth-toolkit/issues/1591)) and duplicate unique indexes that broke fresh installs on Oracle and strict MySQL ([#&#8203;1656](https://github.com/django-oauth/django-oauth-toolkit/issues/1656)). **Before upgrading**, read the breaking-changes section below: most items are `makemigrations` steps for swapped models, but applications using the `HS256` signing algorithm now require `hash_client_secret=False`. ##### WARNING - POTENTIAL BREAKING CHANGES - Applications using the `HS256` signing algorithm must now be configured with `hash_client_secret=False`. Previously such applications signed ID tokens with the hashed client secret, producing tokens that relying parties could not verify. `Application.clean()` now raises a `ValidationError` for `HS256` + `hash_client_secret=True`, and `Application.jwk_key` raises `ImproperlyConfigured` at signing time if the secret is hashed. To migrate an affected application, recreate it (or reset its secret) with `hash_client_secret=False` so the plaintext secret is stored and can be used as the shared HMAC key. - Changes to the `AbstractRefreshToken` model require doing a `manage.py migrate` after upgrading. - If you use a swapped refresh token model (`OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL`) you will need to update your custom model with `manage.py makemigrations`. If your table already contains refresh tokens you must also backfill `token_checksum` with a data migration — adapt the batched backfill loop from `forwards_func` in `oauth2_provider/migrations/0015_refreshtoken_token_checksum.py` (dropping its swapped-model guard, the early return, and resolving your own model instead) and keep the same operation order: add nullable checksum → drop the old `("token", "revoked")` unique constraint → widen `token` to `TextField` → backfill → make checksum non-nullable → add the `("token_checksum", "revoked")` unique constraint. - If you use a swapped application model (`OAUTH2_PROVIDER_APPLICATION_MODEL`), run `manage.py makemigrations` after upgrading: `AbstractApplication` gained a `registration_source` `CharField` (choices `manual`/`dcr`/`cimd`, default `manual`) to mark how an application was registered — for example via Dynamic Client Registration ([#&#8203;670](https://github.com/django-oauth/django-oauth-toolkit/issues/670)). This replaces the never-released `dcr_created` `BooleanField`. Installs using the built-in Application model just need `manage.py migrate` (migration `0019`). - If you use a swapped application model (`OAUTH2_PROVIDER_APPLICATION_MODEL`), run `manage.py makemigrations` after upgrading: for CIMD ([#&#8203;1742](https://github.com/django-oauth/django-oauth-toolkit/issues/1742)) `AbstractApplication` gained a nullable `cimd_expires_at` `DateTimeField`, and `client_id` widened from `max_length=100` to `255` so a metadata-document URL fits. Installs using the built-in Application model just need `manage.py migrate` (migration `0020`). - If you use a swapped device grant model (`OAUTH2_PROVIDER_DEVICE_GRANT_MODEL`), run `manage.py makemigrations` after upgrading: the redundant field-level `unique=True` was removed from `AbstractDeviceGrant.device_code` ([#&#8203;1656](https://github.com/django-oauth/django-oauth-toolkit/issues/1656)), and `AbstractDeviceGrant.scope` changed from `CharField(max_length=64, null=True)` to a non-nullable `TextField(blank=True)` ([#&#8203;1693](https://github.com/django-oauth/django-oauth-toolkit/issues/1693)). When prompted for a default for existing NULL `scope` rows, provide the one-off default `""` — matching `oauth2_provider/migrations/0016_alter_devicegrant_scope.py`. Uniqueness remains enforced by the `<app_label>_<class>_unique_device_code` constraint. If you are doing a *fresh* install on Oracle (or a MySQL backend that raises warnings as errors), you must also regenerate — or hand-edit — your existing `CreateModel` migration for the swapped model, since it still declares both uniqueness rules and will fail the same way migration `0013` did. - If you use a swapped access token model (`OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL`) and have **not yet applied** the `0012_add_token_checksum` migration (i.e. you are upgrading from a version below 3.0), its `token_checksum` backfill now deterministically skips the swapped model — the schema operations in that migration never applied to swapped models, and the old backfill only worked when the ordering of your app's migrations happened to allow it. `migrate` logs a warning when the backfill is skipped and your table contains access tokens. Until `token_checksum` is backfilled those tokens will not validate; no data is lost, and tokens work again as soon as the checksum is populated. To backfill, add a data migration to your app (ordered after your migration that adds `token_checksum`): adapt the batched backfill loop from `forwards_func` in `oauth2_provider/migrations/0012_add_token_checksum.py`, dropping its swapped-model guard (the early return) and resolving your own model instead. You can check for affected rows with `YourAccessToken.objects.filter(token_checksum__isnull=True).exists()`. Installs that already applied `0012` (any 3.x deployment) are unaffected. ##### Added - [#&#8203;1373](https://github.com/django-oauth/django-oauth-toolkit/issues/1373) Integration and docs for Django Ninja authentication - [#&#8203;1546](https://github.com/django-oauth/django-oauth-toolkit/issues/1546) Support for RP-Initiated Registration - [#&#8203;1099](https://github.com/django-oauth/django-oauth-toolkit/issues/1099) Add RFC 8414 OAuth 2.0 Authorization Server Metadata endpoint (`/.well-known/oauth-authorization-server`) - [#&#8203;1743](https://github.com/django-oauth/django-oauth-toolkit/issues/1743) Add RFC 9728 OAuth 2.0 Protected Resource Metadata endpoint (`/.well-known/oauth-protected-resource`), plus opt-in mixins/decorators (`ProtectedResourceMetadataMixin`, `protected_resource_metadata`) and a DRF authenticator (`OAuth2ProtectedResourceAuthentication`) that advertise it via the `resource_metadata` `WWW-Authenticate` challenge parameter - [#&#8203;1635](https://github.com/django-oauth/django-oauth-toolkit/issues/1635) Dynamic help text on the application `client_secret` field, warning users to copy the secret on creation and explaining it is hashed and unrecoverable when editing. The help text is shared by both the Django admin application form and the front-end register/edit views: the `ApplicationAdmin` uses `ApplicationForm`, and a shared `oauth2_provider/js/application_form.js` updates the text live as the `hash_client_secret` checkbox is toggled on either surface. The form also warns immediately when the `HS256` algorithm is selected while the client secret is — or will be — hashed, instead of only surfacing the error on save. ([#&#8203;1697](https://github.com/django-oauth/django-oauth-toolkit/issues/1697), [#&#8203;1740](https://github.com/django-oauth/django-oauth-toolkit/issues/1740)) - [#&#8203;670](https://github.com/django-oauth/django-oauth-toolkit/issues/670) Dynamic Client Registration Protocol (RFC 7591 / RFC 7592) — `DynamicClientRegistrationView` and `DynamicClientRegistrationManagementView` with configurable permission classes and registration access tokens. Dynamically registered applications are flagged with `AbstractApplication.registration_source` set to `"dcr"` and can be filtered in the Django admin. - [#&#8203;1739](https://github.com/django-oauth/django-oauth-toolkit/issues/1739) `ALLOW_LOCALHOST_LOOPBACK` setting to extend the RFC 8252 §7.3 any-port loopback exemption to `http://localhost` redirect URIs (opt-in, default `False`) - [#&#8203;1742](https://github.com/django-oauth/django-oauth-toolkit/issues/1742) Support for OAuth Client ID Metadata Documents (CIMD, `draft-ietf-oauth-client-id-metadata-document`). A client may present an `https` URL as its `client_id`; when `CIMD_ENABLED` is on the server fetches, validates and persists the metadata document as a public application (SSRF-hardened fetch, failure backoff and an in-flight fetch cap). Applications resolved this way carry `AbstractApplication.registration_source` set to `"cimd"`. Registration can be gated with `CIMD_REGISTRATION_PERMISSION_CLASSES` (default allow-all; `HostAllowlistCIMDPermission` restricts it to `CIMD_ALLOWED_HOSTS`), and the `clearcimdapplications` management command prunes expired CIMD applications that hold no live tokens. See `docs/cimd.rst`. - [#&#8203;1751](https://github.com/django-oauth/django-oauth-toolkit/issues/1751) Advertise the Dynamic Client Registration endpoint as `registration_endpoint` in the RFC 8414 authorization server metadata document when `DCR_ENABLED` is on - [#&#8203;1626](https://github.com/django-oauth/django-oauth-toolkit/issues/1626) RFC 8707 "Resource Indicators" support - clients can optionally specify `resource` parameter during authorization or access token requests - Resource binding stored in Grant, AccessToken and RefreshToken models - Token introspection endpoint returns `aud` claim for tokens with resource indicators - [#&#8203;1749](https://github.com/django-oauth/django-oauth-toolkit/issues/1749) [RFC 9700](https://datatracker.ietf.org/doc/html/rfc9700) (OAuth 2.0 Security Best Current Practice) compliance gates, each controlled by a `COMPLIANT_BCP_RFC9700_<topic>` setting that defaults to `False` (current behavior, warns when the discouraged behavior is used) and is scheduled to default to `True` in 4.0 (enforces the compliant behavior): `COMPLIANT_BCP_RFC9700_IMPLICIT_GRANT` (§2.1.2), `COMPLIANT_BCP_RFC9700_PASSWORD_GRANT` (§2.4), `COMPLIANT_BCP_RFC9700_PKCE_METHOD` (§2.1.1), `COMPLIANT_BCP_RFC9700_ACCESS_TOKEN_TRANSPORT` (§4.3.2), `COMPLIANT_BCP_RFC9700_AUTHZ_RESPONSE_ISS` (§4.4), and `COMPLIANT_BCP_RFC9700_TOKEN_STORAGE` (§4). Enforced behaviors are also removed from the RFC 8414 authorization-server metadata and the OIDC discovery document, so both stay consistent with what the server accepts. - [#&#8203;1749](https://github.com/django-oauth/django-oauth-toolkit/issues/1749) [RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207) `iss` authorization-response parameter and the `authorization_response_iss_parameter_supported` metadata field (mix-up defense), gated by `COMPLIANT_BCP_RFC9700_AUTHZ_RESPONSE_ISS`. - [#&#8203;1749](https://github.com/django-oauth/django-oauth-toolkit/issues/1749) Config-validation gates for the RFC 9700 recommendations expressed through existing settings (the settings stay canonical; the gate only sets validation severity — insecure value → check Warning while the gate is `False`, check Error once it is `True`): `COMPLIANT_BCP_RFC9700_REFRESH_TOKEN` (`REFRESH_TOKEN_REUSE_PROTECTION`, §4.14.2), `COMPLIANT_BCP_RFC9700_REDIRECT_URI_SCHEME` (`ALLOWED_REDIRECT_URI_SCHEMES`, §2.1), `COMPLIANT_BCP_RFC9700_REDIRECT_URI_MATCHING` (`ALLOW_URI_WILDCARDS`, §4.1.1), and `COMPLIANT_BCP_RFC9700_PKCE_REQUIRED` (`PKCE_REQUIRED`, §2.1.1). - [#&#8203;1749](https://github.com/django-oauth/django-oauth-toolkit/issues/1749) A `--deploy` security system check that flags every RFC 9700 recommendation currently on a non-compliant value (warnings `oauth2_provider.W001`–`W010`, errors `oauth2_provider.E002`–`E005` when the corresponding config-validation gate is enabled), plus an error (`oauth2_provider.E001`) for the incompatible combination of hashed token storage and a non-zero `REFRESH_TOKEN_GRACE_PERIOD_SECONDS`. - [#&#8203;1749](https://github.com/django-oauth/django-oauth-toolkit/issues/1749) New `docs/security.rst` page mapping each RFC 9700 recommendation to the corresponding setting. The demo IdP exposes every gate as an `OAUTH2_PROVIDER_COMPLIANT_BCP_RFC9700_*` environment variable so the Docker image and the e2e suite can exercise both gate positions. - [#&#8203;1660](https://github.com/django-oauth/django-oauth-toolkit/issues/1660) Extract the `HttpRequest` creation in `OAuth2Validator.validate_user` into an overridable `build_http_request` method, so subclasses can pass extra attributes through to their authentication backends. ##### Changed - [#&#8203;1732](https://github.com/django-oauth/django-oauth-toolkit/issues/1732) Bearer `Authorization` header parsing is now harmonized across the codebase via a shared `oauth2_provider.utils.parse_bearer_token` helper implementing RFC 7235 / RFC 6750 semantics. As a result, `OAuth2TokenMiddleware` and `OAuth2ExtraTokenMiddleware` now accept the scheme case-insensitively (e.g. a lowercase `bearer` header, which is RFC-correct, is no longer ignored) and no longer mis-parse non-Bearer schemes that merely start with `Bearer` (e.g. `BearerX token` was previously treated as a Bearer token and is now rejected). - [#&#8203;1688](https://github.com/django-oauth/django-oauth-toolkit/issues/1688) `cleartokens` now removes revoked refresh tokens once `REFRESH_TOKEN_GRACE_PERIOD_SECONDS` has passed, instead of keeping them until `REFRESH_TOKEN_EXPIRE_SECONDS`. When `REFRESH_TOKEN_REUSE_PROTECTION` is enabled, revoked tokens are still kept until they expire so that token reuse can be detected. - [#&#8203;1601](https://github.com/django-oauth/django-oauth-toolkit/issues/1601) `RefreshToken.token` is now a `TextField` and lookups use a new SHA-256 `token_checksum` field, removing the 255 character limit so long refresh tokens (e.g. Microsoft's JWT refresh tokens) are supported. This mirrors the `AccessToken.token_checksum` approach introduced in 3.0.0 ([#&#8203;1447](https://github.com/django-oauth/django-oauth-toolkit/issues/1447)). The revocation endpoint also looks up access tokens by checksum now, restoring an indexed lookup there. - [#&#8203;1652](https://github.com/django-oauth/django-oauth-toolkit/issues/1652) The `0012_add_token_checksum` backfill now computes checksums in batched `bulk_update` calls (1000 rows per statement) instead of saving each access token individually, sharply reducing how long the migration locks the access token table on large installations. Running `cleartokens` before upgrading is still the best preparation for tables with many expired tokens. See the warning above if you use a swapped access token model. ##### Deprecated - [#&#8203;1749](https://github.com/django-oauth/django-oauth-toolkit/issues/1749) Using the OAuth 2.0 implicit grant, the resource owner password credentials grant, the PKCE `plain` `code_challenge_method`, or an access token in the URI query string now emits a `DeprecationWarning`, per [RFC 9700](https://datatracker.ietf.org/doc/html/rfc9700). Each is gated by the corresponding `COMPLIANT_BCP_RFC9700_*` setting, whose default is scheduled to flip to `True` (enforcing rejection) in 4.0. - [#&#8203;1700](https://github.com/django-oauth/django-oauth-toolkit/issues/1700) Deprecate the `AUTHENTICATION_SERVER_EXP_TIME_ZONE` setting. Token introspection `exp` values are Unix timestamps and are always interpreted as UTC per RFC 7662/RFC 7519. The setting still works for backwards compatibility but now emits a `DeprecationWarning` and will be removed in a future release. ##### Fixed - [#&#8203;1619](https://github.com/django-oauth/django-oauth-toolkit/issues/1619) Accept wildcard `redirect_uris` whose hostname uses the double-dash form required for Netlify deploy-preview URLs (`https://*--sitename.netlify.app`). The validator previously stripped only a single leading hyphen after removing the `*`, leaving a hostname that began with `-` and was rejected by `URIValidator`; it now strips up to two leading hyphens while rejecting longer runs. - [#&#8203;694](https://github.com/django-oauth/django-oauth-toolkit/issues/694) `ReadWriteScopedResourceMixin.__new__()` no longer forwards positional/keyword arguments to `object.__new__()`, which raised `TypeError: object.__new__() takes exactly one argument` when instantiating any view mixing this in with any argument at all — notably breaking Django REST Framework's `cls(**initkwargs)` view instantiation. - [#&#8203;1006](https://github.com/django-oauth/django-oauth-toolkit/issues/1006) A `client_id` or `username` containing a NUL (`\x00`) byte no longer causes a 500 error on database backends (e.g. PostgreSQL) that raise `ValueError` instead of executing the query; such values are now correctly treated as not matching any client/user. - [#&#8203;1738](https://github.com/django-oauth/django-oauth-toolkit/issues/1738) Fix the `rw_protected_resource` decorator accumulating the read/write scope on a shared list across requests. The required-scope list was built once at decoration time and appended to on every request, so after a write (`POST`) request the `write` scope stayed in the list and a subsequent read (`GET`) request with a read-only token was wrongly rejected. The behaviour was request-order dependent, not thread-safe, and also mutated a caller-supplied `scopes` list. The read/write scope is now added to a fresh per-request list. - [#&#8203;1693](https://github.com/django-oauth/django-oauth-toolkit/issues/1693) `AbstractDeviceGrant.scope` is now a `TextField(blank=True)` like the other grant and token models, instead of `CharField(max_length=64, null=True)`. 64 characters is well below the limits common in the broader OAuth ecosystem (Okta allows 1024, Google 2048), so longer scope strings no longer fail or get truncated in the device authorization flow. Existing rows with a NULL scope are backfilled to an empty string by migration `0016`. - [#&#8203;1593](https://github.com/django-oauth/django-oauth-toolkit/issues/1593) Use `pk` instead of `id` in `clear_expired()` and `RefreshToken.revoke()` so token models with a custom primary key field are supported. - [#&#8203;1594](https://github.com/django-oauth/django-oauth-toolkit/issues/1594) Fix introspection token expiry handling to consistently use UTC and avoid the deprecated `datetime.utcfromtimestamp`. - [#&#8203;1696](https://github.com/django-oauth/django-oauth-toolkit/issues/1696) Fix `auth_time` in oauth2 validator when user has never logged in. - [#&#8203;1603](https://github.com/django-oauth/django-oauth-toolkit/issues/1603) Honor user-overridden `OIDC_SERVER_CLASS` when `OIDC_ENABLED` is `True` and `OAUTH2_SERVER_CLASS` is not explicitly set; previously only the default was used in this fallback path. - [#&#8203;1591](https://github.com/django-oauth/django-oauth-toolkit/issues/1591) Fix `migrate` hanging on `0012_add_token_checksum` when a database router or multi-database configuration is in use. The `RunPython` data migrations in `0006` and `0012` now pin their queries to `schema_editor.connection.alias`, so the backfill runs on the connection performing the migration instead of being routed to a second connection that deadlocks against the migration transaction's own locks. This also makes both migrations backfill the correct database when migrating a non-default alias (`migrate --database=...`). Thanks to Igor Petrik for the diagnosis and fix. - [#&#8203;1656](https://github.com/django-oauth/django-oauth-toolkit/issues/1656) Remove the redundant `unique=True` on `DeviceGrant.device_code`, which duplicated the `unique_device_code` `UniqueConstraint` and created two identical unique indexes on the same column. Fresh installs failed on Oracle (`ORA-02261`) and on MySQL backends that raise database warnings as errors (`ER_DUP_INDEX`, 1831). Migration `0013` is fixed in place because the duplicate was created inside `CREATE TABLE`, so a follow-up migration could never fix fresh installs. Databases that already applied the old `0013` keep one harmless extra unique index; it can optionally be dropped by hand. Thanks to Febin Micheal Antony ([#&#8203;1659](https://github.com/django-oauth/django-oauth-toolkit/issues/1659)) and moscowmule2240 ([#&#8203;1718](https://github.com/django-oauth/django-oauth-toolkit/issues/1718)) for the fixes. ##### Security - [#&#8203;1734](https://github.com/django-oauth/django-oauth-toolkit/issues/1734) Generate device-flow `user_code` values with the cryptographically secure `secrets` module instead of the predictable `random` module (Mersenne Twister). The `user_code` is a device authorization credential and must be unguessable per [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628) sections 5.1 and 5.2. - [#&#8203;1735](https://github.com/django-oauth/django-oauth-toolkit/issues/1735) Stop writing client secrets to the logs. On a failed client authentication, the `OAuth2Validator` logged the submitted `client_secret` (and, for Basic auth, the base64 `client_id:client_secret` credential string) at `DEBUG` level. These messages now log at most the `client_id` (when it is available; the base64/unicode decode-failure paths log a generic message with no credential), so password-equivalent client secrets and raw credential strings no longer leak into log files or aggregators. - [#&#8203;1736](https://github.com/django-oauth/django-oauth-toolkit/issues/1736) Stop exposing cleartext access tokens, refresh tokens, and authorization codes in the Django admin. The default `AccessTokenAdmin`, `RefreshTokenAdmin`, and `GrantAdmin` classes listed the raw `token`/`code` in `list_display` and included them in `search_fields`. Because these values are stored in cleartext, any staff user with view access saw replayable credentials, and searching placed them in the `?q=` query string (captured by access logs and browser history). The columns are now masked (last characters only) and are no longer searchable (search is available by application and user instead). The raw `token`/`code` field is also excluded from the admin change/view form, which showed the editable cleartext field to any staff user with view access; a masked read-only value is shown instead. Adding tokens/codes through the admin is now disabled (`has_add_permission` returns `False` on the `AccessToken`, `RefreshToken`, `Grant`, and `IDToken` admins) — these are issued by the OAuth flows and are not meant to be hand-created, and the add form would otherwise present an editable cleartext field. Relatedly, the `AccessToken`, `RefreshToken`, and `Grant` model `__str__` methods no longer return the raw token/code (which the admin renders in a row's change-page title and breadcrumbs, and which also appears in `repr()` and logs); they now return a `"<Model> #<pk>"` identifier. - [#&#8203;1737](https://github.com/django-oauth/django-oauth-toolkit/issues/1737) Fix HS256-signed ID tokens being signed with the *hashed* client secret. When an application used the `HS256` algorithm with `hash_client_secret=True` (the default), the ID token was signed with the stored password-hash string as the HMAC key instead of the shared client secret, so a relying party holding the real (plaintext) secret could never verify the signature — and a password hash was misused as a MAC key. `HS256` now requires `hash_client_secret=False`: `Application.clean()` rejects the combination, and `jwk_key` raises `ImproperlyConfigured` rather than emit an unverifiable token. `HS256` with an empty client secret is likewise rejected (an empty HMAC key would make ID tokens trivially forgeable). See the breaking-changes note above. - [#&#8203;1719](https://github.com/django-oauth/django-oauth-toolkit/issues/1719) Fix an unauthenticated open redirect from the authorization endpoint. A `prompt=none` request from an unauthenticated user was redirected to the supplied `redirect_uri` with a `login_required` error *before* the client and `redirect_uri` were validated, allowing an attacker to redirect a victim's browser to an arbitrary origin. The request is now validated against a registered client before any redirect, per [OpenID Connect Core 1.0 section 3.1.2.6](https://openid.net/specs/openid-connect-core-1_0.html#AuthError). Reported by Brian Lee (SSLab, Georgia Tech). </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjEuNCIsInVwZGF0ZWRJblZlciI6IjQzLjI2MS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin renovate/django-oauth-toolkit-3.x-lockfile:renovate/django-oauth-toolkit-3.x-lockfile
git switch renovate/django-oauth-toolkit-3.x-lockfile

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff renovate/django-oauth-toolkit-3.x-lockfile
git switch renovate/django-oauth-toolkit-3.x-lockfile
git rebase main
git switch main
git merge --ff-only renovate/django-oauth-toolkit-3.x-lockfile
git switch renovate/django-oauth-toolkit-3.x-lockfile
git rebase main
git switch main
git merge --no-ff renovate/django-oauth-toolkit-3.x-lockfile
git switch main
git merge --squash renovate/django-oauth-toolkit-3.x-lockfile
git switch main
git merge --ff-only renovate/django-oauth-toolkit-3.x-lockfile
git switch main
git merge renovate/django-oauth-toolkit-3.x-lockfile
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
django-tinytools/django-tinyuser!27
No description provided.