Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
* #1637 Support for Django 6.0
* #670 Dynamic Client Registration Protocol (RFC 7591 / RFC 7592) — `DynamicClientRegistrationView` and `DynamicClientRegistrationManagementView` with configurable permission classes and registration access tokens

### Removed
* #1636 Remove support for Python 3.8 and 3.9
Expand Down
158 changes: 158 additions & 0 deletions docs/views/dynamic_client_registration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
Dynamic Client Registration
===========================

Django OAuth Toolkit includes support for the OAuth 2.0 Dynamic Client Registration Protocol
(`RFC 7591 <https://datatracker.ietf.org/doc/html/rfc7591>`_) and the OAuth 2.0 Dynamic Client
Registration Management Protocol (`RFC 7592 <https://datatracker.ietf.org/doc/html/rfc7592>`_).

These views are automatically included in ``base_urlpatterns`` when you use
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is inaccurate relative to the implementation: the DCR routes are added via urlpatterns = ... + dcr_urlpatterns, not included in base_urlpatterns. Please adjust the wording to avoid pointing readers to the wrong internal URL list (e.g., say the endpoints are included when include(\"oauth2_provider.urls\") is used, without naming base_urlpatterns).

Suggested change
These views are automatically included in ``base_urlpatterns`` when you use
These views are automatically available when you use

Copilot uses AI. Check for mistakes.
``include("oauth2_provider.urls")``.


Endpoints
---------

POST /o/register/
~~~~~~~~~~~~~~~~~

Creates a new OAuth2 application (RFC 7591). Authentication is controlled by
``DCR_REGISTRATION_PERMISSION_CLASSES``.

**Request body (JSON):**

.. code-block:: json

{
"redirect_uris": ["https://example.com/callback"],
"grant_types": ["authorization_code"],
"client_name": "My Application",
"token_endpoint_auth_method": "client_secret_basic"
}

**Response (201):**

.. code-block:: json

{
"client_id": "abc123",
"client_secret": "...",
"redirect_uris": ["https://example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "client_secret_basic",
"client_name": "My Application",
"registration_access_token": "...",
"registration_client_uri": "https://example.com/o/register/abc123/"
}

GET/PUT/DELETE /o/register/{client_id}/
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Read, update, or delete the client configuration (RFC 7592). Requires a
``Bearer {registration_access_token}`` header issued during registration.

- **GET** — returns current client metadata (same format as registration response)
- **PUT** — accepts the same JSON body as POST; updates the application
- **DELETE** — deletes the application and all associated tokens; returns 204


Field Mapping
-------------

+-------------------------------------+-----------------------------------+----------------------------------+
| RFC 7591 field | DOT Application field | Notes |
+=====================================+===================================+==================================+
| ``redirect_uris`` (array) | ``redirect_uris`` (space-joined) | |
+-------------------------------------+-----------------------------------+----------------------------------+
| ``client_name`` | ``name`` | |
+-------------------------------------+-----------------------------------+----------------------------------+
| ``grant_types`` (array) | ``authorization_grant_type`` | ``refresh_token`` is ignored; |
| | | only one non-refresh grant type |
| | | is supported per application |
+-------------------------------------+-----------------------------------+----------------------------------+
| ``token_endpoint_auth_method: none``| ``client_type = "public"`` | |
+-------------------------------------+-----------------------------------+----------------------------------+
| ``token_endpoint_auth_method: ...`` | ``client_type = "confidential"`` | Default |
+-------------------------------------+-----------------------------------+----------------------------------+


Configuration
-------------

Add the following keys to ``OAUTH2_PROVIDER`` in your Django settings. All are optional and have
sensible defaults.

``DCR_ENABLED``
Set to ``True`` to activate the Dynamic Client Registration endpoints.
When ``False`` (the default), both endpoints return ``404`` even though the
URL patterns are always registered.

Default: ``False``

``DCR_REGISTRATION_PERMISSION_CLASSES``
A tuple of importable class paths whose instances are instantiated and called as
``instance.has_permission(request) -> bool``. All classes must pass (AND logic).

Default: ``("oauth2_provider.dcr.IsAuthenticatedDCRPermission",)``

Built-in classes:

* ``oauth2_provider.dcr.IsAuthenticatedDCRPermission`` — requires Django session authentication.
* ``oauth2_provider.dcr.AllowAllDCRPermission`` — open registration; no authentication required.

``DCR_REGISTRATION_SCOPE``
The scope string stored on the registration ``AccessToken`` used to protect the RFC 7592
management endpoints.

Default: ``"oauth2_provider:registration"``

``DCR_REGISTRATION_TOKEN_EXPIRE_SECONDS``
Number of seconds until the registration access token expires, or ``None`` for a
far-future expiry (year 9999, effectively non-expiring).

Default: ``None``

``DCR_ROTATE_REGISTRATION_TOKEN_ON_UPDATE``
When ``True``, a PUT request to the management endpoint revokes the current registration
access token and issues a new one, returning it in the response.

Default: ``True``


Examples
--------

Open registration (no auth required):

.. code-block:: python

OAUTH2_PROVIDER = {
"DCR_REGISTRATION_PERMISSION_CLASSES": ("oauth2_provider.dcr.AllowAllDCRPermission",),
}

Custom permission class (e.g. initial-access token):

.. code-block:: python

# myapp/permissions.py
class InitialAccessTokenPermission:
def has_permission(self, request) -> bool:
token = request.META.get("HTTP_AUTHORIZATION", "").removeprefix("Bearer ").strip()
return MyInitialToken.objects.filter(token=token, active=True).exists()

# settings.py
OAUTH2_PROVIDER = {
"DCR_REGISTRATION_PERMISSION_CLASSES": ("myapp.permissions.InitialAccessTokenPermission",),
}

Smoke test with ``curl``:

.. code-block:: bash

# Register (open mode)
curl -X POST https://example.com/o/register/ \\
-H "Content-Type: application/json" \\
-d '{"redirect_uris":["https://app.example.com/cb"],"grant_types":["authorization_code"]}'

# Read configuration
curl https://example.com/o/register/{client_id}/ \\
-H "Authorization: Bearer {registration_access_token}"
1 change: 1 addition & 0 deletions docs/views/views.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Django OAuth Toolkit provides a set of pre-defined views for different purposes:
application
token
mixins
dynamic_client_registration
20 changes: 20 additions & 0 deletions oauth2_provider/dcr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
Permission classes for the Dynamic Client Registration endpoint (RFC 7591).

Each class must implement ``has_permission(request) -> bool``.
Configure via ``OAUTH2_PROVIDER["DCR_REGISTRATION_PERMISSION_CLASSES"]``.
"""


class IsAuthenticatedDCRPermission:
"""Allow registration only to session-authenticated users (default)."""

def has_permission(self, request) -> bool:
return bool(request.user and request.user.is_authenticated)


class AllowAllDCRPermission:
"""Allow registration to anyone (open registration)."""

def has_permission(self, request) -> bool:
return True
7 changes: 7 additions & 0 deletions oauth2_provider/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@
"ALWAYS_RELOAD_OAUTHLIB_CORE": False,
"CLEAR_EXPIRED_TOKENS_BATCH_SIZE": 10000,
"CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL": 0,
# Dynamic Client Registration (RFC 7591/7592)
"DCR_ENABLED": False,
"DCR_REGISTRATION_PERMISSION_CLASSES": ("oauth2_provider.dcr.IsAuthenticatedDCRPermission",),
"DCR_REGISTRATION_SCOPE": "oauth2_provider:registration",
"DCR_REGISTRATION_TOKEN_EXPIRE_SECONDS": None, # None = year 9999 (no expiry)
"DCR_ROTATE_REGISTRATION_TOKEN_ON_UPDATE": True,
}

# List of settings that cannot be empty
Expand Down Expand Up @@ -154,6 +160,7 @@
"GRANT_ADMIN_CLASS",
"ID_TOKEN_ADMIN_CLASS",
"REFRESH_TOKEN_ADMIN_CLASS",
"DCR_REGISTRATION_PERMISSION_CLASSES",
)


Expand Down
10 changes: 9 additions & 1 deletion oauth2_provider/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,13 @@
path("logout/", views.RPInitiatedLogoutView.as_view(), name="rp-initiated-logout"),
]

dcr_urlpatterns = [
path("register/", views.DynamicClientRegistrationView.as_view(), name="dcr-register"),
path(
"register/<str:client_id>/",
views.DynamicClientRegistrationManagementView.as_view(),
name="dcr-register-management",
),
]
Comment on lines +60 to +67
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation inside dcr_urlpatterns is inconsistent (line 61 is over-indented relative to the rest of the list). This is easy to miss in reviews and makes the file harder to scan; align the indentation consistently with the surrounding URL pattern lists.

Copilot uses AI. Check for mistakes.

urlpatterns = base_urlpatterns + management_urlpatterns + oidc_urlpatterns
urlpatterns = base_urlpatterns + management_urlpatterns + oidc_urlpatterns + dcr_urlpatterns
4 changes: 4 additions & 0 deletions oauth2_provider/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@
from .oidc import ConnectDiscoveryInfoView, JwksInfoView, RPInitiatedLogoutView, UserInfoView
from .token import AuthorizedTokenDeleteView, AuthorizedTokensListView
from .device import DeviceAuthorizationView, DeviceUserCodeView, DeviceConfirmView, DeviceGrantStatusView
from .dynamic_client_registration import (
DynamicClientRegistrationView,
DynamicClientRegistrationManagementView,
)
Loading
Loading