Skip to content

Commit 07b3742

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 0a5ffdb commit 07b3742

File tree

8 files changed

+283
-4
lines changed

8 files changed

+283
-4
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ Peter Karman
100100
Peter McDonald
101101
Petr Dlouhý
102102
pySilver
103+
Raphael Lullis
103104
Rodney Richardson
104105
Rustem Saiargaliev
105106
Rustem Saiargaliev

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
* #1506 Support for Wildcard Origin and Redirect URIs
1111
* #1586 Turkish language support added
12+
* #1546 Support for RP-Initiated Registration
1213

1314
<!--
1415
### 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
@@ -92,6 +92,8 @@
9292
"client_secret_post",
9393
"client_secret_basic",
9494
],
95+
"OIDC_RP_INITIATED_REGISTRATION_ENABLED": False,
96+
"OIDC_RP_INITIATED_REGISTRATION_VIEW_NAME": None,
9597
"OIDC_RP_INITIATED_LOGOUT_ENABLED": False,
9698
"OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT": True,
9799
"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,12 +1,14 @@
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.contrib.auth.mixins import LoginRequiredMixin
77
from django.contrib.auth.views import redirect_to_login
8-
from django.http import HttpResponse
8+
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
99
from django.shortcuts import resolve_url
10+
from django.urls import reverse
11+
from django.urls.exceptions import NoReverseMatch
1012
from django.utils import timezone
1113
from django.utils.decorators import method_decorator
1214
from django.views.decorators.csrf import csrf_exempt
@@ -154,6 +156,8 @@ def get(self, request, *args, **kwargs):
154156
prompt = request.GET.get("prompt")
155157
if prompt == "login":
156158
return self.handle_prompt_login()
159+
elif prompt == "create":
160+
return self.handle_prompt_create()
157161

158162
all_scopes = get_scopes_backend().get_all_scopes()
159163
kwargs["scopes_descriptions"] = [all_scopes[scope] for scope in scopes]
@@ -252,13 +256,72 @@ def handle_prompt_login(self):
252256
self.get_redirect_field_name(),
253257
)
254258

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

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)