Repository: oauthlib/oauthlib

Issue:
prompt=none is not handled correctly
**Describe the bug**
The behavior  for handling `prompt=none` described in the OIDC spec (https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) is:
- If `prompt=none`, return error it user isn't authenticated
- If `prompt=none + something else` then return an error.

The current implementation raises an `OIDCNoPrompt` exception whenever we get `prompt=none`, the code to properly handle the exists in https://github.com/oauthlib/oauthlib/blob/master/oauthlib/openid/connect/core/grant_types/base.py#L302

**Expected behavior**

The functions `validate_silent_login` and `validate_silent_authorization` must be called and the proper errors must be raised


Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 478c2ee..3817c52 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -16,6 +16,9 @@ OAuth2.0 Client - Bugfixes
     constructor.
   * #725: LegacyApplicationClient.prepare_request_body now correctly uses the default `scope` provided in constructor
 
+OAuth2.0 Provider - Bugfixes
+  * #756: Different prompt values are now handled according to spec
+
 3.1.0 (2019-08-06)
 ------------------
 OAuth2.0 Provider - Features
diff --git a/oauthlib/openid/connect/core/grant_types/__init__.py b/oauthlib/openid/connect/core/grant_types/__init__.py
index 768bb00..887a585 100644
--- a/oauthlib/openid/connect/core/grant_types/__init__.py
+++ b/oauthlib/openid/connect/core/grant_types/__init__.py
@@ -8,6 +8,5 @@ from .dispatchers import (
     AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher,
     ImplicitTokenGrantDispatcher,
 )
-from .exceptions import OIDCNoPrompt
 from .hybrid import HybridGrant
 from .implicit import ImplicitGrant
diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py
index d0a4812..76173e6 100644
--- a/oauthlib/openid/connect/core/grant_types/base.py
+++ b/oauthlib/openid/connect/core/grant_types/base.py
@@ -8,7 +8,6 @@ from oauthlib.oauth2.rfc6749.errors import (
     ConsentRequired, InvalidRequestError, LoginRequired,
 )
 
-from .exceptions import OIDCNoPrompt
 
 log = logging.getLogger(__name__)
 
@@ -33,13 +32,7 @@ class GrantTypeBase:
 
         :returns: (list of scopes, dict of request info)
         """
-        # If request.prompt is 'none' then no login/authorization form should
-        # be presented to the user. Instead, a silent login/authorization
-        # should be performed.
-        if request.prompt == 'none':
-            raise OIDCNoPrompt()
-        else:
-            return self.proxy_target.validate_authorization_request(request)
+        return self.proxy_target.validate_authorization_request(request)
 
     def _inflate_claims(self, request):
         # this may be called multiple times in a single request so make sure we only de-serialize the claims once
diff --git a/oauthlib/openid/connect/core/grant_types/exceptions.py b/oauthlib/openid/connect/core/grant_types/exceptions.py
deleted file mode 100644
index 4636fe7..0000000
--- a/oauthlib/openid/connect/core/grant_types/exceptions.py
+++ /dev/null
@@ -1,32 +0,0 @@
-class OIDCNoPrompt(Exception):
-    """Exception used to inform users that no explicit authorization is needed.
-
-    Normally users authorize requests after validation of the request is done.
-    Then post-authorization validation is again made and a response containing
-    an auth code or token is created. However, when OIDC clients request
-    no prompting of user authorization the final response is created directly.
-
-    Example (without the shortcut for no prompt)
-
-    scopes, req_info = endpoint.validate_authorization_request(url, ...)
-    authorization_view = create_fancy_auth_form(scopes, req_info)
-    return authorization_view
-
-    Example (with the no prompt shortcut)
-    try:
-        scopes, req_info = endpoint.validate_authorization_request(url, ...)
-        authorization_view = create_fancy_auth_form(scopes, req_info)
-        return authorization_view
-    except OIDCNoPrompt:
-        # Note: Location will be set for you
-        headers, body, status = endpoint.create_authorization_response(url, ...)
-        redirect_view = create_redirect(headers, body, status)
-        return redirect_view
-    """
-
-    def __init__(self):
-        msg = ("OIDC request for no user interaction received. Do not ask user "
-               "for authorization, it should been done using silent "
-               "authentication through create_authorization_response. "
-               "See OIDCNoPrompt.__doc__ for more details.")
-        super().__init__(msg)


Test patch:
diff --git a/tests/openid/connect/core/grant_types/test_authorization_code.py b/tests/openid/connect/core/grant_types/test_authorization_code.py
index 91e24b3..f19b5fa 100644
--- a/tests/openid/connect/core/grant_types/test_authorization_code.py
+++ b/tests/openid/connect/core/grant_types/test_authorization_code.py
@@ -3,11 +3,13 @@ import json
 from unittest import mock
 
 from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749.errors import (
+    ConsentRequired, InvalidRequestError, LoginRequired,
+)
 from oauthlib.oauth2.rfc6749.tokens import BearerToken
 from oauthlib.openid.connect.core.grant_types.authorization_code import (
     AuthorizationCodeGrant,
 )
-from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt
 
 from tests.oauth2.rfc6749.grant_types.test_authorization_code import (
     AuthorizationCodeGrantTest,
@@ -77,11 +79,7 @@ class OpenIDAuthCodeTest(TestCase):
     @mock.patch('oauthlib.common.generate_token')
     def test_no_prompt_authorization(self, generate_token):
         generate_token.return_value = 'abc'
-        scope, info = self.auth.validate_authorization_request(self.request)
         self.request.prompt = 'none'
-        self.assertRaises(OIDCNoPrompt,
-                          self.auth.validate_authorization_request,
-                          self.request)
 
         bearer = BearerToken(self.mock_validator)
 
@@ -92,7 +90,7 @@ class OpenIDAuthCodeTest(TestCase):
         self.assertIsNone(b)
         self.assertEqual(s, 302)
 
-        # Test alernative response modes
+        # Test alternative response modes
         self.request.response_mode = 'fragment'
         h, b, s = self.auth.create_authorization_response(self.request, bearer)
         self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
@@ -100,20 +98,60 @@ class OpenIDAuthCodeTest(TestCase):
         # Ensure silent authentication and authorization is done
         self.mock_validator.validate_silent_login.return_value = False
         self.mock_validator.validate_silent_authorization.return_value = True
+        self.assertRaises(LoginRequired,
+                          self.auth.validate_authorization_request,
+                          self.request)
         h, b, s = self.auth.create_authorization_response(self.request, bearer)
         self.assertIn('error=login_required', h['Location'])
 
         self.mock_validator.validate_silent_login.return_value = True
         self.mock_validator.validate_silent_authorization.return_value = False
+        self.assertRaises(ConsentRequired,
+                          self.auth.validate_authorization_request,
+                          self.request)
         h, b, s = self.auth.create_authorization_response(self.request, bearer)
         self.assertIn('error=consent_required', h['Location'])
 
         # ID token hint must match logged in user
         self.mock_validator.validate_silent_authorization.return_value = True
         self.mock_validator.validate_user_match.return_value = False
+        self.assertRaises(LoginRequired,
+                          self.auth.validate_authorization_request,
+                          self.request)
         h, b, s = self.auth.create_authorization_response(self.request, bearer)
         self.assertIn('error=login_required', h['Location'])
 
+    def test_none_multi_prompt(self):
+        bearer = BearerToken(self.mock_validator)
+
+        self.request.prompt = 'none login'
+        self.assertRaises(InvalidRequestError,
+                          self.auth.validate_authorization_request,
+                          self.request)
+        h, b, s = self.auth.create_authorization_response(self.request, bearer)
+        self.assertIn('error=invalid_request', h['Location'])
+
+        self.request.prompt = 'none consent'
+        self.assertRaises(InvalidRequestError,
+                          self.auth.validate_authorization_request,
+                          self.request)
+        h, b, s = self.auth.create_authorization_response(self.request, bearer)
+        self.assertIn('error=invalid_request', h['Location'])
+
+        self.request.prompt = 'none select_account'
+        self.assertRaises(InvalidRequestError,
+                          self.auth.validate_authorization_request,
+                          self.request)
+        h, b, s = self.auth.create_authorization_response(self.request, bearer)
+        self.assertIn('error=invalid_request', h['Location'])
+
+        self.request.prompt = 'consent none login'
+        self.assertRaises(InvalidRequestError,
+                          self.auth.validate_authorization_request,
+                          self.request)
+        h, b, s = self.auth.create_authorization_response(self.request, bearer)
+        self.assertIn('error=invalid_request', h['Location'])
+
     def set_scopes(self, client_id, code, client, request):
         request.scopes = self.request.scopes
         request.user = 'bob'
diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py
index 80069ac..6c22e90 100644
--- a/tests/openid/connect/core/grant_types/test_implicit.py
+++ b/tests/openid/connect/core/grant_types/test_implicit.py
@@ -4,7 +4,6 @@ from unittest import mock
 from oauthlib.common import Request
 from oauthlib.oauth2.rfc6749 import errors
 from oauthlib.oauth2.rfc6749.tokens import BearerToken
-from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt
 from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant
 
 from tests.oauth2.rfc6749.grant_types.test_implicit import ImplicitGrantTest
@@ -64,41 +63,79 @@ class OpenIDImplicitTest(TestCase):
     @mock.patch('oauthlib.common.generate_token')
     def test_no_prompt_authorization(self, generate_token):
         generate_token.return_value = 'abc'
-        scope, info = self.auth.validate_authorization_request(self.request)
         self.request.prompt = 'none'
-        self.assertRaises(OIDCNoPrompt,
-                          self.auth.validate_authorization_request,
-                          self.request)
 
         bearer = BearerToken(self.mock_validator)
+
+        self.request.response_mode = 'query'
         self.request.id_token_hint = 'me@email.com'
         h, b, s = self.auth.create_authorization_response(self.request, bearer)
-        self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
+        self.assertURLEqual(h['Location'], self.url_query)
         self.assertIsNone(b)
         self.assertEqual(s, 302)
 
-        # Test alernative response modes
-        self.request.response_mode = 'query'
+        # Test alternative response modes
+        self.request.response_mode = 'fragment'
         h, b, s = self.auth.create_authorization_response(self.request, bearer)
-        self.assertURLEqual(h['Location'], self.url_query)
+        self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
 
         # Ensure silent authentication and authorization is done
         self.mock_validator.validate_silent_login.return_value = False
         self.mock_validator.validate_silent_authorization.return_value = True
+        self.assertRaises(errors.LoginRequired,
+                          self.auth.validate_authorization_request,
+                          self.request)
         h, b, s = self.auth.create_authorization_response(self.request, bearer)
         self.assertIn('error=login_required', h['Location'])
 
         self.mock_validator.validate_silent_login.return_value = True
         self.mock_validator.validate_silent_authorization.return_value = False
+        self.assertRaises(errors.ConsentRequired,
+                          self.auth.validate_authorization_request,
+                          self.request)
         h, b, s = self.auth.create_authorization_response(self.request, bearer)
         self.assertIn('error=consent_required', h['Location'])
 
         # ID token hint must match logged in user
         self.mock_validator.validate_silent_authorization.return_value = True
         self.mock_validator.validate_user_match.return_value = False
+        self.assertRaises(errors.LoginRequired,
+                          self.auth.validate_authorization_request,
+                          self.request)
         h, b, s = self.auth.create_authorization_response(self.request, bearer)
         self.assertIn('error=login_required', h['Location'])
 
+    def test_none_multi_prompt(self):
+        bearer = BearerToken(self.mock_validator)
+
+        self.request.prompt = 'none login'
+        self.assertRaises(errors.InvalidRequestError,
+                          self.auth.validate_authorization_request,
+                          self.request)
+        h, b, s = self.auth.create_authorization_response(self.request, bearer)
+        self.assertIn('error=invalid_request', h['Location'])
+
+        self.request.prompt = 'none consent'
+        self.assertRaises(errors.InvalidRequestError,
+                          self.auth.validate_authorization_request,
+                          self.request)
+        h, b, s = self.auth.create_authorization_response(self.request, bearer)
+        self.assertIn('error=invalid_request', h['Location'])
+
+        self.request.prompt = 'none select_account'
+        self.assertRaises(errors.InvalidRequestError,
+                          self.auth.validate_authorization_request,
+                          self.request)
+        h, b, s = self.auth.create_authorization_response(self.request, bearer)
+        self.assertIn('error=invalid_request', h['Location'])
+
+        self.request.prompt = 'consent none login'
+        self.assertRaises(errors.InvalidRequestError,
+                          self.auth.validate_authorization_request,
+                          self.request)
+        h, b, s = self.auth.create_authorization_response(self.request, bearer)
+        self.assertIn('error=invalid_request', h['Location'])
+
     @mock.patch('oauthlib.common.generate_token')
     def test_required_nonce(self, generate_token):
         generate_token.return_value = 'abc'