Repository: jazzband/django-oauth-toolkit

Issue:
prompt=none redirects to login screen. 
**Describe the bug**
When trying to do silent login or prompt=none login /o/authorize redirects to the login screen. 

This is because it oauth2_provider.views.AuthorizationView extends LoginRequiredMixin via BaseAuthorizationView. 


**To Reproduce**
<!-- Steps to reproduce the behavior -->
setup any OIDC silent auth enabled client, I used https://github.com/dopry/svelte-oidc, with the django oauth toolkit and enable the chrome debugger in your browser, login then reload the client. 

You will see a request to http://127.0.0.1:8000/o/authorize/... in an iframe with a 302 Location redirect to  /admin/login?next=/o/authorize/

you will also see in the console `fused to display 'http://127.0.0.1:8000/' in a frame because it set 'X-Frame-Options' to 'deny'.`

**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
per https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest


> prompt - OPTIONAL. Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. The defined values are:
> - none

>   The Authorization Server MUST NOT display any authentication or consent user interface pages. An error is returned if an End-User is not already authenticated or the Client does not have pre-configured consent for the requested Claims or does not fulfill other conditions for processing the request. The error code will typically be login_required, interaction_required, or another code defined in [Section 3.1.2.6](https://openid.net/specs/openid-connect-core-1_0.html#AuthError). This can be used as a method to check for existing authentication and/or consent.

and there should be no xframe deny error. 
 
**Version**
<!-- Version of django-oauth-toolkit -->
25f6de50d4b7287a76c87e1cf511d8f9a61c6faa


<!-- Have you tested with the latest version and/or master branch? -->
<!-- Replace '[ ]' with '[x]' to indicate that. -->
- [X] I have tested with the latest published release and it's still a problem.
- [X] I have tested with the master branch and it's still a problem.

**Additional context**
<!-- Add any other context about the problem here. -->


prompt=none redirects to login screen. 
**Describe the bug**
When trying to do silent login or prompt=none login /o/authorize redirects to the login screen. 

This is because it oauth2_provider.views.AuthorizationView extends LoginRequiredMixin via BaseAuthorizationView. 


**To Reproduce**
<!-- Steps to reproduce the behavior -->
setup any OIDC silent auth enabled client, I used https://github.com/dopry/svelte-oidc, with the django oauth toolkit and enable the chrome debugger in your browser, login then reload the client. 

You will see a request to http://127.0.0.1:8000/o/authorize/... in an iframe with a 302 Location redirect to  /admin/login?next=/o/authorize/

you will also see in the console `fused to display 'http://127.0.0.1:8000/' in a frame because it set 'X-Frame-Options' to 'deny'.`

**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
per https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest


> prompt - OPTIONAL. Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. The defined values are:
> - none

>   The Authorization Server MUST NOT display any authentication or consent user interface pages. An error is returned if an End-User is not already authenticated or the Client does not have pre-configured consent for the requested Claims or does not fulfill other conditions for processing the request. The error code will typically be login_required, interaction_required, or another code defined in [Section 3.1.2.6](https://openid.net/specs/openid-connect-core-1_0.html#AuthError). This can be used as a method to check for existing authentication and/or consent.

and there should be no xframe deny error. 
 
**Version**
<!-- Version of django-oauth-toolkit -->
25f6de50d4b7287a76c87e1cf511d8f9a61c6faa


<!-- Have you tested with the latest version and/or master branch? -->
<!-- Replace '[ ]' with '[x]' to indicate that. -->
- [X] I have tested with the latest published release and it's still a problem.
- [X] I have tested with the master branch and it's still a problem.

**Additional context**
<!-- Add any other context about the problem here. -->




Discussion:
Yes, getting the same problem.

 https://github.com/jazzband/django-oauth-toolkit/blob/d66608bb72ddcf42b69a324306388a698dee4545/oauth2_provider/views/base.py#L150C9-L152C46
```
prompt = request.GET.get("prompt")
        if prompt == "login":
            return self.handle_prompt_login()
```
There is no handling of the prompt == "none".

Also for some reason, I am unable to overwrite [Authorize view](https://django-oauth-toolkit.readthedocs.io/en/latest/advanced_topics.html#overriding-views). 
@dopry did you have any success here or workaround?
@pi3ch  I haven't had time to address this issue yet. A PR would be welcome, but this is also on my radar right now as I need it for a client project. 


Yes, getting the same problem.

 https://github.com/jazzband/django-oauth-toolkit/blob/d66608bb72ddcf42b69a324306388a698dee4545/oauth2_provider/views/base.py#L150C9-L152C46
```
prompt = request.GET.get("prompt")
        if prompt == "login":
            return self.handle_prompt_login()
```
There is no handling of the prompt == "none".

Also for some reason, I am unable to overwrite [Authorize view](https://django-oauth-toolkit.readthedocs.io/en/latest/advanced_topics.html#overriding-views). 
@dopry did you have any success here or workaround?
@pi3ch  I haven't had time to address this issue yet. A PR would be welcome, but this is also on my radar right now as I need it for a client project. 



Resolving patch:
diff --git a/oauth2_provider/views/base.py b/oauth2_provider/views/base.py
--- a/oauth2_provider/views/base.py
+++ b/oauth2_provider/views/base.py
@@ -244,6 +244,33 @@ def handle_prompt_login(self):
             self.get_redirect_field_name(),
         )
 
+    def handle_no_permission(self):
+        """
+        Generate response for unauthorized users.
+
+        If prompt is set to none, then we redirect with an error code
+        as defined by OIDC 3.1.2.6
+
+        Some code copied from OAuthLibMixin.error_response, but that is designed
+        to operated on OAuth1Error from oauthlib wrapped in a OAuthToolkitError
+        """
+        prompt = self.request.GET.get("prompt")
+        redirect_uri = self.request.GET.get("redirect_uri")
+        if prompt == "none" and redirect_uri:
+            response_parameters = {"error": "login_required"}
+
+            # REQUIRED if the Authorization Request included the state parameter.
+            # Set to the value received from the Client
+            state = self.request.GET.get("state")
+            if state:
+                response_parameters["state"] = state
+
+            separator = "&" if "?" in redirect_uri else "?"
+            redirect_to = redirect_uri + separator + urlencode(response_parameters)
+            return self.redirect(redirect_to, application=None)
+        else:
+            return super().handle_no_permission()
+
 
 @method_decorator(csrf_exempt, name="dispatch")
 class TokenView(OAuthLibMixin, View):


Test patch:
diff --git a/tests/app/idp/idp/oauth.py b/tests/app/idp/idp/oauth.py
new file mode 100644
--- /dev/null
+++ b/tests/app/idp/idp/oauth.py
@@ -0,0 +1,40 @@
+from django.conf import settings
+from django.contrib.auth.middleware import AuthenticationMiddleware
+from django.contrib.sessions.middleware import SessionMiddleware
+
+from oauth2_provider.oauth2_validators import OAuth2Validator
+
+
+# get_response is required for middlware, it doesn't need to do anything
+# the way we're using it, so we just use a lambda that returns None
+def get_response():
+    None
+
+
+class CustomOAuth2Validator(OAuth2Validator):
+    def validate_silent_login(self, request) -> None:
+        # request is an OAuthLib.common.Request and doesn't have the session
+        # or user of the django request. We will emulate the session and auth
+        # middleware here, since that is what the idp is using for auth. You
+        # may need to modify this if you are using a different session
+        # middleware or auth backend.
+
+        session_cookie_name = settings.SESSION_COOKIE_NAME
+        HTTP_COOKIE = request.headers.get("HTTP_COOKIE")
+        COOKIES = HTTP_COOKIE.split("; ")
+        for cookie in COOKIES:
+            cookie_name, cookie_value = cookie.split("=")
+            if cookie.startswith(session_cookie_name):
+                break
+        session_middleware = SessionMiddleware(get_response)
+        session = session_middleware.SessionStore(cookie_value)
+        # add session to request for compatibility with django.contrib.auth
+        request.session = session
+
+        # call the auth middleware to set request.user
+        auth_middleware = AuthenticationMiddleware(get_response)
+        auth_middleware.process_request(request)
+        return request.user.is_authenticated
+
+    def validate_silent_authorization(self, request) -> None:
+        return True
diff --git a/tests/app/idp/idp/settings.py b/tests/app/idp/idp/settings.py
--- a/tests/app/idp/idp/settings.py
+++ b/tests/app/idp/idp/settings.py
@@ -129,6 +129,7 @@
 DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
 
 OAUTH2_PROVIDER = {
+    "OAUTH2_VALIDATOR_CLASS": "idp.oauth.CustomOAuth2Validator",
     "OIDC_ENABLED": True,
     "OIDC_RP_INITIATED_LOGOUT_ENABLED": True,
     # this key is just for out test app, you should never store a key like this in a production environment.
diff --git a/tests/test_authorization_code.py b/tests/test_authorization_code.py
--- a/tests/test_authorization_code.py
+++ b/tests/test_authorization_code.py
@@ -545,6 +545,33 @@ def test_code_post_auth_fails_when_redirect_uri_path_is_invalid(self):
 
 @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW)
 class TestOIDCAuthorizationCodeView(BaseTest):
+    def test_login(self):
+        """
+        Test login page is rendered if user is not authenticated
+        """
+        self.oauth2_settings.PKCE_REQUIRED = False
+
+        query_data = {
+            "client_id": self.application.client_id,
+            "response_type": "code",
+            "state": "random_state_string",
+            "scope": "openid",
+            "redirect_uri": "http://example.org",
+        }
+        path = reverse("oauth2_provider:authorize")
+        response = self.client.get(path, data=query_data)
+        # The authorization view redirects to the login page with the
+        self.assertEqual(response.status_code, 302)
+        scheme, netloc, path, params, query, fragment = urlparse(response["Location"])
+        self.assertEqual(path, settings.LOGIN_URL)
+        parsed_query = parse_qs(query)
+        next = parsed_query["next"][0]
+        self.assertIn(f"client_id={self.application.client_id}", next)
+        self.assertIn("response_type=code", next)
+        self.assertIn("state=random_state_string", next)
+        self.assertIn("scope=openid", next)
+        self.assertIn("redirect_uri=http%3A%2F%2Fexample.org", next)
+
     def test_id_token_skip_authorization_completely(self):
         """
         If application.skip_authorization = True, should skip the authorization page.
@@ -645,6 +672,33 @@ def test_prompt_login(self):
 
         self.assertNotIn("prompt=login", next)
 
+    def test_prompt_none_unauthorized(self):
+        """
+        Test response for redirect when supplied with prompt: none
+
+        Should redirect to redirect_uri with an error of login_required
+        """
+        self.oauth2_settings.PKCE_REQUIRED = False
+
+        query_data = {
+            "client_id": self.application.client_id,
+            "response_type": "code",
+            "state": "random_state_string",
+            "scope": "read write",
+            "redirect_uri": "http://example.org",
+            "prompt": "none",
+        }
+
+        response = self.client.get(reverse("oauth2_provider:authorize"), data=query_data)
+
+        self.assertEqual(response.status_code, 302)
+
+        scheme, netloc, path, params, query, fragment = urlparse(response["Location"])
+        parsed_query = parse_qs(query)
+
+        self.assertIn("login_required", parsed_query["error"])
+        self.assertIn("random_state_string", parsed_query["state"])
+
 
 class BaseAuthorizationCodeTokenView(BaseTest):
     def get_auth(self, scope="read write"):