Skip to content

Commit 36121c5

Browse files
andyzicklerdopry
authored andcommitted
fix: prompt=none redirects to login screen
fixes #1268
1 parent a4b26b1 commit 36121c5

File tree

6 files changed

+128
-2
lines changed

6 files changed

+128
-2
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Allisson Azevedo
2323
Andrea Greco
2424
Andrej Zbín
2525
Andrew Chen Wang
26+
Andrew Zickler
2627
Antoine Laurent
2728
Anvesh Agarwal
2829
Aristóbulo Meneses

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
* #1311 Add option to disable client_secret hashing to allow verifying JWTs' signatures.
2727
* #1337 Gracefully handle expired or deleted refresh tokens, in `validate_user`.
2828
* #1350 Support Python 3.12 and Django 5.0
29-
* #1249 Add code_challenge_methods_supported property to auto discovery informations
30-
per [RFC 8414 section 2](https://www.rfc-editor.org/rfc/rfc8414.html#page-7)
29+
* #1249 Add code_challenge_methods_supported property to auto discovery informations, per [RFC 8414 section 2](https://www.rfc-editor.org/rfc/rfc8414.html#page-7)
30+
3131

3232
### Fixed
3333
* #1322 Instructions in documentation on how to create a code challenge and code verifier
3434
* #1284 Allow to logout with no id_token_hint even if the browser session already expired
3535
* #1296 Added reverse function in migration 0006_alter_application_client_secret
3636
* #1336 Fix encapsulation for Redirect URI scheme validation
3737
* #1357 Move import of setting_changed signal from test to django core modules
38+
* #1268 fix prompt=none redirects to login screen
3839

3940
### Removed
4041
* #1350 Remove support for Python 3.7 and Django 2.2

oauth2_provider/views/base.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,33 @@ def handle_prompt_login(self):
244244
self.get_redirect_field_name(),
245245
)
246246

247+
def handle_no_permission(self):
248+
"""
249+
Generate response for unauthorized users.
250+
251+
If prompt is set to none, then we redirect with an error code
252+
as defined by OIDC 3.1.2.6
253+
254+
Some code copied from OAuthLibMixin.error_response, but that is designed
255+
to operated on OAuth1Error from oauthlib wrapped in a OAuthToolkitError
256+
"""
257+
prompt = self.request.GET.get("prompt")
258+
redirect_uri = self.request.GET.get("redirect_uri")
259+
if prompt == "none" and redirect_uri:
260+
response_parameters = {"error": "login_required"}
261+
262+
# REQUIRED if the Authorization Request included the state parameter.
263+
# Set to the value received from the Client
264+
state = self.request.GET.get("state")
265+
if state:
266+
response_parameters["state"] = state
267+
268+
separator = "&" if "?" in redirect_uri else "?"
269+
redirect_to = redirect_uri + separator + urlencode(response_parameters)
270+
return self.redirect(redirect_to, application=None)
271+
else:
272+
return super().handle_no_permission()
273+
247274

248275
@method_decorator(csrf_exempt, name="dispatch")
249276
class TokenView(OAuthLibMixin, View):

tests/app/idp/idp/oauth.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django.conf import settings
2+
from django.contrib.auth.middleware import AuthenticationMiddleware
3+
from django.contrib.sessions.middleware import SessionMiddleware
4+
5+
from oauth2_provider.oauth2_validators import OAuth2Validator
6+
7+
8+
# get_response is required for middlware, it doesn't need to do anything
9+
# the way we're using it, so we just use a lambda that returns None
10+
def get_response():
11+
None
12+
13+
14+
class CustomOAuth2Validator(OAuth2Validator):
15+
def validate_silent_login(self, request) -> None:
16+
# request is an OAuthLib.common.Request and doesn't have the session
17+
# or user of the django request. We will emulate the session and auth
18+
# middleware here, since that is what the idp is using for auth. You
19+
# may need to modify this if you are using a different session
20+
# middleware or auth backend.
21+
22+
session_cookie_name = settings.SESSION_COOKIE_NAME
23+
HTTP_COOKIE = request.headers.get("HTTP_COOKIE")
24+
COOKIES = HTTP_COOKIE.split("; ")
25+
for cookie in COOKIES:
26+
cookie_name, cookie_value = cookie.split("=")
27+
if cookie.startswith(session_cookie_name):
28+
break
29+
session_middleware = SessionMiddleware(get_response)
30+
session = session_middleware.SessionStore(cookie_value)
31+
# add session to request for compatibility with django.contrib.auth
32+
request.session = session
33+
34+
# call the auth middleware to set request.user
35+
auth_middleware = AuthenticationMiddleware(get_response)
36+
auth_middleware.process_request(request)
37+
return request.user.is_authenticated
38+
39+
def validate_silent_authorization(self, request) -> None:
40+
return True

tests/app/idp/idp/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
130130

131131
OAUTH2_PROVIDER = {
132+
"OAUTH2_VALIDATOR_CLASS": "idp.oauth.CustomOAuth2Validator",
132133
"OIDC_ENABLED": True,
133134
"OIDC_RP_INITIATED_LOGOUT_ENABLED": True,
134135
# this key is just for out test app, you should never store a key like this in a production environment.

tests/test_authorization_code.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,35 @@ def test_code_post_auth_fails_when_redirect_uri_path_is_invalid(self):
545545

546546
@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW)
547547
class TestOIDCAuthorizationCodeView(BaseTest):
548+
def test_login(self):
549+
"""
550+
Test login page is rendered if user is not authenticated
551+
"""
552+
self.oauth2_settings.PKCE_REQUIRED = False
553+
554+
query_data = {
555+
"client_id": self.application.client_id,
556+
"response_type": "code",
557+
"state": "random_state_string",
558+
"scope": "openid",
559+
"redirect_uri": "http://example.org",
560+
}
561+
path = reverse("oauth2_provider:authorize")
562+
response = self.client.get(path, data=query_data)
563+
# The authorization view redirects to the login page with the
564+
self.assertEqual(response.status_code, 302)
565+
scheme, netloc, path, params, query, fragment = urlparse(response["Location"])
566+
self.assertEqual(path, settings.LOGIN_URL)
567+
parsed_query = parse_qs(query)
568+
next = parsed_query["next"][0]
569+
self.assertIn(f"client_id={self.application.client_id}", next)
570+
self.assertIn("response_type=code", next)
571+
self.assertIn("state=random_state_string", next)
572+
self.assertIn("scope=openid", next)
573+
self.assertIn("redirect_uri=http%3A%2F%2Fexample.org", next)
574+
575+
576+
548577
def test_id_token_skip_authorization_completely(self):
549578
"""
550579
If application.skip_authorization = True, should skip the authorization page.
@@ -645,6 +674,33 @@ def test_prompt_login(self):
645674

646675
self.assertNotIn("prompt=login", next)
647676

677+
def test_prompt_none_unauthorized(self):
678+
"""
679+
Test response for redirect when supplied with prompt: none
680+
681+
Should redirect to redirect_uri with an error of login_required
682+
"""
683+
self.oauth2_settings.PKCE_REQUIRED = False
684+
685+
query_data = {
686+
"client_id": self.application.client_id,
687+
"response_type": "code",
688+
"state": "random_state_string",
689+
"scope": "read write",
690+
"redirect_uri": "http://example.org",
691+
"prompt": "none",
692+
}
693+
694+
response = self.client.get(reverse("oauth2_provider:authorize"), data=query_data)
695+
696+
self.assertEqual(response.status_code, 302)
697+
698+
scheme, netloc, path, params, query, fragment = urlparse(response["Location"])
699+
parsed_query = parse_qs(query)
700+
701+
self.assertIn("login_required", parsed_query["error"])
702+
self.assertIn("random_state_string", parsed_query["state"])
703+
648704

649705
class BaseAuthorizationCodeTokenView(BaseTest):
650706
def get_auth(self, scope="read write"):

0 commit comments

Comments
 (0)