diff options
-rw-r--r-- | oauthlib/oauth2/rfc6749/endpoints/pre_configured.py | 23 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/grant_types/__init__.py | 2 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/grant_types/implicit.py | 32 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/grant_types/openid_connect.py | 65 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/request_validator.py | 24 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/endpoints/test_claims_handling.py | 2 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/endpoints/test_scope_handling.py | 2 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/grant_types/test_openid_connect.py | 114 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/test_server.py | 8 |
9 files changed, 241 insertions, 31 deletions
diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 6428b8d..07c3715 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -9,8 +9,11 @@ for consuming and providing OAuth 2.0 RFC6749. from __future__ import absolute_import, unicode_literals from ..grant_types import (AuthCodeGrantDispatcher, AuthorizationCodeGrant, - ClientCredentialsGrant, ImplicitGrant, + AuthTokenGrantDispatcher, + ClientCredentialsGrant, + ImplicitTokenGrantDispatcher, ImplicitGrant, OpenIDConnectAuthCode, OpenIDConnectImplicit, + OpenIDConnectHybrid, RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant) from ..tokens import BearerToken @@ -49,33 +52,37 @@ class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, refresh_grant = RefreshTokenGrant(request_validator) openid_connect_auth = OpenIDConnectAuthCode(request_validator) openid_connect_implicit = OpenIDConnectImplicit(request_validator) + openid_connect_hybrid = OpenIDConnectHybrid(request_validator) bearer = BearerToken(request_validator, token_generator, token_expires_in, refresh_token_generator) - auth_grant_choice = AuthCodeGrantDispatcher( default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth) + auth_grant_choice = AuthCodeGrantDispatcher(default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth) + implicit_grant_choice = ImplicitTokenGrantDispatcher(default_implicit_grant=implicit_grant, oidc_implicit_grant=openid_connect_implicit) # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination AuthorizationEndpoint.__init__(self, default_response_type='code', response_types={ 'code': auth_grant_choice, - 'token': implicit_grant, + 'token': implicit_grant_choice, 'id_token': openid_connect_implicit, 'id_token token': openid_connect_implicit, - 'code token': openid_connect_auth, - 'code id_token': openid_connect_auth, - 'code token id_token': openid_connect_auth, + 'code token': openid_connect_hybrid, + 'code id_token': openid_connect_hybrid, + 'code id_token token': openid_connect_hybrid, 'none': auth_grant }, default_token_type=bearer) + + token_grant_choice = AuthTokenGrantDispatcher(request_validator, default_token_grant=auth_grant, oidc_token_grant=openid_connect_auth) + TokenEndpoint.__init__(self, default_grant_type='authorization_code', grant_types={ - 'authorization_code': auth_grant, + 'authorization_code': token_grant_choice, 'password': password_grant, 'client_credentials': credentials_grant, 'refresh_token': refresh_grant, - 'openid': openid_connect_auth }, default_token_type=bearer) ResourceEndpoint.__init__(self, default_token='Bearer', diff --git a/oauthlib/oauth2/rfc6749/grant_types/__init__.py b/oauthlib/oauth2/rfc6749/grant_types/__init__.py index 1da1281..2e4bfe4 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/__init__.py +++ b/oauthlib/oauth2/rfc6749/grant_types/__init__.py @@ -16,3 +16,5 @@ from .openid_connect import OpenIDConnectImplicit from .openid_connect import OpenIDConnectHybrid from .openid_connect import OIDCNoPrompt from .openid_connect import AuthCodeGrantDispatcher +from .openid_connect import AuthTokenGrantDispatcher +from .openid_connect import ImplicitTokenGrantDispatcher diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index 858ef77..2b9c49d 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -11,7 +11,6 @@ from oauthlib import common from oauthlib.uri_validate import is_absolute_uri from .. import errors -from ..request_validator import RequestValidator from .base import GrantTypeBase log = logging.getLogger(__name__) @@ -229,7 +228,7 @@ class ImplicitGrant(GrantTypeBase): return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples, fragment=True)}, None, 302 - # In OIDC implicit flow it is possible to have a request_type that does not include the access token! + # In OIDC implicit flow it is possible to have a request_type that does not include the access_token! # "id_token token" - return the access token and the id token # "id_token" - don't return the access token if "token" in request.response_type.split(): @@ -239,7 +238,12 @@ class ImplicitGrant(GrantTypeBase): for modifier in self._token_modifiers: token = modifier(token, token_handler, request) - self.request_validator.save_token(token, request) + + # In OIDC implicit flow it is possible to have a request_type that does + # not include the access_token! In this case there is no need to save a token. + if "token" in request.response_type.split(): + self.request_validator.save_token(token, request) + return self.prepare_authorization_response( request, token, {}, None, 302) @@ -317,8 +321,7 @@ class ImplicitGrant(GrantTypeBase): # Then check for normal errors. request_info = self._run_custom_validators(request, - self.custom_validators.all_pre) - + self.custom_validators.all_pre) # If the resource owner denies the access request or if the request # fails for reasons other than a missing or invalid redirection URI, @@ -352,20 +355,21 @@ class ImplicitGrant(GrantTypeBase): self.validate_scopes(request) request_info.update({ - 'client_id': request.client_id, - 'redirect_uri': request.redirect_uri, - 'response_type': request.response_type, - 'state': request.state, - 'request': request, + 'client_id': request.client_id, + 'redirect_uri': request.redirect_uri, + 'response_type': request.response_type, + 'state': request.state, + 'request': request, }) - request_info = self._run_custom_validators(request, - self.custom_validators.all_post, - request_info) + request_info = self._run_custom_validators( + request, + self.custom_validators.all_post, + request_info + ) return request.scopes, request_info - def _run_custom_validators(self, request, validations, diff --git a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py b/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py index 4c98864..4371b28 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py +++ b/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py @@ -12,11 +12,11 @@ from json import loads from ..errors import ConsentRequired, InvalidRequestError, LoginRequired from ..request_validator import RequestValidator from .authorization_code import AuthorizationCodeGrant -from .base import GrantTypeBase from .implicit import ImplicitGrant log = logging.getLogger(__name__) + class OIDCNoPrompt(Exception): """Exception used to inform users that no explicit authorization is needed. @@ -76,6 +76,65 @@ class AuthCodeGrantDispatcher(object): return self._handler_for_request(request).validate_authorization_request(request) +class ImplicitTokenGrantDispatcher(object): + """ + This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope + including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. + """ + def __init__(self, default_implicit_grant=None, oidc_implicit_grant=None): + self.default_implicit_grant = default_implicit_grant + self.oidc_implicit_grant = oidc_implicit_grant + + def _handler_for_request(self, request): + handler = self.default_implicit_grant + + if request.scopes and "openid" in request.scopes and 'id_token' in request.response_type: + handler = self.oidc_implicit_grant + + log.debug('Selecting handler for request %r.', handler) + return handler + + def create_authorization_response(self, request, token_handler): + return self._handler_for_request(request).create_authorization_response(request, token_handler) + + def validate_authorization_request(self, request): + return self._handler_for_request(request).validate_authorization_request(request) + + +class AuthTokenGrantDispatcher(object): + """ + This is an adapter class that will route simple Token requests, those that authorization_code have a scope + including 'openid' to either the default_token_grant or the oidc_token_grant based on the scopes requested. + """ + def __init__(self, request_validator, default_token_grant=None, oidc_token_grant=None): + self.default_token_grant = default_token_grant + self.oidc_token_grant = oidc_token_grant + self.request_validator = request_validator + + def _handler_for_request(self, request): + handler = self.default_token_grant + scopes = () + parameters = dict(request.decoded_body) + client_id = parameters.get('client_id', None) + code = parameters.get('code', None) + redirect_uri = parameters.get('redirect_uri', None) + + # If code is not pressent fallback to `default_token_grant` wich will + # raise an error for the missing `code` in `create_token_response` step. + if code: + scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request) + + if 'openid' in scopes: + handler = self.oidc_token_grant + + log.debug('Selecting handler for request %r.', handler) + return handler + + def create_token_response(self, request, token_handler): + handler = self._handler_for_request(request) + return handler.create_token_response(request, token_handler) + + class OpenIDConnectBase(object): # Just proxy the majority of method calls through to the @@ -307,7 +366,7 @@ class OpenIDConnectBase(object): self._inflate_claims(request) if not self.request_validator.validate_user_match( - request.id_token_hint, request.scopes, request.claims, request): + request.id_token_hint, request.scopes, request.claims, request): msg = "Session user does not match client supplied user." raise LoginRequired(request=request, description=msg) @@ -356,6 +415,7 @@ class OpenIDConnectAuthCode(OpenIDConnectBase): self.openid_authorization_validator) self.register_token_modifier(self.add_id_token) + class OpenIDConnectImplicit(OpenIDConnectBase): def __init__(self, request_validator=None, **kwargs): @@ -369,6 +429,7 @@ class OpenIDConnectImplicit(OpenIDConnectBase): self.openid_implicit_authorization_validator) self.register_token_modifier(self.add_id_token) + class OpenIDConnectHybrid(OpenIDConnectBase): def __init__(self, request_validator=None, **kwargs): diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 0adfa1b..ba129d5 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -238,6 +238,30 @@ class RequestValidator(object): """ raise NotImplementedError('Subclasses must implement this method.') + def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): + """ Extracts scopes from saved authorization code. + + The scopes returned by this method is used to route token requests + based on scopes passed to Authorization Code requests. + + With that the token endpoint knows when to include OpenIDConnect + id_token in token response only based on authorization code scopes. + + Only code param should be sufficient to retrieve grant code from + any storage you are using, `client_id` and `redirect_uri` can gave a + blank value `""` don't forget to check it before using those values + in a select query if a database is used. + + :param client_id: Unicode client identifier + :param code: Unicode authorization code grant + :param redirect_uri: Unicode absolute URI + :return: A list of scope + + Method is used by: + - Authorization Token Grant Dispatcher + """ + raise NotImplementedError('Subclasses must implement this method.') + def save_token(self, token, request, *args, **kwargs): """Persist the token with a token type specific method. diff --git a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py b/tests/oauth2/rfc6749/endpoints/test_claims_handling.py index 9795c80..ff72673 100644 --- a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py +++ b/tests/oauth2/rfc6749/endpoints/test_claims_handling.py @@ -91,7 +91,7 @@ class TestClaimsHandling(TestCase): code = get_query_credentials(h['Location'])['code'][0] token_uri = 'http://example.com/path' _, body, _ = self.server.create_token_response(token_uri, - body='grant_type=authorization_code&code=%s' % code) + body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code) self.assertDictEqual(self.claims_saved_with_bearer_token, claims) diff --git a/tests/oauth2/rfc6749/endpoints/test_scope_handling.py b/tests/oauth2/rfc6749/endpoints/test_scope_handling.py index 87781b3..8490c03 100644 --- a/tests/oauth2/rfc6749/endpoints/test_scope_handling.py +++ b/tests/oauth2/rfc6749/endpoints/test_scope_handling.py @@ -87,7 +87,7 @@ class TestScopeHandling(TestCase): self.assertIn('Location', h) code = get_query_credentials(h['Location'])['code'][0] _, body, _ = getattr(self, backend_server_type).create_token_response(token_uri, - body='grant_type=authorization_code&code=%s' % code) + body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code) self.assertEqual(json.loads(body)['scope'], decoded_scope) # implicit grant diff --git a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py b/tests/oauth2/rfc6749/grant_types/test_openid_connect.py index f10d36c..573d491 100644 --- a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py +++ b/tests/oauth2/rfc6749/grant_types/test_openid_connect.py @@ -6,7 +6,11 @@ import json import mock from oauthlib.common import Request -from oauthlib.oauth2.rfc6749.grant_types import (OIDCNoPrompt, +from oauthlib.oauth2.rfc6749.grant_types import (AuthTokenGrantDispatcher, + AuthorizationCodeGrant, + ImplicitGrant, + ImplicitTokenGrantDispatcher, + OIDCNoPrompt, OpenIDConnectAuthCode, OpenIDConnectHybrid, OpenIDConnectImplicit) @@ -24,6 +28,7 @@ class OpenIDAuthCodeInterferenceTest(AuthorizationCodeGrantTest): super(OpenIDAuthCodeInterferenceTest, self).setUp() self.auth = OpenIDConnectAuthCode(request_validator=self.mock_validator) + class OpenIDImplicitInterferenceTest(ImplicitGrantTest): """Test that OpenID don't interfere with normal OAuth 2 flows.""" @@ -270,6 +275,7 @@ class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): def setUp(self): @@ -280,6 +286,7 @@ class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token + class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): def setUp(self): @@ -289,3 +296,108 @@ class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): token = 'MOCKED_TOKEN' self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + + +class ImplicitTokenGrantDispatcherTest(TestCase): + def setUp(self): + self.request = Request('http://a.b/path') + request_validator = mock.MagicMock() + implicit_grant = ImplicitGrant(request_validator) + openid_connect_implicit = OpenIDConnectImplicit(request_validator) + + self.dispatcher = ImplicitTokenGrantDispatcher( + default_implicit_grant=implicit_grant, + oidc_implicit_grant=openid_connect_implicit + ) + + def test_create_authorization_response_openid(self): + self.request.scopes = ('hello', 'openid') + self.request.response_type = 'id_token' + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, OpenIDConnectImplicit)) + + def test_validate_authorization_request_openid(self): + self.request.scopes = ('hello', 'openid') + self.request.response_type = 'id_token' + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, OpenIDConnectImplicit)) + + def test_create_authorization_response_oauth(self): + self.request.scopes = ('hello', 'world') + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + def test_validate_authorization_request_oauth(self): + self.request.scopes = ('hello', 'world') + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + +class DispatcherTest(TestCase): + def setUp(self): + self.request = Request('http://a.b/path') + self.request.decoded_body = ( + ("client_id", "me"), + ("code", "code"), + ("redirect_url", "https://a.b/cb"), + ) + + self.request_validator = mock.MagicMock() + self.auth_grant = AuthorizationCodeGrant(self.request_validator) + self.openid_connect_auth = OpenIDConnectAuthCode(self.request_validator) + + +class AuthTokenGrantDispatcherOpenIdTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOpenIdTest, self).setUp() + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') + self.dispatcher = AuthTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_openid(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, OpenIDConnectAuthCode)) + self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) + + +class AuthTokenGrantDispatcherOpenIdWithoutCodeTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOpenIdWithoutCodeTest, self).setUp() + self.request.decoded_body = ( + ("client_id", "me"), + ("code", ""), + ("redirect_url", "https://a.b/cb"), + ) + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') + self.dispatcher = AuthTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_openid_without_code(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, AuthorizationCodeGrant)) + self.assertFalse(self.dispatcher.request_validator.get_authorization_code_scopes.called) + + +class AuthTokenGrantDispatcherOAuthTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOAuthTest, self).setUp() + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'world') + self.dispatcher = AuthTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_oauth(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, AuthorizationCodeGrant)) + self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py index 305b795..da303ce 100644 --- a/tests/oauth2/rfc6749/test_server.py +++ b/tests/oauth2/rfc6749/test_server.py @@ -279,7 +279,7 @@ twIDAQAB @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): - body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' headers, body, status_code = self.endpoint.create_token_response( '', body=body) body = json.loads(body) @@ -293,7 +293,7 @@ twIDAQAB } self.assertEqual(body, token) - body = 'grant_type=authorization_code&code=abc&state=xyz' + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&state=xyz' headers, body, status_code = self.endpoint.create_token_response( '', body=body) body = json.loads(body) @@ -349,12 +349,12 @@ twIDAQAB self.assertEqual(body, token) def test_missing_type(self): - _, body, _ = self.endpoint.create_token_response('', body='') + _, body, _ = self.endpoint.create_token_response('', body='client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&code=abc') token = {'error': 'unsupported_grant_type'} self.assertEqual(json.loads(body), token) def test_invalid_type(self): - body = 'grant_type=invalid' + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=invalid&code=abc' _, body, _ = self.endpoint.create_token_response('', body=body) token = {'error': 'unsupported_grant_type'} self.assertEqual(json.loads(body), token) |