Skip to content

Commit 0f18817

Browse files
jesseyayn2ygk
andauthored
support prompt login (#1164)
Co-authored-by: Alan Crosswell <[email protected]> Co-authored-by: Alan Crosswell <[email protected]> Co-authored-by: Alan Crosswell <[email protected]>
1 parent c22c179 commit 0f18817

File tree

5 files changed

+79
-0
lines changed

5 files changed

+79
-0
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Hossein Shakiba
4141
Hiroki Kiyohara
4242
Jens Timmerman
4343
Jerome Leclanche
44+
Jesse Gibbs
4445
Jim Graham
4546
Jonas Nygaard Pedersen
4647
Jonathan Steffan

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
## [unreleased]
1818

1919
### Added
20+
* Support `prompt=login` for the OIDC Authorization Code Flow end user [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
2021
* Add spanish (es) translations.
2122

2223
### Changed

docs/oidc.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,15 @@ token, so you will probably want to re-use that::
359359
claims["color_scheme"] = get_color_scheme(request.user)
360360
return claims
361361

362+
Customizing the login flow
363+
==========================
364+
365+
Clients can request that the user logs in each time a request to the
366+
``/authorize`` endpoint is made during the OIDC Authorization Code Flow by
367+
adding the ``prompt=login`` query parameter and value. Only ``login`` is
368+
currently supported. See
369+
OIDC's `3.1.2.1 Authentication Request <https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_
370+
for details.
362371

363372
OIDC Views
364373
==========

oauth2_provider/views/base.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import json
22
import logging
3+
from urllib.parse import parse_qsl, urlencode, urlparse
34

45
from django.contrib.auth.mixins import LoginRequiredMixin
6+
from django.contrib.auth.views import redirect_to_login
57
from django.http import HttpResponse
8+
from django.shortcuts import resolve_url
69
from django.utils import timezone
710
from django.utils.decorators import method_decorator
811
from django.views.decorators.csrf import csrf_exempt
@@ -144,6 +147,10 @@ def get(self, request, *args, **kwargs):
144147
# Application is not available at this time.
145148
return self.error_response(error, application=None)
146149

150+
prompt = request.GET.get("prompt")
151+
if prompt == "login":
152+
return self.handle_prompt_login()
153+
147154
all_scopes = get_scopes_backend().get_all_scopes()
148155
kwargs["scopes_descriptions"] = [all_scopes[scope] for scope in scopes]
149156
kwargs["scopes"] = scopes
@@ -211,6 +218,32 @@ def get(self, request, *args, **kwargs):
211218

212219
return self.render_to_response(self.get_context_data(**kwargs))
213220

221+
def handle_prompt_login(self):
222+
path = self.request.build_absolute_uri()
223+
resolved_login_url = resolve_url(self.get_login_url())
224+
225+
# If the login url is the same scheme and net location then use the
226+
# path as the "next" url.
227+
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
228+
current_scheme, current_netloc = urlparse(path)[:2]
229+
if (not login_scheme or login_scheme == current_scheme) and (
230+
not login_netloc or login_netloc == current_netloc
231+
):
232+
path = self.request.get_full_path()
233+
234+
parsed = urlparse(path)
235+
236+
parsed_query = dict(parse_qsl(parsed.query))
237+
parsed_query.pop("prompt")
238+
239+
parsed = parsed._replace(query=urlencode(parsed_query))
240+
241+
return redirect_to_login(
242+
parsed.geturl(),
243+
resolved_login_url,
244+
self.get_redirect_field_name(),
245+
)
246+
214247

215248
@method_decorator(csrf_exempt, name="dispatch")
216249
class TokenView(OAuthLibMixin, View):

tests/test_authorization_code.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from urllib.parse import parse_qs, urlparse
66

77
import pytest
8+
from django.conf import settings
89
from django.contrib.auth import get_user_model
910
from django.test import RequestFactory, TestCase
1011
from django.urls import reverse
@@ -612,6 +613,40 @@ def test_id_token_code_post_auth_allow(self):
612613
self.assertIn("state=random_state_string", response["Location"])
613614
self.assertIn("code=", response["Location"])
614615

616+
def test_prompt_login(self):
617+
"""
618+
Test response for redirect when supplied with prompt: login
619+
"""
620+
self.oauth2_settings.PKCE_REQUIRED = False
621+
self.client.login(username="test_user", password="123456")
622+
623+
query_data = {
624+
"client_id": self.application.client_id,
625+
"response_type": "code",
626+
"state": "random_state_string",
627+
"scope": "read write",
628+
"redirect_uri": "http://example.org",
629+
"prompt": "login",
630+
}
631+
632+
response = self.client.get(reverse("oauth2_provider:authorize"), data=query_data)
633+
634+
self.assertEqual(response.status_code, 302)
635+
636+
scheme, netloc, path, params, query, fragment = urlparse(response["Location"])
637+
638+
self.assertEqual(path, settings.LOGIN_URL)
639+
640+
parsed_query = parse_qs(query)
641+
next = parsed_query["next"][0]
642+
643+
self.assertIn("redirect_uri=http%3A%2F%2Fexample.org", next)
644+
self.assertIn("state=random_state_string", next)
645+
self.assertIn("scope=read+write", next)
646+
self.assertIn(f"client_id={self.application.client_id}", next)
647+
648+
self.assertNotIn("prompt=login", next)
649+
615650

616651
class BaseAuthorizationCodeTokenView(BaseTest):
617652
def get_auth(self, scope="read write"):

0 commit comments

Comments
 (0)