Skip to content

Commit 404e248

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

File tree

6 files changed

+124
-2
lines changed

6 files changed

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

646646
self.assertNotIn("prompt=login", next)
647647

648+
def test_prompt_none_unauthorized(self):
649+
"""
650+
Test response for redirect when supplied with prompt: none
651+
652+
Should redirect to redirect_uri with an error of login_required
653+
"""
654+
self.oauth2_settings.PKCE_REQUIRED = False
655+
656+
query_data = {
657+
"client_id": self.application.client_id,
658+
"response_type": "code",
659+
"state": "random_state_string",
660+
"scope": "read write",
661+
"redirect_uri": "http://example.org",
662+
"prompt": "none",
663+
}
664+
665+
response = self.client.get(reverse("oauth2_provider:authorize"), data=query_data)
666+
667+
self.assertEqual(response.status_code, 302)
668+
669+
scheme, netloc, path, params, query, fragment = urlparse(response["Location"])
670+
parsed_query = parse_qs(query)
671+
672+
self.assertIn("login_required", parsed_query["error"])
673+
self.assertIn("random_state_string", parsed_query["state"])
674+
675+
def test_prompt_none_with_error(self):
676+
"""
677+
Test response for redirect when supplied with prompt: none
678+
679+
Should redirect to redirect_uri with an error of login_required
680+
"""
681+
self.oauth2_settings.PKCE_REQUIRED = False
682+
683+
query_data = {
684+
"client_id": 'invalid',
685+
"response_type": "code",
686+
"state": "random_state_string",
687+
"scope": "read write",
688+
"redirect_uri": "http://example.org",
689+
"prompt": "none",
690+
}
691+
692+
response = self.client.get(reverse("oauth2_provider:authorize"), data=query_data)
693+
694+
self.assertEqual(response.status_code, 302)
695+
696+
scheme, netloc, path, params, query, fragment = urlparse(response["Location"])
697+
parsed_query = parse_qs(query)
698+
699+
self.assertIn("login_required", parsed_query["error"])
700+
self.assertIn("random_state_string", parsed_query["state"])
701+
648702

649703
class BaseAuthorizationCodeTokenView(BaseTest):
650704
def get_auth(self, scope="read write"):

0 commit comments

Comments
 (0)