diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 739fcd1a..6c375592 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,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 768bb00f..887a5850 100644 --- a/oauthlib/openid/connect/core/grant_types/__init__.py +++ b/oauthlib/openid/connect/core/grant_types/__init__.py @@ -8,6 +8,5 @@ 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 d0a48127..76173e6c 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -8,7 +8,6 @@ ConsentRequired, InvalidRequestError, LoginRequired, ) -from .exceptions import OIDCNoPrompt log = logging.getLogger(__name__) @@ -33,13 +32,7 @@ def validate_authorization_request(self, request): :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 4636fe7c..00000000 --- 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) 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 91e24b36..f19b5fa8 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 @@ 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 @@ def test_authorization(self, generate_token): @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 @@ def test_no_prompt_authorization(self, generate_token): 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 @@ def test_no_prompt_authorization(self, generate_token): # 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 80069ac1..6c22e905 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 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 @@ def test_authorization(self, generate_token): @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'