Dynamic Client Registration
Django OAuth Toolkit includes support for the OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591) and the OAuth 2.0 Dynamic Client Registration Management Protocol (RFC 7592).
These views are automatically available when you use
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):
{
"redirect_uris": ["https://example.com/callback"],
"grant_types": ["authorization_code"],
"client_name": "My Application",
"token_endpoint_auth_method": "client_secret_basic"
}
Response (201):
{
"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/"
}
Applications created through this endpoint are flagged with dcr_created=True on the
Application model, so dynamically registered clients can be distinguished from manually
provisioned ones — the Django admin’s application list can be filtered on this field.
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 the registration response, except
client_secret, which is only returned once on the initialPOSTsince it is hashed at rest and cannot be recovered afterward)PUT — full replacement of the client metadata (RFC 7592 section 2.2): accepts the same JSON body as POST and must include every metadata field the client wants to keep. Omitted fields are reset to their registration defaults — for example, an omitted
token_endpoint_auth_methodreverts the client to confidential and an omittedclient_nameclears the name. Read the current configuration withGETfirst, modify it, and send the complete document back.DELETE — deletes the application and all associated tokens; returns 204
Field Mapping
RFC 7591 field |
DOT Application field |
Notes |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Default |
Note
client_secret_basic and client_secret_post are both accepted at registration, since
DOT’s token endpoint authenticates confidential clients through either HTTP Basic auth or
request-body credentials. The Application model does not record which method was requested, so
per RFC 7591 section 2 (the server
“MAY replace any of the client’s requested metadata values … with suitable values”) responses
normalize the registered value to client_secret_basic; clients may nevertheless use either
method at the token endpoint.
Configuration
Add the following keys to OAUTH2_PROVIDER in your Django settings. All are optional and have
sensible defaults.
DCR_ENABLEDSet to
Trueto activate the Dynamic Client Registration endpoints. WhenFalse(the default), both endpoints return404even though the URL patterns are always registered.Default:
FalseDCR_REGISTRATION_PERMISSION_CLASSESA 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.
Note
The registration view itself is
csrf_exemptso that anonymous andAuthorization-header clients can POST to it. CSRF protection for session-cookie-authenticated requests is enforced byIsAuthenticatedDCRPermissioninstead: such requests must include a valid CSRF token or they are rejected. If you write a custom permission class that accepts Django session authentication, calloauth2_provider.dcr.enforce_csrf(request)for cookie-authenticated requests to keep the endpoint CSRF-protected.DCR_REGISTRATION_SCOPEThe scope string stored on the registration
AccessTokenused to protect the RFC 7592 management endpoints.Default:
"oauth2_provider:registration"DCR_REGISTRATION_TOKEN_EXPIRE_SECONDSNumber of seconds until the registration access token expires, or
Nonefor a far-future expiry (year 9999, effectively non-expiring).Default:
NoneDCR_ROTATE_REGISTRATION_TOKEN_ON_UPDATEWhen
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):
OAUTH2_PROVIDER = {
"DCR_ENABLED": True,
"DCR_REGISTRATION_PERMISSION_CLASSES": ("oauth2_provider.dcr.AllowAllDCRPermission",),
}
Custom permission class (e.g. initial-access token):
# myapp/permissions.py
from oauth2_provider.utils import parse_bearer_token
class InitialAccessTokenPermission:
def has_permission(self, request) -> bool:
# parse_bearer_token implements RFC 7235 / RFC 6750 semantics
# (exact, case-insensitive scheme match); None means the header
# is not a well-formed Bearer authorization.
token = parse_bearer_token(request.META.get("HTTP_AUTHORIZATION", ""))
if token is None:
return False
return MyInitialToken.objects.filter(token=token, active=True).exists()
# settings.py
OAUTH2_PROVIDER = {
"DCR_ENABLED": True,
"DCR_REGISTRATION_PERMISSION_CLASSES": ("myapp.permissions.InitialAccessTokenPermission",),
}
Smoke test with curl:
# 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}"