Skip to content

Commit 96616ed

Browse files
committed
Add support for RP-Initiated Registration
By adding a `prompt=create` parameter to the authorization request, the user is redirected to the OP's registration point where they can create an account, and on successful registration the user is then redirected back to the authorization view with prompt=login Closes #1546
1 parent 94dd076 commit 96616ed

File tree

8 files changed

+283
-5
lines changed

8 files changed

+283
-5
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Peter McDonald
103103
Petr Dlouhý
104104
pySilver
105105
@realsuayip
106+
Raphael Lullis
106107
Rodney Richardson
107108
Rustem Saiargaliev
108109
Rustem Saiargaliev

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
* Support for Django 5.2
1111
* Support for Python 3.14 (Django >= 5.2.8)
1212
* #1539 Add device authorization grant support
13-
13+
* #1546 Support for RP-Initiated Registration
1414

1515
<!--
1616
### Changed

docs/settings.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ this you must also provide the service at that endpoint.
353353
If unset, the default location is used, eg if ``django-oauth-toolkit`` is
354354
mounted at ``/o/``, it will be ``<server-address>/o/userinfo/``.
355355

356+
356357
OIDC_RP_INITIATED_LOGOUT_ENABLED
357358
~~~~~~~~~~~~~~~~~~~~~~~~
358359
Default: ``False``
@@ -388,6 +389,24 @@ Whether to delete the access, refresh and ID tokens of the user that is being lo
388389
The types of applications for which tokens are deleted can be customized with ``RPInitiatedLogoutView.token_types_to_delete``.
389390
The default is to delete the tokens of all applications if this flag is enabled.
390391

392+
OIDC_RP_INITIATED_REGISTRATION_ENABLED
393+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
394+
Default: ``False``
395+
396+
Whether to allow the Relying Party (RP) to direct a user to an OpenID
397+
Provider (OP) to create a new account rather than authenticate with an
398+
existing one. This is done by adding a `prompt=create` parameter to
399+
the authorization request.
400+
401+
OIDC_RP_INITIATED_REGISTRATION_VIEW_NAME
402+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
403+
Default: ''
404+
405+
The name of the view for the URL that the user will be redirected to
406+
in case RP-Initated Registration is enabled.
407+
408+
409+
391410
OIDC_ISS_ENDPOINT
392411
~~~~~~~~~~~~~~~~~
393412
Default: ``""``

oauth2_provider/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@
101101
"client_secret_post",
102102
"client_secret_basic",
103103
],
104+
"OIDC_RP_INITIATED_REGISTRATION_ENABLED": False,
105+
"OIDC_RP_INITIATED_REGISTRATION_VIEW_NAME": None,
104106
"OIDC_RP_INITIATED_LOGOUT_ENABLED": False,
105107
"OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT": True,
106108
"OIDC_RP_INITIATED_LOGOUT_STRICT_REDIRECT_URIS": False,

oauth2_provider/views/base.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import hashlib
22
import json
33
import logging
4-
from urllib.parse import parse_qsl, urlencode, urlparse
4+
from urllib.parse import parse_qsl, quote, urlencode, urlparse
55

66
from django import http
77
from django.contrib.auth.mixins import LoginRequiredMixin
88
from django.contrib.auth.views import redirect_to_login
9-
from django.http import HttpResponse
9+
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
1010
from django.shortcuts import resolve_url
11+
from django.urls import reverse
12+
from django.urls.exceptions import NoReverseMatch
1113
from django.utils import timezone
1214
from django.utils.decorators import method_decorator
1315
from django.views.decorators.csrf import csrf_exempt
@@ -158,6 +160,8 @@ def get(self, request, *args, **kwargs):
158160
prompt = request.GET.get("prompt")
159161
if prompt == "login":
160162
return self.handle_prompt_login()
163+
elif prompt == "create":
164+
return self.handle_prompt_create()
161165

162166
all_scopes = get_scopes_backend().get_all_scopes()
163167
kwargs["scopes_descriptions"] = [all_scopes[scope] for scope in scopes]
@@ -256,13 +260,72 @@ def handle_prompt_login(self):
256260
self.get_redirect_field_name(),
257261
)
258262

263+
def handle_prompt_create(self):
264+
"""
265+
When prompt=create is in the authorization request,
266+
redirect the user to the registration page. After
267+
registration, the user should be redirected back to the
268+
authorization endpoint without the prompt parameter to
269+
continue the OIDC flow.
270+
271+
Implements OpenID Connect Prompt Create 1.0 specification.
272+
https://openid.net/specs/openid-connect-prompt-create-1_0.html
273+
274+
"""
275+
try:
276+
assert not self.request.user.is_authenticated, "account_selection_required"
277+
path = self.request.build_absolute_uri()
278+
279+
views_to_attempt = [oauth2_settings.OIDC_RP_INITIATED_REGISTRATION_VIEW_NAME, "account_signup"]
280+
281+
registration_url = None
282+
for view_name in views_to_attempt:
283+
try:
284+
registration_url = reverse(view_name)
285+
continue
286+
except NoReverseMatch:
287+
pass
288+
289+
# Parse the current URL and remove the prompt parameter
290+
parsed = urlparse(path)
291+
parsed_query = dict(parse_qsl(parsed.query))
292+
parsed_query.pop("prompt")
293+
294+
# Create the next parameter to redirect back to the authorization endpoint
295+
next_url = parsed._replace(query=urlencode(parsed_query)).geturl()
296+
297+
assert oauth2_settings.OIDC_RP_INITIATED_REGISTRATION_ENABLED, "access_denied"
298+
assert registration_url is not None, "access_denied"
299+
300+
# Add next parameter to registration URL
301+
separator = "&" if "?" in registration_url else "?"
302+
redirect_to = f"{registration_url}{separator}next={quote(next_url)}"
303+
304+
return HttpResponseRedirect(redirect_to)
305+
306+
except AssertionError as exc:
307+
redirect_uri = self.request.GET.get("redirect_uri")
308+
if redirect_uri:
309+
response_parameters = {"error": str(exc)}
310+
state = self.request.GET.get("state")
311+
if state:
312+
response_parameters["state"] = state
313+
314+
separator = "&" if "?" in redirect_uri else "?"
315+
redirect_to = redirect_uri + separator + urlencode(response_parameters)
316+
return self.redirect(redirect_to, application=None)
317+
else:
318+
return HttpResponseBadRequest(str(exc))
319+
259320
def handle_no_permission(self):
260321
"""
261322
Generate response for unauthorized users.
262323
263324
If prompt is set to none, then we redirect with an error code
264325
as defined by OIDC 3.1.2.6
265326
327+
If prompt is set to create, then we redirect to the registration page.
328+
266329
Some code copied from OAuthLibMixin.error_response, but that is designed
267330
to operated on OAuth1Error from oauthlib wrapped in a OAuthToolkitError
268331
"""
@@ -280,6 +343,9 @@ def handle_no_permission(self):
280343
separator = "&" if "?" in redirect_uri else "?"
281344
redirect_to = redirect_uri + separator + urlencode(response_parameters)
282345
return self.redirect(redirect_to, application=None)
346+
elif prompt == "create":
347+
# If prompt=create and user is not authenticated, redirect to registration
348+
return self.handle_prompt_create()
283349
else:
284350
return super().handle_no_permission()
285351

oauth2_provider/views/oidc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ def get(self, request, *args, **kwargs):
100100
),
101101
"code_challenge_methods_supported": [key for key, _ in AbstractGrant.CODE_CHALLENGE_METHODS],
102102
"claims_supported": oidc_claims,
103+
"prompt_values_supported": ["none", "login"],
103104
}
105+
if oauth2_settings.OIDC_RP_INITIATED_REGISTRATION_ENABLED:
106+
data["prompt_values_supported"].append("create")
107+
104108
if oauth2_settings.OIDC_RP_INITIATED_LOGOUT_ENABLED:
105109
data["end_session_endpoint"] = end_session_endpoint
106110
response = JsonResponse(data)

tests/presets.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
OIDC_SETTINGS_RP_LOGOUT_DENY_EXPIRED["OIDC_RP_INITIATED_LOGOUT_ACCEPT_EXPIRED_TOKENS"] = False
3838
OIDC_SETTINGS_RP_LOGOUT_KEEP_TOKENS = deepcopy(OIDC_SETTINGS_RP_LOGOUT)
3939
OIDC_SETTINGS_RP_LOGOUT_KEEP_TOKENS["OIDC_RP_INITIATED_LOGOUT_DELETE_TOKENS"] = False
40+
OIDC_SETTINGS_RP_REGISTRATION = deepcopy(OIDC_SETTINGS_RW)
41+
OIDC_SETTINGS_RP_REGISTRATION["OIDC_RP_INITIATED_REGISTRATION_ENABLED"] = True
42+
OIDC_SETTINGS_RP_REGISTRATION["OIDC_RP_INITIATED_REGISTRATION_VIEW_NAME"] = "testapp:register"
43+
4044
REST_FRAMEWORK_SCOPES = {
4145
"SCOPES": {
4246
"read": "Read scope",

0 commit comments

Comments
 (0)