diff options
author | Wiliam Souza <wiliamsouza83@gmail.com> | 2018-01-30 17:30:26 -0200 |
---|---|---|
committer | Omer Katz <omer.drow@gmail.com> | 2018-01-30 21:30:26 +0200 |
commit | 2fe1cdb88e076f624824496c4aba6a8665e991d9 (patch) | |
tree | 494c371a83c8d23b87d6ea97ba3933a2ca8f5cda | |
parent | d7fc1336d81b39f3d2193eb3155ff66da6caadd9 (diff) | |
download | oauthlib-2fe1cdb88e076f624824496c4aba6a8665e991d9.tar.gz |
Openid connect jwt (#488)
* Add JWT token with it the server knows how to validate this new type of token in resource requests
* Change find_token_type sorted function to reverse result and choose the valued estimated token handler
* Add validate_id_token method to RequestValidator
* Added unittest for JWTToken model
* Updated version of Mock
* Add get_jwt_bearer_token and validate_jwt_bearer_token oauthlib.oauth2.RequestValidator and change oauthlib.oauth2.tokens JWTToken to use it
* Change to improve token type estimate test
* Add a note in RequestValidator.validate_jwt_bearer_token about error 5xx rather 4xx
-rw-r--r-- | oauthlib/oauth2/rfc6749/endpoints/pre_configured.py | 7 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/endpoints/resource.py | 2 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/request_validator.py | 64 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/tokens.py | 46 | ||||
-rw-r--r-- | requirements-test.txt | 2 | ||||
-rwxr-xr-x | setup.py | 2 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/test_tokens.py | 128 |
7 files changed, 243 insertions, 8 deletions
diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 07c3715..0c26986 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -16,7 +16,7 @@ from ..grant_types import (AuthCodeGrantDispatcher, AuthorizationCodeGrant, OpenIDConnectHybrid, RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant) -from ..tokens import BearerToken +from ..tokens import BearerToken, JWTToken from .authorization import AuthorizationEndpoint from .resource import ResourceEndpoint from .revocation import RevocationEndpoint @@ -57,6 +57,9 @@ class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, bearer = BearerToken(request_validator, token_generator, token_expires_in, refresh_token_generator) + jwt = JWTToken(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) implicit_grant_choice = ImplicitTokenGrantDispatcher(default_implicit_grant=implicit_grant, oidc_implicit_grant=openid_connect_implicit) @@ -86,7 +89,7 @@ class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, }, default_token_type=bearer) ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': bearer}) + token_types={'Bearer': bearer, 'JWT': jwt}) RevocationEndpoint.__init__(self, request_validator) diff --git a/oauthlib/oauth2/rfc6749/endpoints/resource.py b/oauthlib/oauth2/rfc6749/endpoints/resource.py index d03ed21..f19c60c 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/resource.py +++ b/oauthlib/oauth2/rfc6749/endpoints/resource.py @@ -83,5 +83,5 @@ class ResourceEndpoint(BaseEndpoint): to give an estimation based on the request. """ estimates = sorted(((t.estimate_type(request), n) - for n, t in self.tokens.items())) + for n, t in self.tokens.items()), reverse=True) return estimates[0][1] if len(estimates) else None diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index ba129d5..d25a6e0 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -312,8 +312,24 @@ class RequestValidator(object): """ raise NotImplementedError('Subclasses must implement this method.') - def get_id_token(self, token, token_handler, request): + def get_jwt_bearer_token(self, token, token_handler, request): + """Get JWT Bearer token or OpenID Connect ID token + + If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` + + :param token: A Bearer token dict + :param token_handler: the token handler (BearerToken class) + :param request: the HTTP Request (oauthlib.common.Request) + :return: The JWT Bearer token or OpenID Connect ID token (a JWS signed JWT) + + Method is used by JWT Bearer and OpenID Connect tokens: + - JWTToken.create_token """ + raise NotImplementedError('Subclasses must implement this method.') + + def get_id_token(self, token, token_handler, request): + """Get OpenID Connect ID token + In the OpenID Connect workflows when an ID Token is requested this method is called. Subclasses should implement the construction, signing and optional encryption of the ID Token as described in the OpenID Connect spec. @@ -344,6 +360,52 @@ class RequestValidator(object): # the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token raise NotImplementedError('Subclasses must implement this method.') + def validate_jwt_bearer_token(self, token, scopes, request): + """Ensure the JWT Bearer token or OpenID Connect ID token are valids and authorized access to scopes. + + If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` + + If not using OpenID Connect this can `return None` to avoid 5xx rather 401/3 response. + + OpenID connect core 1.0 describe how to validate an id_token: + - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 + + :param token: Unicode Bearer token + :param scopes: List of scopes (defined by you) + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is indirectly used by all core OpenID connect JWT token issuing grant types: + - Authorization Code Grant + - Implicit Grant + - Hybrid Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_id_token(self, token, scopes, request): + """Ensure the id token is valid and authorized access to scopes. + + OpenID connect core 1.0 describe how to validate an id_token: + - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 + + :param token: Unicode Bearer token + :param scopes: List of scopes (defined by you) + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is indirectly used by all core OpenID connect JWT token issuing grant types: + - Authorization Code Grant + - Implicit Grant + - Hybrid Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + def validate_bearer_token(self, token, scopes, request): """Ensure the Bearer token is valid and authorized access to scopes. diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index e0ac431..e68ba59 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -24,8 +24,6 @@ except ImportError: from urllib.parse import urlparse - - class OAuth2Token(dict): def __init__(self, params, old_scope=None): @@ -303,3 +301,47 @@ class BearerToken(TokenBase): return 5 else: return 0 + + +class JWTToken(TokenBase): + __slots__ = ( + 'request_validator', 'token_generator', + 'refresh_token_generator', 'expires_in' + ) + + def __init__(self, request_validator=None, token_generator=None, + expires_in=None, refresh_token_generator=None): + self.request_validator = request_validator + self.token_generator = token_generator or random_token_generator + self.refresh_token_generator = ( + refresh_token_generator or self.token_generator + ) + self.expires_in = expires_in or 3600 + + def create_token(self, request, refresh_token=False, save_token=False): + """Create a JWT Token, using requestvalidator method.""" + + if callable(self.expires_in): + expires_in = self.expires_in(request) + else: + expires_in = self.expires_in + + request.expires_in = expires_in + + return self.request_validator.get_jwt_bearer_token(None, None, request) + + def validate_request(self, request): + token = None + if 'Authorization' in request.headers: + token = request.headers.get('Authorization')[7:] + else: + token = request.access_token + return self.request_validator.validate_jwt_bearer_token( + token, request.scopes, request) + + def estimate_type(self, request): + token = request.headers.get('Authorization', '')[7:] + if token.startswith('ey') and token.count('.') in (2, 4): + return 10 + else: + return 0 diff --git a/requirements-test.txt b/requirements-test.txt index e761883..5bf6e06 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -r requirements.txt coverage>=3.7.1 nose==1.3.7 -mock==1.0.1 +mock>=2.0 @@ -21,7 +21,7 @@ def fread(fn): if sys.version_info[0] == 3: tests_require = ['nose', 'cryptography', 'pyjwt>=1.0.0', 'blinker'] else: - tests_require = ['nose', 'unittest2', 'cryptography', 'mock', 'pyjwt>=1.0.0', 'blinker'] + tests_require = ['nose', 'unittest2', 'cryptography', 'mock>=2.0', 'pyjwt>=1.0.0', 'blinker'] rsa_require = ['cryptography'] signedtoken_require = ['cryptography', 'pyjwt>=1.0.0'] signals_require = ['blinker'] diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index e2e558d..570afb0 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -1,5 +1,7 @@ from __future__ import absolute_import, unicode_literals +import mock + from oauthlib.oauth2.rfc6749.tokens import * from ...unittest import TestCase @@ -80,3 +82,129 @@ class TokenTest(TestCase): self.assertEqual(prepare_bearer_headers(self.token), self.bearer_headers) self.assertEqual(prepare_bearer_body(self.token), self.bearer_body) self.assertEqual(prepare_bearer_uri(self.token, uri=self.uri), self.bearer_uri) + + +class JWTTokenTestCase(TestCase): + + def test_create_token_callable_expires_in(self): + """ + Test retrieval of the expires in value by calling the callable expires_in property + """ + + expires_in_mock = mock.MagicMock() + request_mock = mock.MagicMock() + + token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) + token.create_token(request=request_mock) + + expires_in_mock.assert_called_once_with(request_mock) + + def test_create_token_non_callable_expires_in(self): + """ + When a non callable expires in is set this should just be set to the request + """ + + expires_in_mock = mock.NonCallableMagicMock() + request_mock = mock.MagicMock() + + token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) + token.create_token(request=request_mock) + + self.assertFalse(expires_in_mock.called) + self.assertEqual(request_mock.expires_in, expires_in_mock) + + def test_create_token_calls_get_id_token(self): + """ + When create_token is called the call should be forwarded to the get_id_token on the token validator + """ + request_mock = mock.MagicMock() + + with mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + + request_validator = RequestValidatorMock() + + token = JWTToken(expires_in=mock.MagicMock(), request_validator=request_validator) + token.create_token(request=request_mock) + + request_validator.get_jwt_bearer_token.assert_called_once_with(None, None, request_mock) + + def test_validate_request_token_from_headers(self): + """ + Bearer token get retrieved from headers. + """ + + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ + mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + request_validator_mock = RequestValidatorMock() + + token = JWTToken(request_validator=request_validator_mock) + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.scopes = mock.MagicMock() + request.headers = { + 'Authorization': 'Bearer some-token-from-header' + } + + token.validate_request(request=request) + + request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-header', + request.scopes, + request) + + def test_validate_token_from_request(self): + """ + Token get retrieved from request object. + """ + + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ + mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + request_validator_mock = RequestValidatorMock() + + token = JWTToken(request_validator=request_validator_mock) + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.scopes = mock.MagicMock() + request.access_token = 'some-token-from-request-object' + request.headers = {} + + token.validate_request(request=request) + + request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-request-object', + request.scopes, + request) + + def test_estimate_type(self): + """ + Estimate type results for a jwt token + """ + + def test_token(token, expected_result): + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock: + jwt_token = JWTToken() + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.headers = { + 'Authorization': 'Bearer {}'.format(token) + } + + result = jwt_token.estimate_type(request=request) + + self.assertEqual(result, expected_result) + + test_items = ( + ('eyfoo.foo.foo', 10), + ('eyfoo.foo.foo.foo.foo', 10), + ('eyfoobar', 0) + ) + + for token, expected_result in test_items: + test_token(token, expected_result) |