From e575cca3e5d18b1e7051c64f435f2cdea71a29ab Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sun, 1 Oct 2017 03:07:11 -0300 Subject: OpenID connect improvements (#484) * Change create_token_response to only save access_token when it's present in request.response_type * Remove unused import, fix indentation and improve comment * Fix AuthorizationEndpoint response_type for OpenID Connect hybrid flow * Add new ImplicitTokenGrantDispatcher Changes AuthorizationEndpoint response_type `'token'`, `'id_token'` and `'id_token token'` to work with OpenID Connect and OAuth2 implicit flow in a transparent way * Add new AuthTokenGrantDispatcher Change AuthorizationEndpoint grant_types `'authorization_code'` to work with OpenID Connect and OAuth2 authorization flow in a transparent way * Change tests to include required client_id and redirect_uri * Remove AuthorizationEndpoint grant_types `'openid'` Now OpenID Connect and OAuth2 authorization flow can use `authorization_code` in a transparent way * Add sone blank lines and fix indentation * Change AuthorizationEndpoint grant type id_token and id_token token to use openid_connect_implicit direct * Change default empty value to None and fix a typo * Add assert called to AuthTokenGrantDispatcher tests * Add request to get_authorization_code_scopes --- .../oauth2/rfc6749/endpoints/pre_configured.py | 23 +++-- oauthlib/oauth2/rfc6749/grant_types/__init__.py | 2 + oauthlib/oauth2/rfc6749/grant_types/implicit.py | 32 +++--- .../oauth2/rfc6749/grant_types/openid_connect.py | 65 +++++++++++- oauthlib/oauth2/rfc6749/request_validator.py | 24 +++++ .../rfc6749/endpoints/test_claims_handling.py | 2 +- .../rfc6749/endpoints/test_scope_handling.py | 2 +- .../rfc6749/grant_types/test_openid_connect.py | 114 ++++++++++++++++++++- 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) -- cgit v1.2.1 From c6b11373648af4b81367b0424b65b15ee8b58261 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 18 Oct 2017 23:40:32 +0900 Subject: Refactor OAuth2Error --- oauthlib/oauth2/rfc6749/errors.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index e0c29a0..180f636 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -18,8 +18,8 @@ class OAuth2Error(Exception): status_code = 400 description = '' - def __init__(self, description=None, uri=None, state=None, status_code=None, - request=None): + def __init__(self, description=None, uri=None, state=None, + status_code=None, request=None): """ description: A human-readable ASCII [USASCII] text providing additional information, used to assist the client @@ -39,8 +39,9 @@ class OAuth2Error(Exception): request: Oauthlib Request object """ - self.response_mode = None - self.description = description or self.description + if description is not None: + self.description = description + message = '(%s) %s' % (self.error, self.description) if request: message += ' ' + repr(request) @@ -61,10 +62,17 @@ class OAuth2Error(Exception): self.grant_type = request.grant_type if not state: self.state = request.state + else: + self.redirect_uri = None + self.client_id = None + self.scopes = None + self.response_type = None + self.response_mode = None + self.grant_type = None def in_uri(self, uri): - return add_params_to_uri(uri, self.twotuples, - fragment=self.response_mode == "fragment") + fragment = self.response_mode == "fragment" + return add_params_to_uri(uri, self.twotuples, fragment) @property def twotuples(self): -- cgit v1.2.1 From c1eda9f1c92f7110ad41af5e48864bfb2ac37c7c Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 18 Oct 2017 23:57:51 +0900 Subject: Fix travis for PyPy This is due to https://github.com/pyca/cryptography/pull/3970 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b66a576..ee88674 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: - python: pypy-5.3 env: TOXENV=pypy -install: pip install tox coveralls +install: pip install cryptography==2.0.3 tox coveralls script: tox after_success: coveralls -- cgit v1.2.1 From 1dcea0406affbcbf28e6e8f4a5307adf6bca46d1 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 19 Oct 2017 00:09:25 +0900 Subject: Try another way to fix travis --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ee88674..4dee48b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,9 @@ matrix: - python: pypy-5.3 env: TOXENV=pypy -install: pip install cryptography==2.0.3 tox coveralls +install: + - pip install -U setuptools + - pip install tox coveralls script: tox after_success: coveralls -- cgit v1.2.1 From fb7ec207b17e0cacf52f9f3c2643c4b9036d827c Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 19 Oct 2017 00:27:41 +0900 Subject: Version bump 2.0.5 --- CHANGELOG.rst | 6 ++++++ oauthlib/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 397fc07..8a20f92 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Changelog ========= +2.0.5 (2017-10-19) +------------------ + +* Fix OAuth2Error.response_mode for #463. +* Documentation improvement. + 2.0.4 (2017-09-17) ------------------ * Fixed typo that caused OAuthlib to crash because of the fix in "Address missing OIDC errors and fix a typo in the AccountSelectionRequired exception". diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 9121582..459f307 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -10,7 +10,7 @@ """ __author__ = 'Idan Gazit ' -__version__ = '2.0.4' +__version__ = '2.0.5' import logging -- cgit v1.2.1 From 14215244249f2d8df73ec47dbab5db1efd0fc2f4 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 24 Oct 2017 06:14:47 -0700 Subject: Include license file in the generated wheel package (#494) The wheel package format supports including the license file. This is done using the [metadata] section in the setup.cfg file. For additional information on this feature, see: https://wheel.readthedocs.io/en/stable/index.html#including-the-license-in-the-generated-wheel-file --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 2a9acf1..ed8a958 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,5 @@ [bdist_wheel] universal = 1 + +[metadata] +license_file = LICENSE -- cgit v1.2.1 From 4b85d90a54572f54b8b19d036d76d043cf116699 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 27 Oct 2017 08:56:28 -0700 Subject: When deploying a release to PyPI, include the wheel distribution (#496) For Travis CI documentation on including a bdist_wheel distribution, see: https://docs.travis-ci.com/user/deployment/pypi/#Uploading-different-distributions Fixes #493 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 4dee48b..f5e9aca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ deploy: user: ib.lundgren password: secure: PGZF9pRiTGCSwQjk1ddTKF3x4rQ0iAiPbg2uSixyO68uMXRgJjwHhSrNM0OEqtK5YWU5FE5L0DwR1nkrpEJKO4a5q2EOgos+gVoKpJfinoUNOOkjc1VHpqKM0uRf/OKrw1alvWUwqvW8B+DOb9TY5c5VZxQuRL+iwdrtwzFlKls= + distributions: sdist bdist_wheel on: tags: true repo: idan/oauthlib -- cgit v1.2.1 From fa0b63cfaced831d8b916c5a125128f582acf044 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Tue, 14 Nov 2017 23:38:33 +0800 Subject: Check access token in self.token dict (#500) * Check access token in self.token dict * fix typo --- oauthlib/oauth2/rfc6749/clients/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index c2f8809..5c5acee 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -186,7 +186,7 @@ class Client(object): if not self.token_type.lower() in case_insensitive_token_types: raise ValueError("Unsupported token type: %s" % self.token_type) - if not self.access_token: + if not (self.access_token or self.token.get('access_token')): raise ValueError("Missing access token.") if self._expires_at and self._expires_at < time.time(): -- cgit v1.2.1 From cfb82feb03fcd60b3b66ac09bf1b478cd5f11b7d Mon Sep 17 00:00:00 2001 From: Viktor Haag Date: Tue, 14 Nov 2017 07:44:44 -0800 Subject: Add support for HMAC-SHA256 (builds on PR#388) (#498) * Add support for HMAC-SHA256 * Add explicit declaration of HMAC-SHA1 and point HMAC at it To avoid confusion, HMAC constant name should explicitly state which SHA variant is used, but for backwards compatibility, SIGNATURE_HMAC is still needed * add support for HMAC-SHA256 including tests and comments * constructor tests verify client built with correct signer method --- oauthlib/oauth1/__init__.py | 2 +- oauthlib/oauth1/rfc5849/__init__.py | 11 ++++--- oauthlib/oauth1/rfc5849/signature.py | 57 ++++++++++++++++++++++++++++++++++++ tests/oauth1/rfc5849/test_client.py | 40 +++++++++++++++++++++++-- 4 files changed, 103 insertions(+), 7 deletions(-) diff --git a/oauthlib/oauth1/__init__.py b/oauthlib/oauth1/__init__.py index f9dff74..dc908d4 100644 --- a/oauthlib/oauth1/__init__.py +++ b/oauthlib/oauth1/__init__.py @@ -9,7 +9,7 @@ and Server classes. from __future__ import absolute_import, unicode_literals from .rfc5849 import Client -from .rfc5849 import SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT +from .rfc5849 import SIGNATURE_HMAC, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_PLAINTEXT from .rfc5849 import SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_QUERY from .rfc5849 import SIGNATURE_TYPE_BODY from .rfc5849.request_validator import RequestValidator diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py index 06902e2..f9113ab 100644 --- a/oauthlib/oauth1/rfc5849/__init__.py +++ b/oauthlib/oauth1/rfc5849/__init__.py @@ -27,10 +27,12 @@ from oauthlib.common import Request, urlencode, generate_nonce from oauthlib.common import generate_timestamp, to_unicode from . import parameters, signature -SIGNATURE_HMAC = "HMAC-SHA1" +SIGNATURE_HMAC_SHA1 = "HMAC-SHA1" +SIGNATURE_HMAC_SHA256 = "HMAC-SHA256" +SIGNATURE_HMAC = SIGNATURE_HMAC_SHA1 SIGNATURE_RSA = "RSA-SHA1" SIGNATURE_PLAINTEXT = "PLAINTEXT" -SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT) +SIGNATURE_METHODS = (SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_PLAINTEXT) SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER' SIGNATURE_TYPE_QUERY = 'QUERY' @@ -43,7 +45,8 @@ class Client(object): """A client used to sign OAuth 1.0 RFC 5849 requests.""" SIGNATURE_METHODS = { - SIGNATURE_HMAC: signature.sign_hmac_sha1_with_client, + SIGNATURE_HMAC_SHA1: signature.sign_hmac_sha1_with_client, + SIGNATURE_HMAC_SHA256: signature.sign_hmac_sha256_with_client, SIGNATURE_RSA: signature.sign_rsa_sha1_with_client, SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client } @@ -57,7 +60,7 @@ class Client(object): resource_owner_key=None, resource_owner_secret=None, callback_uri=None, - signature_method=SIGNATURE_HMAC, + signature_method=SIGNATURE_HMAC_SHA1, signature_type=SIGNATURE_TYPE_AUTH_HEADER, rsa_key=None, verifier=None, realm=None, encoding='utf-8', decoding=None, diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index 10d057f..30001ef 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -469,6 +469,63 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret): # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8 return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8') + +def sign_hmac_sha256_with_client(base_string, client): + return sign_hmac_sha256(base_string, + client.client_secret, + client.resource_owner_secret + ) + + +def sign_hmac_sha256(base_string, client_secret, resource_owner_secret): + """**HMAC-SHA256** + + The "HMAC-SHA256" signature method uses the HMAC-SHA256 signature + algorithm as defined in `RFC4634`_:: + + digest = HMAC-SHA256 (key, text) + + Per `section 3.4.2`_ of the spec. + + .. _`RFC4634`: http://tools.ietf.org/html/rfc4634 + .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2 + """ + + # The HMAC-SHA256 function variables are used in following way: + + # text is set to the value of the signature base string from + # `Section 3.4.1.1`_. + # + # .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1 + text = base_string + + # key is set to the concatenated values of: + # 1. The client shared-secret, after being encoded (`Section 3.6`_). + # + # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + key = utils.escape(client_secret or '') + + # 2. An "&" character (ASCII code 38), which MUST be included + # even when either secret is empty. + key += '&' + + # 3. The token shared-secret, after being encoded (`Section 3.6`_). + # + # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + key += utils.escape(resource_owner_secret or '') + + # FIXME: HMAC does not support unicode! + key_utf8 = key.encode('utf-8') + text_utf8 = text.encode('utf-8') + signature = hmac.new(key_utf8, text_utf8, hashlib.sha256) + + # digest is used to set the value of the "oauth_signature" protocol + # parameter, after the result octet string is base64-encoded + # per `RFC2045, Section 6.8`. + # + # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8 + return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8') + _jwtrs1 = None #jwt has some nice pycrypto/cryptography abstractions diff --git a/tests/oauth1/rfc5849/test_client.py b/tests/oauth1/rfc5849/test_client.py index dcb4c3d..777efc2 100644 --- a/tests/oauth1/rfc5849/test_client.py +++ b/tests/oauth1/rfc5849/test_client.py @@ -2,7 +2,8 @@ from __future__ import absolute_import, unicode_literals from oauthlib.common import Request -from oauthlib.oauth1 import (SIGNATURE_PLAINTEXT, SIGNATURE_RSA, +from oauthlib.oauth1 import (SIGNATURE_PLAINTEXT, SIGNATURE_HMAC_SHA1, + SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY) from oauthlib.oauth1.rfc5849 import Client, bytes_type @@ -62,13 +63,48 @@ class ClientConstructorTests(TestCase): self.assertIsInstance(k, bytes_type) self.assertIsInstance(v, bytes_type) + def test_hmac_sha1(self): + client = Client('client_key') + # instance is using the correct signer method + self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_HMAC_SHA1], + client.SIGNATURE_METHODS[client.signature_method]) + + def test_hmac_sha256(self): + client = Client('client_key', signature_method=SIGNATURE_HMAC_SHA256) + # instance is using the correct signer method + self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_HMAC_SHA256], + client.SIGNATURE_METHODS[client.signature_method]) + def test_rsa(self): client = Client('client_key', signature_method=SIGNATURE_RSA) - self.assertIsNone(client.rsa_key) # don't need an RSA key to instantiate + # instance is using the correct signer method + self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_RSA], + client.SIGNATURE_METHODS[client.signature_method]) + # don't need an RSA key to instantiate + self.assertIsNone(client.rsa_key) class SignatureMethodTest(TestCase): + def test_hmac_sha1_method(self): + client = Client('client_key', timestamp='1234567890', nonce='abc') + u, h, b = client.sign('http://example.com') + correct = ('OAuth oauth_nonce="abc", oauth_timestamp="1234567890", ' + 'oauth_version="1.0", oauth_signature_method="HMAC-SHA1", ' + 'oauth_consumer_key="client_key", ' + 'oauth_signature="hH5BWYVqo7QI4EmPBUUe9owRUUQ%3D"') + self.assertEqual(h['Authorization'], correct) + + def test_hmac_sha256_method(self): + client = Client('client_key', signature_method=SIGNATURE_HMAC_SHA256, + timestamp='1234567890', nonce='abc') + u, h, b = client.sign('http://example.com') + correct = ('OAuth oauth_nonce="abc", oauth_timestamp="1234567890", ' + 'oauth_version="1.0", oauth_signature_method="HMAC-SHA256", ' + 'oauth_consumer_key="client_key", ' + 'oauth_signature="JzgJWBxX664OiMW3WE4MEjtYwOjI%2FpaUWHqtdHe68Es%3D"') + self.assertEqual(h['Authorization'], correct) + def test_rsa_method(self): private_key = ( "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDk1/bxy" -- cgit v1.2.1 From 9b95e4e8f094d78abe577203ad1ef53aecfdb270 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 8 Nov 2017 09:55:03 +0100 Subject: Added initial introspect support --- docs/feature_matrix.rst | 1 + docs/oauth2/endpoints/endpoints.rst | 6 +- docs/oauth2/endpoints/introspect.rst | 26 ++++ oauthlib/oauth2/__init__.py | 1 + oauthlib/oauth2/rfc6749/endpoints/__init__.py | 1 + oauthlib/oauth2/rfc6749/endpoints/introspect.py | 135 +++++++++++++++++++++ .../oauth2/rfc6749/endpoints/pre_configured.py | 27 +++-- oauthlib/oauth2/rfc6749/errors.py | 2 +- oauthlib/oauth2/rfc6749/request_validator.py | 20 +++ .../rfc6749/endpoints/test_introspect_endpoint.py | 132 ++++++++++++++++++++ 10 files changed, 339 insertions(+), 12 deletions(-) create mode 100644 docs/oauth2/endpoints/introspect.rst create mode 100644 oauthlib/oauth2/rfc6749/endpoints/introspect.py create mode 100644 tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py diff --git a/docs/feature_matrix.rst b/docs/feature_matrix.rst index 0f9021d..59f3f3a 100644 --- a/docs/feature_matrix.rst +++ b/docs/feature_matrix.rst @@ -17,6 +17,7 @@ OAuth 2 client and provider support for - Bearer Tokens - Draft MAC tokens - Token Revocation +- Token Introspection - OpenID Connect Authentication with support for SAML2 and JWT tokens, dynamic client registration and more to diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst index 0e70798..5f7ae8c 100644 --- a/docs/oauth2/endpoints/endpoints.rst +++ b/docs/oauth2/endpoints/endpoints.rst @@ -14,11 +14,12 @@ client attempts to access the user resources on their behalf. :maxdepth: 2 authorization + introspect token resource revocation -There are three different endpoints, the authorization endpoint which mainly +There are three main endpoints, the authorization endpoint which mainly handles user authorization, the token endpoint which provides tokens and the resource endpoint which provides access to protected resources. It is to the endpoints you will feed requests and get back an almost complete response. This @@ -27,3 +28,6 @@ later. The main purpose of the endpoint in OAuthLib is to figure out which grant type or token to dispatch the request to. + +Then, you can extend your OAuth implementation by proposing introspect or +revocation endpoints. diff --git a/docs/oauth2/endpoints/introspect.rst b/docs/oauth2/endpoints/introspect.rst new file mode 100644 index 0000000..53ade8b --- /dev/null +++ b/docs/oauth2/endpoints/introspect.rst @@ -0,0 +1,26 @@ +=================== +Token introspection +=================== + +Introspect endpoints read opaque access and/or refresh tokens upon client +request. Also known as tokeninfo. + +.. code-block:: python + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Token revocation + uri = 'https://example.com/introspect' + headers, body, http_method = {}, 'token=sldafh309sdf', 'POST' + + headers, body, status = server.create_introspect_response(uri, + headers=headers, body=body, http_method=http_method) + + from your_framework import http_response + http_response(body, status=status, headers=headers) + + +.. autoclass:: oauthlib.oauth2.IntrospectEndpoint + :members: diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index c8d934e..dc7b431 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -15,6 +15,7 @@ from .rfc6749.clients import LegacyApplicationClient from .rfc6749.clients import BackendApplicationClient from .rfc6749.clients import ServiceApplicationClient from .rfc6749.endpoints import AuthorizationEndpoint +from .rfc6749.endpoints import IntrospectEndpoint from .rfc6749.endpoints import TokenEndpoint from .rfc6749.endpoints import ResourceEndpoint from .rfc6749.endpoints import RevocationEndpoint diff --git a/oauthlib/oauth2/rfc6749/endpoints/__init__.py b/oauthlib/oauth2/rfc6749/endpoints/__init__.py index 848bec6..9557f92 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/__init__.py +++ b/oauthlib/oauth2/rfc6749/endpoints/__init__.py @@ -9,6 +9,7 @@ for consuming and providing OAuth 2.0 RFC6749. from __future__ import absolute_import, unicode_literals from .authorization import AuthorizationEndpoint +from .introspect import IntrospectEndpoint from .token import TokenEndpoint from .resource import ResourceEndpoint from .revocation import RevocationEndpoint diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py new file mode 100644 index 0000000..7613acc --- /dev/null +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.endpoint.introspect +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An implementation of the OAuth 2.0 `Token Introspection`. + +.. _`Token Introspection`: https://tools.ietf.org/html/rfc7662 +""" +from __future__ import absolute_import, unicode_literals + +import json +import logging + +from oauthlib.common import Request + +from ..errors import (InvalidClientError, InvalidRequestError, OAuth2Error, + UnsupportedTokenTypeError) +from .base import BaseEndpoint, catch_errors_and_unavailability + +log = logging.getLogger(__name__) + + +class IntrospectEndpoint(BaseEndpoint): + + """Introspect token endpoint. + + This endpoint defines a method to query an OAuth 2.0 authorization + server to determine the active state of an OAuth 2.0 token and to + determine meta-information about this token. OAuth 2.0 deployments + can use this method to convey information about the authorization + context of the token from the authorization server to the protected + resource. + + To prevent the values of access tokens from leaking into + server-side logs via query parameters, an authorization server + offering token introspection MAY disallow the use of HTTP GET on + the introspection endpoint and instead require the HTTP POST method + to be used at the introspection endpoint. + """ + + valid_token_types = ('access_token', 'refresh_token') + + def __init__(self, request_validator, supported_token_types=None): + BaseEndpoint.__init__(self) + self.request_validator = request_validator + self.supported_token_types = ( + supported_token_types or self.valid_token_types) + + @catch_errors_and_unavailability + def create_introspect_response(self, uri, http_method='POST', body=None, + headers=None): + """Create introspect valid or invalid response + + If the authorization server is unable to determine the state + of the token without additional information, it SHOULD return + an introspection response indicating the token is not active + as described in Section 2.2. + """ + request = Request(uri, http_method, body, headers) + try: + self.validate_introspect_request(request) + log.debug('Token introspect valid for %r.', request) + except OAuth2Error as e: + log.debug('Client error during validation of %r. %r.', request, e) + return {}, e.json, e.status_code + + claims = self.request_validator.introspect_token( + request.token, + request.token_type_hint, + request + ) + headers = { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } + if claims is None: + return headers, json.dumps(dict(active=False)), 200 + if "active" in claims: + claims.pop("active") + return headers, json.dumps(dict(active=True, **claims)), 200 + + def validate_introspect_request(self, request): + """Ensure the request is valid. + + The protected resource calls the introspection endpoint using + an HTTP POST request with parameters sent as + "application/x-www-form-urlencoded". + + token REQUIRED. The string value of the token. + + token_type_hint OPTIONAL. + A hint about the type of the token submitted for + introspection. The protected resource MAY pass this parameter to + help the authorization server optimize the token lookup. If the + server is unable to locate the token using the given hint, it MUST + extend its search across all of its supported token types. An + authorization server MAY ignore this parameter, particularly if it + is able to detect the token type automatically. + * access_token: An Access Token as defined in [`RFC6749`], + `section 1.4`_ + + * refresh_token: A Refresh Token as defined in [`RFC6749`], + `section 1.5`_ + + The introspection endpoint MAY accept other OPTIONAL + parameters to provide further context to the query. For + instance, an authorization server may desire to know the IP + address of the client accessing the protected resource to + determine if the correct client is likely to be presenting the + token. The definition of this or any other parameters are + outside the scope of this specification, to be defined by + service documentation or extensions to this specification. + + .. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4 + .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5 + .. _`RFC6749`: http://tools.ietf.org/html/rfc6749 + """ + if not request.token: + raise InvalidRequestError(request=request, + description='Missing token parameter.') + + if self.request_validator.client_authentication_required(request): + if not self.request_validator.authenticate_client(request): + log.debug('Client authentication failed, %r.', request) + raise InvalidClientError(request=request) + elif not self.request_validator.authenticate_client_id(request.client_id, request): + log.debug('Client authentication failed, %r.', request) + raise InvalidClientError(request=request) + + if (request.token_type_hint and + request.token_type_hint in self.valid_token_types and + request.token_type_hint not in self.supported_token_types): + raise UnsupportedTokenTypeError(request=request) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 07c3715..f1dfead 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -18,13 +18,14 @@ from ..grant_types import (AuthCodeGrantDispatcher, AuthorizationCodeGrant, ResourceOwnerPasswordCredentialsGrant) from ..tokens import BearerToken from .authorization import AuthorizationEndpoint +from .introspect import IntrospectEndpoint from .resource import ResourceEndpoint from .revocation import RevocationEndpoint from .token import TokenEndpoint -class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring all four major grant types.""" @@ -88,10 +89,11 @@ class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) -class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" @@ -126,10 +128,11 @@ class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoin ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) -class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint, - RevocationEndpoint): +class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Implicit code grant and Bearer tokens.""" @@ -159,10 +162,11 @@ class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint, token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator, supported_token_types=['access_token']) + IntrospectEndpoint.__init__(self, request_validator) -class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens.""" @@ -195,10 +199,11 @@ class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint, ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) -class BackendApplicationServer(TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Client Credentials grant and Bearer tokens.""" @@ -228,3 +233,5 @@ class BackendApplicationServer(TokenEndpoint, ResourceEndpoint, token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator, supported_token_types=['access_token']) + IntrospectEndpoint.__init__(self, request_validator, + supported_token_types=['access_token']) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 180f636..1d5e98d 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -267,7 +267,7 @@ class UnsupportedGrantTypeError(OAuth2Error): class UnsupportedTokenTypeError(OAuth2Error): """ - The authorization server does not support the revocation of the + The authorization server does not support the hint of the presented token type. I.e. the client tried to revoke an access token on a server not supporting this feature. """ diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index ba129d5..525ba33 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -166,6 +166,26 @@ class RequestValidator(object): """ return False + def introspect_token(self, token, token_type_hint, request, *args, **kwargs): + """Introspect an access or refresh token. + + Called once introspect token request is validated. This method + should return a dictionary with any desired claims associated + with the *token*. The implementation can use *token_type_hint* + to lookup this type first, but then it must fallback to other + types known, to be compliant with RFC. + + The dict of claims is added to request.token after this method. + + :param token: The token string. + :param token_type_hint: access_token or refresh_token. + :param request: The HTTP Request (oauthlib.common.Request) + + Method is used by: + - Introspect Endpoint (all grants are compatible) + """ + raise NotImplementedError('Subclasses must implement this method.') + def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): """Invalidate an authorization code after use. diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py new file mode 100644 index 0000000..7ec8190 --- /dev/null +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +from json import loads + +from mock import MagicMock + +from oauthlib.common import urlencode +from oauthlib.oauth2 import RequestValidator, IntrospectEndpoint + +from ....unittest import TestCase + + +class IntrospectEndpointTest(TestCase): + + def setUp(self): + self.validator = MagicMock(wraps=RequestValidator()) + self.validator.client_authentication_required.return_value = True + self.validator.authenticate_client.return_value = True + self.validator.validate_bearer_token.return_value = True + self.validator.introspect_token.return_value = {} + self.endpoint = IntrospectEndpoint(self.validator) + + self.uri = 'should_not_matter' + self.headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + self.resp_h = { + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json', + 'Pragma': 'no-cache' + } + self.resp_b = { + "active": True + } + + def test_introspect_token(self): + for token_type in ('access_token', 'refresh_token', 'invalid'): + body = urlencode([('token', 'foo'), + ('token_type_hint', token_type)]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), self.resp_b) + self.assertEqual(s, 200) + + def test_introspect_token_nohint(self): + # don't specify token_type_hint + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), self.resp_b) + self.assertEqual(s, 200) + + def test_introspect_token_false(self): + self.validator.introspect_token.return_value = None + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), {"active": False}) + self.assertEqual(s, 200) + + def test_introspect_token_claims(self): + self.validator.introspect_token.return_value = {"foo": "bar"} + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), {"active": True, "foo": "bar"}) + self.assertEqual(s, 200) + + def test_introspect_token_claims_spoof_active(self): + self.validator.introspect_token.return_value = {"foo": "bar", "active": False} + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), {"active": True, "foo": "bar"}) + self.assertEqual(s, 200) + + def test_introspect_token_client_authentication_failed(self): + self.validator.authenticate_client.return_value = False + body = urlencode([('token', 'foo'), + ('token_type_hint', 'access_token')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'invalid_client') + self.assertEqual(s, 401) + + def test_introspect_token_public_client_authentication(self): + self.validator.client_authentication_required.return_value = False + self.validator.authenticate_client_id.return_value = True + for token_type in ('access_token', 'refresh_token', 'invalid'): + body = urlencode([('token', 'foo'), + ('token_type_hint', token_type)]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), self.resp_b) + self.assertEqual(s, 200) + + def test_introspect_token_public_client_authentication_failed(self): + self.validator.client_authentication_required.return_value = False + self.validator.authenticate_client_id.return_value = False + body = urlencode([('token', 'foo'), + ('token_type_hint', 'access_token')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'invalid_client') + self.assertEqual(s, 401) + + + def test_introspect_unsupported_token(self): + endpoint = IntrospectEndpoint(self.validator, + supported_token_types=['access_token']) + body = urlencode([('token', 'foo'), + ('token_type_hint', 'refresh_token')]) + h, b, s = endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'unsupported_token_type') + self.assertEqual(s, 400) + + h, b, s = endpoint.create_introspect_response(self.uri, + headers=self.headers, body='') + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'invalid_request') + self.assertEqual(s, 400) -- cgit v1.2.1 From ef8a3b47305b23b278310c1f21106c677a748434 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 19 Dec 2017 15:18:48 +0100 Subject: Added default supported_token_types for Mobile --- oauthlib/oauth2/rfc6749/endpoints/pre_configured.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index f1dfead..378339a 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -162,7 +162,8 @@ class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator, supported_token_types=['access_token']) - IntrospectEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator, + supported_token_types=['access_token']) class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint, -- cgit v1.2.1 From 296c6bc5931c95f631c1a496dacc523959fc50e9 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 19 Dec 2017 15:19:09 +0100 Subject: Improved doc by adding links to RFC and list of claims. --- oauthlib/oauth2/rfc6749/request_validator.py | 30 +++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 525ba33..4b76b7a 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -169,11 +169,28 @@ class RequestValidator(object): def introspect_token(self, token, token_type_hint, request, *args, **kwargs): """Introspect an access or refresh token. - Called once introspect token request is validated. This method - should return a dictionary with any desired claims associated - with the *token*. The implementation can use *token_type_hint* - to lookup this type first, but then it must fallback to other - types known, to be compliant with RFC. + Called once the introspect request is validated. This method should + verify the *token* and either return a dictionary with the list of + claims associated, or `None` in case the token is unknown. + + Below the list of registered claims you should be interested in: + - scope : space-separated list of scopes + - client_id : client identifier + - username : human-readable identifier for the resource owner + - token_type : type of the token + - exp : integer timestamp indicating when this token will expire + - iat : integer timestamp indicating when this token was issued + - nbf : integer timestamp indicating when it can be "not-before" used + - sub : subject of the token - identifier of the resource owner + - aud : list of string identifiers representing the intended audience + - iss : string representing issuer of this token + - jti : string identifier for the token + + Note that most of them are coming directly from JWT RFC. More details + can be found in `Introspect Claims`_ or `_JWT Claims`_. + + The implementation can use *token_type_hint* to improve lookup + efficency, but must fallback to other types to be compliant with RFC. The dict of claims is added to request.token after this method. @@ -183,6 +200,9 @@ class RequestValidator(object): Method is used by: - Introspect Endpoint (all grants are compatible) + + .. _`Introspect Claims`: https://tools.ietf.org/html/rfc7662#section-2.2 + .. _`JWT Claims`: https://tools.ietf.org/html/rfc7519#section-4 """ raise NotImplementedError('Subclasses must implement this method.') -- cgit v1.2.1 From 66d7296229122536163beabcc9552a0d8debbf60 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 2 Jan 2018 17:00:03 +0100 Subject: Added bottle-oauthlib (#509) --- README.rst | 2 ++ docs/faq.rst | 17 +++++++++++++---- docs/oauth2/endpoints/endpoints.rst | 2 +- docs/oauth2/server.rst | 8 ++++++-- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index eb85ffa..656d72c 100644 --- a/README.rst +++ b/README.rst @@ -56,6 +56,7 @@ The following packages provide OAuth support using OAuthLib. - For Django there is `django-oauth-toolkit`_, which includes `Django REST framework`_ support. - For Flask there is `flask-oauthlib`_ and `Flask-Dance`_. - For Pyramid there is `pyramid-oauthlib`_. +- For Bottle there is `bottle-oauthlib`_. If you have written an OAuthLib package that supports your favorite framework, please open a Pull Request, updating the documentation. @@ -65,6 +66,7 @@ please open a Pull Request, updating the documentation. .. _`Django REST framework`: http://django-rest-framework.org .. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance .. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib +.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib Using OAuthLib? Please get in touch! ------------------------------------ diff --git a/docs/faq.rst b/docs/faq.rst index 4d896f5..0c61af9 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -65,10 +65,17 @@ How do I use OAuthLib with Google, Twitter and other providers? How do I use OAuthlib as a provider with Django, Flask and other web frameworks? -------------------------------------------------------------------------------- - Providers using Django should seek out `django-oauth-toolkit`_ - and those using Flask `flask-oauthlib`_. For other frameworks, - please get in touch by opening a `GitHub issue`_, on `G+`_ or - on IRC #oauthlib irc.freenode.net. + Providers can be implemented in any web frameworks. However, some of + them have ready-to-use libraries to help integration: + - Django `django-oauth-toolkit`_ + - Flask `flask-oauthlib`_ + - Pyramid `pyramid-oauthlib`_ + - Bottle `bottle-oauthlib`_ + + For other frameworks, please get in touch by opening a `GitHub issue`_, on `G+`_ or + on IRC #oauthlib irc.freenode.net. If you have written an OAuthLib package that + supports your favorite framework, please open a Pull Request to update the docs. + What is the difference between authentication and authorization? ---------------------------------------------------------------- @@ -91,6 +98,8 @@ Some argue OAuth 2 is worse than 1, is that true? .. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib .. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit .. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib +.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib +.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib .. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new .. _`G+`: https://plus.google.com/communities/101889017375384052571 .. _`difference`: http://www.cyberciti.biz/faq/authentication-vs-authorization/ diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst index 0e70798..9bd1c4e 100644 --- a/docs/oauth2/endpoints/endpoints.rst +++ b/docs/oauth2/endpoints/endpoints.rst @@ -23,7 +23,7 @@ handles user authorization, the token endpoint which provides tokens and the resource endpoint which provides access to protected resources. It is to the endpoints you will feed requests and get back an almost complete response. This process is simplified for you using a decorator such as the django one described -later. +later (but it's applicable to all other web frameworks librairies). The main purpose of the endpoint in OAuthLib is to figure out which grant type or token to dispatch the request to. diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst index 9d6b502..9900e36 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -6,8 +6,10 @@ OAuthLib is a dependency free library that may be used with any web framework. That said, there are framework specific helper libraries to make your life easier. -- For Django there is `django-oauth-toolkit`_. -- For Flask there is `flask-oauthlib`_. +- Django `django-oauth-toolkit`_ +- Flask `flask-oauthlib`_ +- Pyramid `pyramid-oauthlib`_ +- Bottle `bottle-oauthlib`_ If there is no support for your favourite framework and you are interested in providing it then you have come to the right place. OAuthLib can handle @@ -17,6 +19,8 @@ as well as provide an interface for a backend to store tokens, clients, etc. .. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit .. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib +.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib +.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib .. contents:: Tutorial Contents :depth: 3 -- cgit v1.2.1 From d7fc1336d81b39f3d2193eb3155ff66da6caadd9 Mon Sep 17 00:00:00 2001 From: Antoine Bertin Date: Mon, 29 Jan 2018 10:17:54 +0100 Subject: Fix cliend_id in web request body (#505) Previously, cliend_id was always included in the request body in the Authorization Code flow and the client_id parameter was ignored in contradiction with the docs. Fixes #495 --- oauthlib/oauth2/rfc6749/clients/web_application.py | 2 +- tests/oauth2/rfc6749/clients/test_web_application.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index c099d99..bc62c8f 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -125,7 +125,7 @@ class WebApplicationClient(Client): """ code = code or self.code return prepare_token_request('authorization_code', code=code, body=body, - client_id=self.client_id, redirect_uri=redirect_uri, **kwargs) + client_id=client_id, redirect_uri=redirect_uri, **kwargs) def parse_request_uri_response(self, uri, state=None): """Parse the URI query for code and state. diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 85b247d..0a80c9a 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -38,7 +38,7 @@ class WebApplicationClientTest(TestCase): code = "zzzzaaaa" body = "not=empty" - body_code = "not=empty&grant_type=authorization_code&code=%s&client_id=%s" % (code, client_id) + body_code = "not=empty&grant_type=authorization_code&code=%s" % code body_redirect = body_code + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback" body_kwargs = body_code + "&some=providers&require=extra+arguments" -- cgit v1.2.1 From 2fe1cdb88e076f624824496c4aba6a8665e991d9 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Tue, 30 Jan 2018 17:30:26 -0200 Subject: 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 --- .../oauth2/rfc6749/endpoints/pre_configured.py | 7 +- oauthlib/oauth2/rfc6749/endpoints/resource.py | 2 +- oauthlib/oauth2/rfc6749/request_validator.py | 64 ++++++++++- oauthlib/oauth2/rfc6749/tokens.py | 46 +++++++- requirements-test.txt | 2 +- setup.py | 2 +- 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 diff --git a/setup.py b/setup.py index 43f4d95..4640ec8 100755 --- a/setup.py +++ b/setup.py @@ -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) -- cgit v1.2.1 From 32e5ad1509a8d46fa402776f54fbabef4b1ded63 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 28 Feb 2018 15:00:08 +0100 Subject: Rtd docs fix (#515) * Added sphinx build for developers Rationale is to build docs locally to prevent RTD to break later. * Replace manual sphinx into make * Renamed idan URL to oauthlib community * Renamed http into https URLs since http is returning 302 * python requests library renamed its home URL * Add ignore list for "make linkcheck" linkcheck is doing requests to github with anonymous access, however creating an issue require an logged-in account * virtualenv changed its homepage and website. * Fixed broken link --- README.rst | 14 ++-- docs/conf.py | 2 + docs/contributing.rst | 10 +-- docs/faq.rst | 4 +- docs/index.rst | 2 +- docs/installation.rst | 2 +- docs/oauth1/preconfigured_servers.rst | 2 +- docs/oauth1/server.rst | 2 +- docs/oauth2/clients/client.rst | 2 +- docs/oauth2/grants/jwt.rst | 2 +- docs/oauth2/server.rst | 4 +- docs/oauth2/tokens/mac.rst | 2 +- docs/oauth2/tokens/saml.rst | 2 +- docs/oauth2/tokens/tokens.rst | 4 +- oauthlib/common.py | 10 +-- oauthlib/oauth1/rfc5849/__init__.py | 4 +- oauthlib/oauth1/rfc5849/endpoints/access_token.py | 2 +- oauthlib/oauth1/rfc5849/endpoints/base.py | 6 +- oauthlib/oauth1/rfc5849/endpoints/request_token.py | 4 +- oauthlib/oauth1/rfc5849/endpoints/resource.py | 2 +- oauthlib/oauth1/rfc5849/parameters.py | 20 ++--- oauthlib/oauth1/rfc5849/request_validator.py | 6 +- oauthlib/oauth1/rfc5849/signature.py | 86 +++++++++++----------- oauthlib/oauth1/rfc5849/utils.py | 2 +- .../oauth2/rfc6749/clients/backend_application.py | 6 +- oauthlib/oauth2/rfc6749/clients/base.py | 10 +-- .../oauth2/rfc6749/clients/legacy_application.py | 6 +- .../oauth2/rfc6749/clients/mobile_application.py | 14 ++-- .../oauth2/rfc6749/clients/service_application.py | 2 +- oauthlib/oauth2/rfc6749/clients/web_application.py | 14 ++-- oauthlib/oauth2/rfc6749/endpoints/authorization.py | 2 +- oauthlib/oauth2/rfc6749/endpoints/revocation.py | 12 +-- oauthlib/oauth2/rfc6749/endpoints/token.py | 2 +- .../rfc6749/grant_types/authorization_code.py | 26 +++---- .../rfc6749/grant_types/client_credentials.py | 6 +- oauthlib/oauth2/rfc6749/grant_types/implicit.py | 34 ++++----- .../oauth2/rfc6749/grant_types/refresh_token.py | 8 +- .../resource_owner_password_credentials.py | 10 +-- oauthlib/oauth2/rfc6749/parameters.py | 32 ++++---- oauthlib/oauth2/rfc6749/request_validator.py | 8 +- oauthlib/oauth2/rfc6749/tokens.py | 14 ++-- setup.py | 2 +- tox.ini | 8 +- 43 files changed, 210 insertions(+), 202 deletions(-) diff --git a/README.rst b/README.rst index 656d72c..b4892a8 100644 --- a/README.rst +++ b/README.rst @@ -4,10 +4,10 @@ OAuthLib *A generic, spec-compliant, thorough implementation of the OAuth request-signing logic for python* -.. image:: https://travis-ci.org/idan/oauthlib.svg?branch=master - :target: https://travis-ci.org/idan/oauthlib -.. image:: https://coveralls.io/repos/idan/oauthlib/badge.svg?branch=master - :target: https://coveralls.io/r/idan/oauthlib +.. image:: https://travis-ci.org/oauthlib/oauthlib.svg?branch=master + :target: https://travis-ci.org/oauthlib/oauthlib +.. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master + :target: https://coveralls.io/r/oauthlib/oauthlib OAuth often seems complicated and difficult-to-implement. There are several @@ -18,8 +18,8 @@ both of the following: 2. They predate the `OAuth 2.0 spec`_, AKA RFC 6749. 3. They assume the usage of a specific HTTP request library. -.. _`OAuth 1.0 spec`: http://tools.ietf.org/html/rfc5849 -.. _`OAuth 2.0 spec`: http://tools.ietf.org/html/rfc6749 +.. _`OAuth 1.0 spec`: https://tools.ietf.org/html/rfc5849 +.. _`OAuth 2.0 spec`: https://tools.ietf.org/html/rfc6749 OAuthLib is a generic utility which implements the logic of OAuth without assuming a specific HTTP request object or web framework. Use it to graft OAuth @@ -45,7 +45,7 @@ Interested in making OAuth requests? Then you might be more interested in using `requests`_ which has OAuthLib powered OAuth support provided by the `requests-oauthlib`_ library. -.. _`requests`: https://github.com/kennethreitz/requests +.. _`requests`: https://github.com/requests/requests .. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib Which web frameworks are supported? diff --git a/docs/conf.py b/docs/conf.py index fb14d05..b1ca34d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -243,3 +243,5 @@ texinfo_documents = [ # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' + +linkcheck_ignore = ["https://github.com/oauthlib/oauthlib/issues/new"] diff --git a/docs/contributing.rst b/docs/contributing.rst index f3de44d..601c567 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -91,7 +91,7 @@ request only to have it rejected because it has diverged too far from master. To pull in upstream changes:: - git remote add upstream https://github.com/idan/oauthlib.git + git remote add upstream https://github.com/oauthlib/oauthlib.git git fetch upstream Check the log to be sure that you actually want the changes, before merging:: @@ -102,7 +102,7 @@ Then merge the changes that you fetched:: git merge upstream/master -For more info, see http://help.github.com/fork-a-repo/ +For more info, see https://help.github.com/fork-a-repo/ How to get your pull request accepted ===================================== @@ -148,7 +148,7 @@ version. For Ubuntu you can easily install all after adding one ppa. $ sudo apt-get install pypy pypy-dev .. _`Tox`: https://tox.readthedocs.io/en/latest/install.html -.. _`virtualenv`: http://www.virtualenv.org/en/latest/#installation +.. _`virtualenv`: https://virtualenv.pypa.io/en/latest/installation/ If you add code you need to add tests! -------------------------------------- @@ -223,5 +223,5 @@ to GitHub:: git push upstream master .. _installation: install.html -.. _GitHub project: https://github.com/idan/oauthlib -.. _issue tracker: https://github.com/idan/oauthlib/issues +.. _GitHub project: https://github.com/oauthlib/oauthlib +.. _issue tracker: https://github.com/oauthlib/oauthlib/issues diff --git a/docs/faq.rst b/docs/faq.rst index 0c61af9..38b0e92 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -100,6 +100,6 @@ Some argue OAuth 2 is worse than 1, is that true? .. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib .. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib .. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib -.. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new +.. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new .. _`G+`: https://plus.google.com/communities/101889017375384052571 -.. _`difference`: http://www.cyberciti.biz/faq/authentication-vs-authorization/ +.. _`difference`: https://www.cyberciti.biz/faq/authentication-vs-authorization/ diff --git a/docs/index.rst b/docs/index.rst index 1699068..1da2ca5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,7 +13,7 @@ Check out :doc:`error_reporting` for details on how to be an awesome bug reporte For news and discussions please head over to our `G+ OAuthLib community`_. -.. _`new issue on GitHub`: https://github.com/idan/oauthlib/issues/new +.. _`new issue on GitHub`: https://github.com/oauthlib/oauthlib/issues/new .. _`G+ OAuthLib community`: https://plus.google.com/communities/101889017375384052571 .. toctree:: diff --git a/docs/installation.rst b/docs/installation.rst index 5a8b2cb..48e4288 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -22,7 +22,7 @@ Bleeding edge from GitHub master .. code-block:: bash - pip install -e git+https://github.com/idan/oauthlib.git#egg=oauthlib + pip install -e git+https://github.com/oauthlib/oauthlib.git#egg=oauthlib Debian and derivatives like Ubuntu, Mint, etc. --------------------------------------------- diff --git a/docs/oauth1/preconfigured_servers.rst b/docs/oauth1/preconfigured_servers.rst index 7f7f386..b32e1ab 100644 --- a/docs/oauth1/preconfigured_servers.rst +++ b/docs/oauth1/preconfigured_servers.rst @@ -12,7 +12,7 @@ Construction is simple, only import your validator and you are good to go:: server = WebApplicationServer(your_validator) -All endpoints are documented in :doc:`endpoints`. +All endpoints are documented in :doc:`Provider endpoints `. .. autoclass:: oauthlib.oauth1.WebApplicationServer :members: diff --git a/docs/oauth1/server.rst b/docs/oauth1/server.rst index f254c91..2a91f30 100644 --- a/docs/oauth1/server.rst +++ b/docs/oauth1/server.rst @@ -436,7 +436,7 @@ shown below as well as run your flask server locally on port `5000`. Drop a line in our `G+ community`_ or open a `GitHub issue`_ =) .. _`G+ community`: https://plus.google.com/communities/101889017375384052571 -.. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new +.. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new If you run into issues it can be helpful to enable debug logging:: diff --git a/docs/oauth2/clients/client.rst b/docs/oauth2/clients/client.rst index 11da2cc..9a5a4ff 100644 --- a/docs/oauth2/clients/client.rst +++ b/docs/oauth2/clients/client.rst @@ -24,5 +24,5 @@ to use them please browse the documentation for each client type below. If you are interested in integrating OAuth 2 support into your favourite HTTP library you might find the requests-oauthlib implementation interesting. - .. _`requests`: https://github.com/kennethreitz/requests + .. _`requests`: https://github.com/requests/requests .. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib diff --git a/docs/oauth2/grants/jwt.rst b/docs/oauth2/grants/jwt.rst index 87aed11..db65342 100644 --- a/docs/oauth2/grants/jwt.rst +++ b/docs/oauth2/grants/jwt.rst @@ -4,4 +4,4 @@ JWT Tokens Not yet implemented. Track progress in `GitHub issue 50`_. -.. _`GitHub issue 50`: https://github.com/idan/oauthlib/issues/50 +.. _`GitHub issue 50`: https://github.com/oauthlib/oauthlib/issues/50 diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst index 9900e36..8f8b77b 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -279,7 +279,7 @@ all methods depending on which grant types you wish to support. A skeleton validator listing the methods required for the WebApplicationServer is available in the `examples`_ folder on GitHub. -.. _`examples`: https://github.com/idan/oauthlib/blob/master/examples/skeleton_oauth2_web_application_server.py +.. _`examples`: https://github.com/oauthlib/oauthlib/blob/master/examples/skeleton_oauth2_web_application_server.py Relevant sections include: @@ -496,7 +496,7 @@ at runtime by a function, rather then by a list. Drop a line in our `G+ community`_ or open a `GitHub issue`_ =) .. _`G+ community`: https://plus.google.com/communities/101889017375384052571 -.. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new +.. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new If you run into issues it can be helpful to enable debug logging. diff --git a/docs/oauth2/tokens/mac.rst b/docs/oauth2/tokens/mac.rst index 4986819..afb6948 100644 --- a/docs/oauth2/tokens/mac.rst +++ b/docs/oauth2/tokens/mac.rst @@ -5,4 +5,4 @@ MAC tokens Not yet implemented. Track progress in `GitHub issue 29`_. Might never be supported depending on whether the work on the specification is resumed or not. -.. _`GitHub issue 29`: https://github.com/idan/oauthlib/issues/29 +.. _`GitHub issue 29`: https://github.com/oauthlib/oauthlib/issues/29 diff --git a/docs/oauth2/tokens/saml.rst b/docs/oauth2/tokens/saml.rst index 9a00937..5faf16a 100644 --- a/docs/oauth2/tokens/saml.rst +++ b/docs/oauth2/tokens/saml.rst @@ -4,4 +4,4 @@ SAML Tokens Not yet implemented. Track progress in `GitHub issue 49`_. -.. _`GitHub issue 49`: https://github.com/idan/oauthlib/issues/49 +.. _`GitHub issue 49`: https://github.com/oauthlib/oauthlib/issues/49 diff --git a/docs/oauth2/tokens/tokens.rst b/docs/oauth2/tokens/tokens.rst index f0adc97..f341509 100644 --- a/docs/oauth2/tokens/tokens.rst +++ b/docs/oauth2/tokens/tokens.rst @@ -15,8 +15,8 @@ providers, notably Facebook, do not provide this information. Per the is missing. You can force a ``MissingTokenTypeError`` exception instead, by setting ``OAUTHLIB_STRICT_TOKEN_TYPE`` in the environment. -.. _requires: http://tools.ietf.org/html/rfc6749#section-5.1 -.. _robustness principle: http://en.wikipedia.org/wiki/Robustness_principle +.. _requires: https://tools.ietf.org/html/rfc6749#section-5.1 +.. _robustness principle: https://en.wikipedia.org/wiki/Robustness_principle .. toctree:: :maxdepth: 2 diff --git a/oauthlib/common.py b/oauthlib/common.py index 705cbd2..afcc09c 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -199,8 +199,8 @@ def generate_nonce(): A random 64-bit number is appended to the epoch timestamp for both randomness and to decrease the likelihood of collisions. - .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 - .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3 + .. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 + .. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3 """ return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp()) @@ -211,8 +211,8 @@ def generate_timestamp(): Per `section 3.3`_ of the OAuth 1 RFC 5849 spec. Per `section 3.2.1`_ of the MAC Access Authentication spec. - .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 - .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3 + .. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 + .. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3 """ return unicode_type(int(time.time())) @@ -257,7 +257,7 @@ def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET): """Generates an OAuth client_id OAuth 2 specify the format of client_id in - http://tools.ietf.org/html/rfc6749#appendix-A. + https://tools.ietf.org/html/rfc6749#appendix-A. """ return generate_token(length, chars) diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py index f9113ab..87a8e6b 100644 --- a/oauthlib/oauth1/rfc5849/__init__.py +++ b/oauthlib/oauth1/rfc5849/__init__.py @@ -122,7 +122,7 @@ class Client(object): replace any netloc part of the request argument's uri attribute value. - .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2 + .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2 """ if self.signature_method == SIGNATURE_PLAINTEXT: # fast-path @@ -300,7 +300,7 @@ class Client(object): raise ValueError( 'Body signatures may only be used with form-urlencoded content') - # We amend http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 + # We amend https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 # with the clause that parameters from body should only be included # in non GET or HEAD requests. Extracting the request body parameters # and including them in the signature base string would give semantic diff --git a/oauthlib/oauth1/rfc5849/endpoints/access_token.py b/oauthlib/oauth1/rfc5849/endpoints/access_token.py index 12b901c..12d13e9 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/access_token.py +++ b/oauthlib/oauth1/rfc5849/endpoints/access_token.py @@ -180,7 +180,7 @@ class AccessTokenEndpoint(BaseEndpoint): # token credentials to the client, and ensure that the temporary # credentials have not expired or been used before. The server MUST # also verify the verification code received from the client. - # .. _`Section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2 + # .. _`Section 3.2`: https://tools.ietf.org/html/rfc5849#section-3.2 # # Note that early exit would enable resource owner authorization # verifier enumertion. diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py index 9d51e69..9702939 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/base.py +++ b/oauthlib/oauth1/rfc5849/endpoints/base.py @@ -127,7 +127,7 @@ class BaseEndpoint(object): # specification. Implementers should review the Security # Considerations section (`Section 4`_) before deciding on which # method to support. - # .. _`Section 4`: http://tools.ietf.org/html/rfc5849#section-4 + # .. _`Section 4`: https://tools.ietf.org/html/rfc5849#section-4 if (not request.signature_method in self.request_validator.allowed_signature_methods): raise errors.InvalidSignatureMethodError( @@ -181,7 +181,7 @@ class BaseEndpoint(object): # ---- RSA Signature verification ---- if request.signature_method == SIGNATURE_RSA: # The server verifies the signature per `[RFC3447] section 8.2.2`_ - # .. _`[RFC3447] section 8.2.2`: http://tools.ietf.org/html/rfc3447#section-8.2.1 + # .. _`[RFC3447] section 8.2.2`: https://tools.ietf.org/html/rfc3447#section-8.2.1 rsa_key = self.request_validator.get_rsa_key( request.client_key, request) valid_signature = signature.verify_rsa_sha1(request, rsa_key) @@ -192,7 +192,7 @@ class BaseEndpoint(object): # Recalculating the request signature independently as described in # `Section 3.4`_ and comparing it to the value received from the # client via the "oauth_signature" parameter. - # .. _`Section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4 + # .. _`Section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 client_secret = self.request_validator.get_client_secret( request.client_key, request) resource_owner_secret = None diff --git a/oauthlib/oauth1/rfc5849/endpoints/request_token.py b/oauthlib/oauth1/rfc5849/endpoints/request_token.py index 515395b..88fd6c0 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/request_token.py +++ b/oauthlib/oauth1/rfc5849/endpoints/request_token.py @@ -156,7 +156,7 @@ class RequestTokenEndpoint(BaseEndpoint): # However they could be seen as a scope or realm to which the # client has access and as such every client should be checked # to ensure it is authorized access to that scope or realm. - # .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2 + # .. _`realm`: https://tools.ietf.org/html/rfc2617#section-1.2 # # Note that early exit would enable client realm access enumeration. # @@ -178,7 +178,7 @@ class RequestTokenEndpoint(BaseEndpoint): # Callback is normally never required, except for requests for # a Temporary Credential as described in `Section 2.1`_ - # .._`Section 2.1`: http://tools.ietf.org/html/rfc5849#section-2.1 + # .._`Section 2.1`: https://tools.ietf.org/html/rfc5849#section-2.1 valid_redirect = self.request_validator.validate_redirect_uri( request.client_key, request.redirect_uri, request) if not request.redirect_uri: diff --git a/oauthlib/oauth1/rfc5849/endpoints/resource.py b/oauthlib/oauth1/rfc5849/endpoints/resource.py index 53f9562..f82e8b1 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/resource.py +++ b/oauthlib/oauth1/rfc5849/endpoints/resource.py @@ -119,7 +119,7 @@ class ResourceEndpoint(BaseEndpoint): # However they could be seen as a scope or realm to which the # client has access and as such every client should be checked # to ensure it is authorized access to that scope or realm. - # .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2 + # .. _`realm`: https://tools.ietf.org/html/rfc2617#section-1.2 # # Note that early exit would enable client realm access enumeration. # diff --git a/oauthlib/oauth1/rfc5849/parameters.py b/oauthlib/oauth1/rfc5849/parameters.py index dcb23dc..2f068a7 100644 --- a/oauthlib/oauth1/rfc5849/parameters.py +++ b/oauthlib/oauth1/rfc5849/parameters.py @@ -5,7 +5,7 @@ oauthlib.parameters This module contains methods related to `section 3.5`_ of the OAuth 1.0a spec. -.. _`section 3.5`: http://tools.ietf.org/html/rfc5849#section-3.5 +.. _`section 3.5`: https://tools.ietf.org/html/rfc5849#section-3.5 """ from __future__ import absolute_import, unicode_literals @@ -42,8 +42,8 @@ def prepare_headers(oauth_params, headers=None, realm=None): oauth_version="1.0" - .. _`section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1 - .. _`RFC2617`: http://tools.ietf.org/html/rfc2617 + .. _`section 3.5.1`: https://tools.ietf.org/html/rfc5849#section-3.5.1 + .. _`RFC2617`: https://tools.ietf.org/html/rfc2617 """ headers = headers or {} @@ -54,7 +54,7 @@ def prepare_headers(oauth_params, headers=None, realm=None): # 1. Parameter names and values are encoded per Parameter Encoding # (`Section 3.6`_) # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 escaped_name = utils.escape(oauth_parameter_name) escaped_value = utils.escape(value) @@ -68,14 +68,14 @@ def prepare_headers(oauth_params, headers=None, realm=None): # 3. Parameters are separated by a "," character (ASCII code 44) and # OPTIONAL linear whitespace per `RFC2617`_. # - # .. _`RFC2617`: http://tools.ietf.org/html/rfc2617 + # .. _`RFC2617`: https://tools.ietf.org/html/rfc2617 authorization_header_parameters = ', '.join( authorization_header_parameters_parts) # 4. The OPTIONAL "realm" parameter MAY be added and interpreted per # `RFC2617 section 1.2`_. # - # .. _`RFC2617 section 1.2`: http://tools.ietf.org/html/rfc2617#section-1.2 + # .. _`RFC2617 section 1.2`: https://tools.ietf.org/html/rfc2617#section-1.2 if realm: # NOTE: realm should *not* be escaped authorization_header_parameters = ('realm="%s", ' % realm + @@ -98,8 +98,8 @@ def _append_params(oauth_params, params): Per `section 3.5.2`_ and `3.5.3`_ of the spec. - .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2 - .. _`3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3 + .. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2 + .. _`3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3 """ merged = list(params) @@ -117,7 +117,7 @@ def prepare_form_encoded_body(oauth_params, body): Per `section 3.5.2`_ of the spec. - .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2 + .. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2 """ # append OAuth params to the existing body @@ -129,7 +129,7 @@ def prepare_request_uri_query(oauth_params, uri): Per `section 3.5.3`_ of the spec. - .. _`section 3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3 + .. _`section 3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3 """ # append OAuth params to the existing set of query components diff --git a/oauthlib/oauth1/rfc5849/request_validator.py b/oauthlib/oauth1/rfc5849/request_validator.py index 2ccb367..bc62ea0 100644 --- a/oauthlib/oauth1/rfc5849/request_validator.py +++ b/oauthlib/oauth1/rfc5849/request_validator.py @@ -109,7 +109,7 @@ class RequestValidator(object): their use more straightforward and as such it could be worth reading what follows in chronological order. - .. _`whitelisting or blacklisting`: http://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html + .. _`whitelisting or blacklisting`: https://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html """ def __init__(self): @@ -445,7 +445,7 @@ class RequestValidator(object): "The server MUST (...) ensure that the temporary credentials have not expired or been used before." - .. _`Section 2.3`: http://tools.ietf.org/html/rfc5849#section-2.3 + .. _`Section 2.3`: https://tools.ietf.org/html/rfc5849#section-2.3 This method should ensure that provided token won't validate anymore. It can be simply removing RequestToken from storage or setting @@ -582,7 +582,7 @@ class RequestValidator(object): channel. The nonce value MUST be unique across all requests with the same timestamp, client credentials, and token combinations." - .. _`Section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3 One of the first validation checks that will be made is for the validity of the nonce and timestamp, which are associated with a client key and diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index 30001ef..4e672ba 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -19,7 +19,7 @@ Steps for signing a request: construct the base string 5. Pass the base string and any keys needed to a signing function -.. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4 +.. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 """ from __future__ import absolute_import, unicode_literals @@ -69,7 +69,7 @@ def construct_base_string(http_method, base_string_uri, ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk 9d7dh3k39sjv7 - .. _`section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1 + .. _`section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1 """ # The signature base string is constructed by concatenating together, @@ -79,7 +79,7 @@ def construct_base_string(http_method, base_string_uri, # "GET", "POST", etc. If the request uses a custom HTTP method, it # MUST be encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 base_string = utils.escape(http_method.upper()) # 2. An "&" character (ASCII code 38). @@ -88,8 +88,8 @@ def construct_base_string(http_method, base_string_uri, # 3. The base string URI from `Section 3.4.1.2`_, after being encoded # (`Section 3.6`_). # - # .. _`Section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2 - # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6 + # .. _`Section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2 + # .. _`Section 3.4.6`: https://tools.ietf.org/html/rfc5849#section-3.4.6 base_string += utils.escape(base_string_uri) # 4. An "&" character (ASCII code 38). @@ -98,8 +98,8 @@ def construct_base_string(http_method, base_string_uri, # 5. The request parameters as normalized in `Section 3.4.1.3.2`_, after # being encoded (`Section 3.6`). # - # .. _`Section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 - # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6 + # .. _`Section 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 + # .. _`Section 3.4.6`: https://tools.ietf.org/html/rfc5849#section-3.4.6 base_string += utils.escape(normalized_encoded_request_parameters) return base_string @@ -123,7 +123,7 @@ def normalize_base_string_uri(uri, host=None): is represented by the base string URI: "https://www.example.net:8080/". - .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2 + .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2 The host argument overrides the netloc part of the uri argument. """ @@ -137,7 +137,7 @@ def normalize_base_string_uri(uri, host=None): # are included by constructing an "http" or "https" URI representing # the request resource (without the query or fragment) as follows: # - # .. _`RFC3986`: http://tools.ietf.org/html/rfc3986 + # .. _`RFC3986`: https://tools.ietf.org/html/rfc3986 if not scheme or not netloc: raise ValueError('uri must include a scheme and netloc') @@ -147,7 +147,7 @@ def normalize_base_string_uri(uri, host=None): # Note that the absolute path cannot be empty; if none is present in # the original URI, it MUST be given as "/" (the server root). # - # .. _`RFC 2616 section 5.1.2`: http://tools.ietf.org/html/rfc2616#section-5.1.2 + # .. _`RFC 2616 section 5.1.2`: https://tools.ietf.org/html/rfc2616#section-5.1.2 if not path: path = '/' @@ -166,8 +166,8 @@ def normalize_base_string_uri(uri, host=None): # to port 80 or when making an HTTPS request `RFC2818`_ to port 443. # All other non-default port numbers MUST be included. # - # .. _`RFC2616`: http://tools.ietf.org/html/rfc2616 - # .. _`RFC2818`: http://tools.ietf.org/html/rfc2818 + # .. _`RFC2616`: https://tools.ietf.org/html/rfc2616 + # .. _`RFC2818`: https://tools.ietf.org/html/rfc2818 default_ports = ( ('http', '80'), ('https', '443'), @@ -190,7 +190,7 @@ def normalize_base_string_uri(uri, host=None): # particular manner that is often different from their original # encoding scheme, and concatenated into a single string. # -# .. _`section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3 +# .. _`section 3.4.1.3`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3 def collect_parameters(uri_query='', body=[], headers=None, exclude_oauth_signature=True, with_realm=False): @@ -249,7 +249,7 @@ def collect_parameters(uri_query='', body=[], headers=None, parameter instances (the "a3" parameter is used twice in this request). - .. _`section 3.4.1.3.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 + .. _`section 3.4.1.3.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 """ headers = headers or {} params = [] @@ -264,8 +264,8 @@ def collect_parameters(uri_query='', body=[], headers=None, # and values and decoding them as defined by # `W3C.REC-html40-19980424`_, Section 17.13.4. # - # .. _`RFC3986, Section 3.4`: http://tools.ietf.org/html/rfc3986#section-3.4 - # .. _`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424 + # .. _`RFC3986, Section 3.4`: https://tools.ietf.org/html/rfc3986#section-3.4 + # .. _`W3C.REC-html40-19980424`: https://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424 if uri_query: params.extend(urldecode(uri_query)) @@ -274,7 +274,7 @@ def collect_parameters(uri_query='', body=[], headers=None, # pairs excluding the "realm" parameter if present. The parameter # values are decoded as defined by `Section 3.5.1`_. # - # .. _`Section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1 + # .. _`Section 3.5.1`: https://tools.ietf.org/html/rfc5849#section-3.5.1 if headers: headers_lower = dict((k.lower(), v) for k, v in headers.items()) authorization_header = headers_lower.get('authorization') @@ -293,7 +293,7 @@ def collect_parameters(uri_query='', body=[], headers=None, # * The HTTP request entity-header includes the "Content-Type" # header field set to "application/x-www-form-urlencoded". # - # .._`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424 + # .._`W3C.REC-html40-19980424`: https://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424 # TODO: enforce header param inclusion conditions bodyparams = extract_params(body) or [] @@ -383,18 +383,18 @@ def normalize_parameters(params): dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1 &oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7 - .. _`section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 + .. _`section 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 """ # The parameters collected in `Section 3.4.1.3`_ are normalized into a # single string as follows: # - # .. _`Section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3 + # .. _`Section 3.4.1.3`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3 # 1. First, the name and value of each parameter are encoded # (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key_values = [(utils.escape(k), utils.escape(v)) for k, v in params] # 2. The parameters are sorted by name, using ascending byte value @@ -430,8 +430,8 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret): Per `section 3.4.2`_ of the spec. - .. _`RFC2104`: http://tools.ietf.org/html/rfc2104 - .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2 + .. _`RFC2104`: https://tools.ietf.org/html/rfc2104 + .. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2 """ # The HMAC-SHA1 function variables are used in following way: @@ -439,13 +439,13 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret): # text is set to the value of the signature base string from # `Section 3.4.1.1`_. # - # .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1 + # .. _`Section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1 text = base_string # key is set to the concatenated values of: # 1. The client shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key = utils.escape(client_secret or '') # 2. An "&" character (ASCII code 38), which MUST be included @@ -454,7 +454,7 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret): # 3. The token shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key += utils.escape(resource_owner_secret or '') # FIXME: HMAC does not support unicode! @@ -466,7 +466,7 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret): # parameter, after the result octet string is base64-encoded # per `RFC2045, Section 6.8`. # - # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8 + # .. _`RFC2045, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8 return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8') @@ -487,8 +487,8 @@ def sign_hmac_sha256(base_string, client_secret, resource_owner_secret): Per `section 3.4.2`_ of the spec. - .. _`RFC4634`: http://tools.ietf.org/html/rfc4634 - .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2 + .. _`RFC4634`: https://tools.ietf.org/html/rfc4634 + .. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2 """ # The HMAC-SHA256 function variables are used in following way: @@ -496,13 +496,13 @@ def sign_hmac_sha256(base_string, client_secret, resource_owner_secret): # text is set to the value of the signature base string from # `Section 3.4.1.1`_. # - # .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1 + # .. _`Section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1 text = base_string # key is set to the concatenated values of: # 1. The client shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key = utils.escape(client_secret or '') # 2. An "&" character (ASCII code 38), which MUST be included @@ -511,7 +511,7 @@ def sign_hmac_sha256(base_string, client_secret, resource_owner_secret): # 3. The token shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key += utils.escape(resource_owner_secret or '') # FIXME: HMAC does not support unicode! @@ -523,7 +523,7 @@ def sign_hmac_sha256(base_string, client_secret, resource_owner_secret): # parameter, after the result octet string is base64-encoded # per `RFC2045, Section 6.8`. # - # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8 + # .. _`RFC2045, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8 return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8') _jwtrs1 = None @@ -548,8 +548,8 @@ def sign_rsa_sha1(base_string, rsa_private_key): with the server that included its RSA public key (in a manner that is beyond the scope of this specification). - .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3 - .. _`RFC3447, Section 8.2`: http://tools.ietf.org/html/rfc3447#section-8.2 + .. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3 + .. _`RFC3447, Section 8.2`: https://tools.ietf.org/html/rfc3447#section-8.2 """ if isinstance(base_string, unicode_type): @@ -578,7 +578,7 @@ def sign_plaintext(client_secret, resource_owner_secret): utilize the signature base string or the "oauth_timestamp" and "oauth_nonce" parameters. - .. _`section 3.4.4`: http://tools.ietf.org/html/rfc5849#section-3.4.4 + .. _`section 3.4.4`: https://tools.ietf.org/html/rfc5849#section-3.4.4 """ @@ -587,7 +587,7 @@ def sign_plaintext(client_secret, resource_owner_secret): # 1. The client shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 signature = utils.escape(client_secret or '') # 2. An "&" character (ASCII code 38), which MUST be included even @@ -596,7 +596,7 @@ def sign_plaintext(client_secret, resource_owner_secret): # 3. The token shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 signature += utils.escape(resource_owner_secret or '') return signature @@ -612,7 +612,7 @@ def verify_hmac_sha1(request, client_secret=None, Per `section 3.4`_ of the spec. - .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4 + .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri attribute MUST be an absolute URI whose netloc part identifies the @@ -620,7 +620,7 @@ def verify_hmac_sha1(request, client_secret=None, item of the request argument's headers dict attribute will be ignored. - .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2 + .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2 """ norm_params = normalize_parameters(request.params) @@ -646,7 +646,7 @@ def verify_rsa_sha1(request, rsa_public_key): Note this method requires the jwt and cryptography libraries. - .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3 + .. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3 To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri attribute MUST be an absolute URI whose netloc part identifies the @@ -654,7 +654,7 @@ def verify_rsa_sha1(request, rsa_public_key): item of the request argument's headers dict attribute will be ignored. - .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2 + .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2 """ norm_params = normalize_parameters(request.params) uri = normalize_base_string_uri(request.uri) @@ -675,7 +675,7 @@ def verify_plaintext(request, client_secret=None, resource_owner_secret=None): Per `section 3.4`_ of the spec. - .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4 + .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 """ signature = sign_plaintext(client_secret, resource_owner_secret) match = safe_string_equals(signature, request.signature) diff --git a/oauthlib/oauth1/rfc5849/utils.py b/oauthlib/oauth1/rfc5849/utils.py index 979e5f6..3762e3b 100644 --- a/oauthlib/oauth1/rfc5849/utils.py +++ b/oauthlib/oauth1/rfc5849/utils.py @@ -49,7 +49,7 @@ def escape(u): Per `section 3.6`_ of the spec. - .. _`section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + .. _`section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 """ if not isinstance(u, unicode_type): diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py index 7505b0d..cbad8b7 100644 --- a/oauthlib/oauth2/rfc6749/clients/backend_application.py +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -52,9 +52,9 @@ class BackendApplicationClient(Client): >>> client.prepare_request_body(scope=['hello', 'world']) 'grant_type=client_credentials&scope=hello+world' - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ return prepare_token_request('client_credentials', body=body, scope=scope, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 5c5acee..a07a5c9 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -173,8 +173,8 @@ class Client(object): nonce="274312:dj83hs9s", mac="kDZvddkndxvhGRXZhvuDjEWhGeE=" - .. _`I-D.ietf-oauth-v2-bearer`: http://tools.ietf.org/html/rfc6749#section-12.2 - .. _`I-D.ietf-oauth-v2-http-mac`: http://tools.ietf.org/html/rfc6749#section-12.2 + .. _`I-D.ietf-oauth-v2-bearer`: https://tools.ietf.org/html/rfc6749#section-12.2 + .. _`I-D.ietf-oauth-v2-http-mac`: https://tools.ietf.org/html/rfc6749#section-12.2 """ if not is_secure_transport(uri): raise InsecureTransportError() @@ -401,9 +401,9 @@ class Client(object): Providers may supply this in all responses but are required to only if it has changed since the authorization request. - .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 - .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 + .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 """ self.token = parse_token_response(body, scope=scope) self._populate_attributes(self.token) diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py index 57fe99e..b16fc9f 100644 --- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py +++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py @@ -64,9 +64,9 @@ class LegacyApplicationClient(Client): >>> client.prepare_request_body(username='foo', password='bar', scope=['hello', 'world']) 'grant_type=password&username=foo&scope=hello+world&password=bar' - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ return prepare_token_request('password', body=body, username=username, password=password, scope=scope, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py index 490efcd..311aacf 100644 --- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -85,11 +85,11 @@ class MobileApplicationClient(Client): >>> client.prepare_request_uri('https://example.com', foo='bar') 'https://example.com?client_id=your_id&response_type=token&foo=bar' - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 """ return prepare_grant_uri(uri, self.client_id, 'token', redirect_uri=redirect_uri, state=state, scope=scope, **kwargs) @@ -164,8 +164,8 @@ class MobileApplicationClient(Client): >>> client.parse_request_body_response(response_body, scope=['other']) ('Scope has changed from "other" to "hello world".', ['other'], ['hello', 'world']) - .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 """ self.token = parse_implicit_response(uri, state=state, scope=scope) self._populate_attributes(self.token) diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index e6c3270..84ea0e9 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -136,7 +136,7 @@ class ServiceApplicationClient(Client): eyJpc3Mi[...omitted for brevity...]. J9l-ZhwP[...omitted for brevity...] - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ import jwt diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index bc62c8f..14b5265 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -76,11 +76,11 @@ class WebApplicationClient(Client): >>> client.prepare_request_uri('https://example.com', foo='bar') 'https://example.com?client_id=your_id&response_type=code&foo=bar' - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 """ return prepare_grant_uri(uri, self.client_id, 'code', redirect_uri=redirect_uri, scope=scope, state=state, **kwargs) @@ -120,8 +120,8 @@ class WebApplicationClient(Client): >>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar') 'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar' - .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1 + .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ code = code or self.code return prepare_token_request('authorization_code', code=code, body=body, diff --git a/oauthlib/oauth2/rfc6749/endpoints/authorization.py b/oauthlib/oauth2/rfc6749/endpoints/authorization.py index b6e0734..92cde34 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/authorization.py +++ b/oauthlib/oauth2/rfc6749/endpoints/authorization.py @@ -59,7 +59,7 @@ class AuthorizationEndpoint(BaseEndpoint): # Enforced through the design of oauthlib.common.Request - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B """ def __init__(self, default_response_type, default_token_type, diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index 4364b81..d5b5b78 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -5,7 +5,7 @@ oauthlib.oauth2.rfc6749.endpoint.revocation An implementation of the OAuth 2 `Token Revocation`_ spec (draft 11). -.. _`Token Revocation`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11 +.. _`Token Revocation`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11 """ from __future__ import absolute_import, unicode_literals @@ -110,11 +110,11 @@ class RevocationEndpoint(BaseEndpoint): The client also includes its authentication credentials as described in `Section 2.3`_. of [`RFC6749`_]. - .. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4 - .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5 - .. _`section 2.3`: http://tools.ietf.org/html/rfc6749#section-2.3 - .. _`Section 4.1.2`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2 - .. _`RFC6749`: http://tools.ietf.org/html/rfc6749 + .. _`section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4 + .. _`section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5 + .. _`section 2.3`: https://tools.ietf.org/html/rfc6749#section-2.3 + .. _`Section 4.1.2`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2 + .. _`RFC6749`: https://tools.ietf.org/html/rfc6749 """ if not request.token: raise InvalidRequestError(request=request, diff --git a/oauthlib/oauth2/rfc6749/endpoints/token.py b/oauthlib/oauth2/rfc6749/endpoints/token.py index ece6325..90fb16f 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/token.py +++ b/oauthlib/oauth2/rfc6749/endpoints/token.py @@ -59,7 +59,7 @@ class TokenEndpoint(BaseEndpoint): # Delegated to each grant type. - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B """ def __init__(self, default_grant_type, default_token_type, grant_types): diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 8661c35..7bea650 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -91,7 +91,7 @@ class AuthorizationCodeGrant(GrantTypeBase): step (C). If valid, the authorization server responds back with an access token and, optionally, a refresh token. - .. _`Authorization Code Grant`: http://tools.ietf.org/html/rfc6749#section-4.1 + .. _`Authorization Code Grant`: https://tools.ietf.org/html/rfc6749#section-4.1 """ default_response_mode = 'query' @@ -175,11 +175,11 @@ class AuthorizationCodeGrant(GrantTypeBase): File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request oauthlib.oauth2.rfc6749.errors.InvalidClientIdError - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 """ try: # request.scopes is only mandated in post auth and both pre and @@ -206,7 +206,7 @@ class AuthorizationCodeGrant(GrantTypeBase): # the authorization server informs the client by adding the following # parameters to the query component of the redirection URI using the # "application/x-www-form-urlencoded" format, per Appendix B: - # http://tools.ietf.org/html/rfc6749#appendix-B + # https://tools.ietf.org/html/rfc6749#appendix-B except errors.OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) request.redirect_uri = request.redirect_uri or self.error_uri @@ -285,7 +285,7 @@ class AuthorizationCodeGrant(GrantTypeBase): raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request) # REQUIRED. The client identifier as described in Section 2.2. - # http://tools.ietf.org/html/rfc6749#section-2.2 + # https://tools.ietf.org/html/rfc6749#section-2.2 if not request.client_id: raise errors.MissingClientIdError(request=request) @@ -293,7 +293,7 @@ class AuthorizationCodeGrant(GrantTypeBase): raise errors.InvalidClientIdError(request=request) # OPTIONAL. As described in Section 3.1.2. - # http://tools.ietf.org/html/rfc6749#section-3.1.2 + # https://tools.ietf.org/html/rfc6749#section-3.1.2 log.debug('Validating redirection uri %s for client %s.', request.redirect_uri, request.client_id) if request.redirect_uri is not None: @@ -320,7 +320,7 @@ class AuthorizationCodeGrant(GrantTypeBase): # the authorization server informs the client by adding the following # parameters to the query component of the redirection URI using the # "application/x-www-form-urlencoded" format, per Appendix B. - # http://tools.ietf.org/html/rfc6749#appendix-B + # https://tools.ietf.org/html/rfc6749#appendix-B # Note that the correct parameters to be added are automatically # populated through the use of specific exceptions. @@ -346,7 +346,7 @@ class AuthorizationCodeGrant(GrantTypeBase): raise errors.UnauthorizedClientError(request=request) # OPTIONAL. The scope of the access request as described by Section 3.3 - # http://tools.ietf.org/html/rfc6749#section-3.3 + # https://tools.ietf.org/html/rfc6749#section-3.3 self.validate_scopes(request) request_info.update({ @@ -384,14 +384,14 @@ class AuthorizationCodeGrant(GrantTypeBase): # credentials (or assigned other authentication requirements), the # client MUST authenticate with the authorization server as described # in Section 3.2.1. - # http://tools.ietf.org/html/rfc6749#section-3.2.1 + # https://tools.ietf.org/html/rfc6749#section-3.2.1 if not self.request_validator.authenticate_client(request): log.debug('Client authentication failed, %r.', request) raise errors.InvalidClientError(request=request) elif not self.request_validator.authenticate_client_id(request.client_id, request): # REQUIRED, if the client is not authenticating with the # authorization server as described in Section 3.2.1. - # http://tools.ietf.org/html/rfc6749#section-3.2.1 + # https://tools.ietf.org/html/rfc6749#section-3.2.1 log.debug('Client authentication failed, %r.', request) raise errors.InvalidClientError(request=request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index bf6c87f..4c50a78 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -47,7 +47,7 @@ class ClientCredentialsGrant(GrantTypeBase): (B) The authorization server authenticates the client, and if valid, issues an access token. - .. _`Client Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.4 + .. _`Client Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.4 """ def create_token_response(self, request, token_handler): @@ -59,8 +59,8 @@ class ClientCredentialsGrant(GrantTypeBase): failed client authentication or is invalid, the authorization server returns an error response as described in `Section 5.2`_. - .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 """ headers = { 'Content-Type': 'application/json', diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index 2b9c49d..bdab814 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -111,9 +111,9 @@ class ImplicitGrant(GrantTypeBase): See `Section 10.3`_ and `Section 10.16`_ for important security considerations when using the implicit grant. - .. _`Implicit Grant`: http://tools.ietf.org/html/rfc6749#section-4.2 - .. _`Section 10.3`: http://tools.ietf.org/html/rfc6749#section-10.3 - .. _`Section 10.16`: http://tools.ietf.org/html/rfc6749#section-10.16 + .. _`Implicit Grant`: https://tools.ietf.org/html/rfc6749#section-4.2 + .. _`Section 10.3`: https://tools.ietf.org/html/rfc6749#section-10.3 + .. _`Section 10.16`: https://tools.ietf.org/html/rfc6749#section-10.16 """ response_types = ['token'] @@ -152,11 +152,11 @@ class ImplicitGrant(GrantTypeBase): access token matches a redirection URI registered by the client as described in `Section 3.1.2`_. - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B """ return self.create_token_response(request, token_handler) @@ -195,9 +195,9 @@ class ImplicitGrant(GrantTypeBase): The authorization server MUST NOT issue a refresh token. - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 """ try: # request.scopes is only mandated in post auth and both pre and @@ -222,7 +222,7 @@ class ImplicitGrant(GrantTypeBase): # the authorization server informs the client by adding the following # parameters to the fragment component of the redirection URI using the # "application/x-www-form-urlencoded" format, per Appendix B: - # http://tools.ietf.org/html/rfc6749#appendix-B + # https://tools.ietf.org/html/rfc6749#appendix-B except errors.OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples, @@ -285,7 +285,7 @@ class ImplicitGrant(GrantTypeBase): raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request) # REQUIRED. The client identifier as described in Section 2.2. - # http://tools.ietf.org/html/rfc6749#section-2.2 + # https://tools.ietf.org/html/rfc6749#section-2.2 if not request.client_id: raise errors.MissingClientIdError(request=request) @@ -293,7 +293,7 @@ class ImplicitGrant(GrantTypeBase): raise errors.InvalidClientIdError(request=request) # OPTIONAL. As described in Section 3.1.2. - # http://tools.ietf.org/html/rfc6749#section-3.1.2 + # https://tools.ietf.org/html/rfc6749#section-3.1.2 if request.redirect_uri is not None: request.using_default_redirect_uri = False log.debug('Using provided redirect_uri %s', request.redirect_uri) @@ -304,7 +304,7 @@ class ImplicitGrant(GrantTypeBase): # to which it will redirect the access token matches a # redirection URI registered by the client as described in # Section 3.1.2. - # http://tools.ietf.org/html/rfc6749#section-3.1.2 + # https://tools.ietf.org/html/rfc6749#section-3.1.2 if not self.request_validator.validate_redirect_uri( request.client_id, request.redirect_uri, request): raise errors.MismatchingRedirectURIError(request=request) @@ -328,7 +328,7 @@ class ImplicitGrant(GrantTypeBase): # the authorization server informs the client by adding the following # parameters to the fragment component of the redirection URI using the # "application/x-www-form-urlencoded" format, per Appendix B. - # http://tools.ietf.org/html/rfc6749#appendix-B + # https://tools.ietf.org/html/rfc6749#appendix-B # Note that the correct parameters to be added are automatically # populated through the use of specific exceptions @@ -351,7 +351,7 @@ class ImplicitGrant(GrantTypeBase): raise errors.UnauthorizedClientError(request=request) # OPTIONAL. The scope of the access request as described by Section 3.3 - # http://tools.ietf.org/html/rfc6749#section-3.3 + # https://tools.ietf.org/html/rfc6749#section-3.3 self.validate_scopes(request) request_info.update({ diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index 6233e7c..c2d86f7 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -19,7 +19,7 @@ class RefreshTokenGrant(GrantTypeBase): """`Refresh token grant`_ - .. _`Refresh token grant`: http://tools.ietf.org/html/rfc6749#section-6 + .. _`Refresh token grant`: https://tools.ietf.org/html/rfc6749#section-6 """ def __init__(self, request_validator=None, @@ -46,8 +46,8 @@ class RefreshTokenGrant(GrantTypeBase): identical to that of the refresh token included by the client in the request. - .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 """ headers = { 'Content-Type': 'application/json', @@ -90,7 +90,7 @@ class RefreshTokenGrant(GrantTypeBase): # the client was issued client credentials (or assigned other # authentication requirements), the client MUST authenticate with the # authorization server as described in Section 3.2.1. - # http://tools.ietf.org/html/rfc6749#section-3.2.1 + # https://tools.ietf.org/html/rfc6749#section-3.2.1 if self.request_validator.client_authentication_required(request): log.debug('Authenticating client, %r.', request) if not self.request_validator.authenticate_client(request): diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py index ede779a..e5f04af 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -67,7 +67,7 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): the resource owner credentials, and if valid, issues an access token. - .. _`Resource Owner Password Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.3 + .. _`Resource Owner Password Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.3 """ def create_token_response(self, request, token_handler): @@ -79,8 +79,8 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): authentication or is invalid, the authorization server returns an error response as described in `Section 5.2`_. - .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 """ headers = { 'Content-Type': 'application/json', @@ -153,8 +153,8 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): brute force attacks (e.g., using rate-limitation or generating alerts). - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ for validator in self.custom_validators.pre_token: validator(request) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index b87b146..0107933 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -5,7 +5,7 @@ oauthlib.oauth2.rfc6749.parameters This module contains methods related to `Section 4`_ of the OAuth 2 RFC. -.. _`Section 4`: http://tools.ietf.org/html/rfc6749#section-4 +.. _`Section 4`: https://tools.ietf.org/html/rfc6749#section-4 """ from __future__ import absolute_import, unicode_literals @@ -61,11 +61,11 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None, &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 Host: server.example.com - .. _`W3C.REC-html401-19991224`: http://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224 - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + .. _`W3C.REC-html401-19991224`: https://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224 + .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 """ if not is_secure_transport(uri): raise InsecureTransportError() @@ -111,7 +111,7 @@ def prepare_token_request(grant_type, body='', **kwargs): grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb - .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1 + .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1 """ params = [('grant_type', grant_type)] @@ -153,9 +153,9 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token", specification MAY define other values for this parameter using the registry defined in `Section 4.1.2`_. - .. _`Section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4 - .. _`Section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5 - .. _`Section 4.1.2`: http://tools.ietf.org/html/rfc7009#section-4.1.2 + .. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4 + .. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5 + .. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2 """ if not is_secure_transport(url): @@ -348,10 +348,10 @@ def parse_token_response(body, scope=None): "example_parameter":"example_value" } - .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 - .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`RFC4627`: http://tools.ietf.org/html/rfc4627 + .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 + .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`RFC4627`: https://tools.ietf.org/html/rfc4627 """ try: params = json.loads(body) @@ -359,7 +359,7 @@ def parse_token_response(body, scope=None): # Fall back to URL-encoded string, to support old implementations, # including (at time of writing) Facebook. See: - # https://github.com/idan/oauthlib/issues/267 + # https://github.com/oauthlib/oauthlib/issues/267 params = dict(urlparse.parse_qsl(body)) for key in ('expires_in', 'expires'): @@ -395,7 +395,7 @@ def validate_token_parameters(params): # If the issued access token scope is different from the one requested by # the client, the authorization server MUST include the "scope" response # parameter to inform the client of the actual scope granted. - # http://tools.ietf.org/html/rfc6749#section-3.3 + # https://tools.ietf.org/html/rfc6749#section-3.3 if params.scope_changed: message = 'Scope has changed from "{old}" to "{new}".'.format( old=params.old_scope, new=params.scope, diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index d25a6e0..182642e 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -34,9 +34,9 @@ class RequestValidator(object): - Resource Owner Password Credentials Grant - Refresh Token Grant - .. _`Section 4.3.2`: http://tools.ietf.org/html/rfc6749#section-4.3.2 - .. _`Section 4.1.3`: http://tools.ietf.org/html/rfc6749#section-4.1.3 - .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 + .. _`Section 4.3.2`: https://tools.ietf.org/html/rfc6749#section-4.3.2 + .. _`Section 4.1.3`: https://tools.ietf.org/html/rfc6749#section-4.1.3 + .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6 """ return True @@ -60,7 +60,7 @@ class RequestValidator(object): - Client Credentials Grant - Refresh Token Grant - .. _`HTTP Basic Authentication Scheme`: http://tools.ietf.org/html/rfc1945#section-11.1 + .. _`HTTP Basic Authentication Scheme`: https://tools.ietf.org/html/rfc1945#section-11.1 """ raise NotImplementedError('Subclasses must implement this method.') diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index e68ba59..4ae20e0 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -4,8 +4,8 @@ oauthlib.oauth2.rfc6749.tokens This module contains methods for adding two types of access tokens to requests. -- Bearer http://tools.ietf.org/html/rfc6750 -- MAC http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 +- Bearer https://tools.ietf.org/html/rfc6750 +- MAC https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 """ from __future__ import absolute_import, unicode_literals @@ -93,8 +93,8 @@ def prepare_mac_header(token, uri, key, http_method, nonce="1336363200:dj83hs9s", mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM=" - .. _`MAC Access Authentication`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 - .. _`extension algorithms`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1 + .. _`MAC Access Authentication`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 + .. _`extension algorithms`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1 :param uri: Request URI. :param headers: Request headers as a dictionary. @@ -180,7 +180,7 @@ def prepare_bearer_uri(token, uri): http://www.example.com/path?access_token=h480djs93hd8 - .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750 + .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750 """ return add_params_to_uri(uri, [(('access_token', token))]) @@ -191,7 +191,7 @@ def prepare_bearer_headers(token, headers=None): Authorization: Bearer h480djs93hd8 - .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750 + .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750 """ headers = headers or {} headers['Authorization'] = 'Bearer %s' % token @@ -203,7 +203,7 @@ def prepare_bearer_body(token, body=''): access_token=h480djs93hd8 - .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750 + .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750 """ return add_params_to_qs(body, [(('access_token', token))]) diff --git a/setup.py b/setup.py index 4640ec8..0c4e564 100755 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ setup( author_email='idan@gazit.me', maintainer='Ib Lundgren', maintainer_email='ib.lundgren@gmail.com', - url='https://github.com/idan/oauthlib', + url='https://github.com/oauthlib/oauthlib', platforms='any', license='BSD', packages=find_packages(exclude=('docs', 'tests', 'tests.*')), diff --git a/tox.ini b/tox.ini index a53676f..2546bee 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,pypy +envlist = py27,py34,py35,py36,pypy,docs [testenv] deps= @@ -9,3 +9,9 @@ commands=nosetests --with-coverage --cover-erase --cover-package=oauthlib -w tes [testenv:py27] deps=unittest2 {[testenv]deps} + +[testenv:docs] +deps=sphinx +changedir=docs +whitelist_externals=make +commands=make html -- cgit v1.2.1 From 3d248180e6fc66946821cce8938168cb15fc4f48 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Wed, 28 Feb 2018 14:00:39 +0000 Subject: Update repository location in Travis. (#514) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f5e9aca..3290c1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,4 +31,4 @@ deploy: distributions: sdist bdist_wheel on: tags: true - repo: idan/oauthlib + repo: oauthlib/oauthlib -- cgit v1.2.1 From d93403ad68ef308a195697fc79519df37812af1f Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Wed, 7 Mar 2018 16:08:52 +0000 Subject: Replace G+ with Gitter. (#517) --- README.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index b4892a8..dab8813 100644 --- a/README.rst +++ b/README.rst @@ -2,13 +2,14 @@ OAuthLib ======== *A generic, spec-compliant, thorough implementation of the OAuth request-signing -logic for python* +logic for Python 2.7 and 3.4+.* .. image:: https://travis-ci.org/oauthlib/oauthlib.svg?branch=master :target: https://travis-ci.org/oauthlib/oauthlib .. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master :target: https://coveralls.io/r/oauthlib/oauthlib - +.. image:: https://badges.gitter.im/oauthlib/oauthlib.svg + :target: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link OAuth often seems complicated and difficult-to-implement. There are several prominent libraries for handling OAuth requests, but they all suffer from one or @@ -33,10 +34,10 @@ Documentation Full documentation is available on `Read the Docs`_. All contributions are very welcome! The documentation is still quite sparse, please open an issue for what -you'd like to know, or discuss it in our `G+ community`_, or even better, send a +you'd like to know, or discuss it in our `Gitter community`_, or even better, send a pull request! -.. _`G+ community`: https://plus.google.com/communities/101889017375384052571 +.. _`Gitter community`: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link .. _`Read the Docs`: https://oauthlib.readthedocs.io/en/latest/index.html Interested in making OAuth requests? @@ -74,7 +75,7 @@ Patching OAuth support onto an http request framework? Creating an OAuth provider extension for a web framework? Simply using OAuthLib to Get Things Done or to learn? -No matter which we'd love to hear from you in our `G+ community`_ or if you have +No matter which we'd love to hear from you in our `Gitter community`_ or if you have anything in particular you would like to have, change or comment on don't hesitate for a second to send a pull request or open an issue. We might be quite busy and therefore slow to reply but we love feedback! @@ -83,7 +84,7 @@ Chances are you have run into something annoying that you wish there was documentation for, if you wish to gain eternal fame and glory, and a drink if we have the pleasure to run into eachother, please send a docs pull request =) -.. _`G+ community`: https://plus.google.com/communities/101889017375384052571 +.. _`Gitter community`: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link License ------- -- cgit v1.2.1 From 70b5827566dc3c0dd8f05cfbb8311b905f7a7254 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 12 Mar 2018 12:03:53 +0000 Subject: Add shields for Python versions, license and RTD. --- README.rst | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index dab8813..b477e41 100644 --- a/README.rst +++ b/README.rst @@ -6,10 +6,22 @@ logic for Python 2.7 and 3.4+.* .. image:: https://travis-ci.org/oauthlib/oauthlib.svg?branch=master :target: https://travis-ci.org/oauthlib/oauthlib + :alt: Travis .. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master :target: https://coveralls.io/r/oauthlib/oauthlib + :alt: Coveralls +.. image:: https://img.shields.io/pypi/pyversions/oauthlib.svg + :target: https://pypi.python.org/pypi/oauthlib + :alt: Download from PyPi +.. image:: https://img.shields.io/pypi/l/oauthlib.svg + :target: https://pypi.python.org/pypi/oauthlib + :alt: License +.. image:: https://img.shields.io/readthedocs/oauthlib.svg + :target: https://oauthlib.readthedocs.io/en/latest/index.html + :alt: Read the Docs .. image:: https://badges.gitter.im/oauthlib/oauthlib.svg - :target: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link + :target: https://gitter.im/oauthlib/Lobby + :alt: Chat on Gitter OAuth often seems complicated and difficult-to-implement. There are several prominent libraries for handling OAuth requests, but they all suffer from one or @@ -37,7 +49,7 @@ welcome! The documentation is still quite sparse, please open an issue for what you'd like to know, or discuss it in our `Gitter community`_, or even better, send a pull request! -.. _`Gitter community`: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link +.. _`Gitter community`: https://gitter.im/oauthlib/Lobby .. _`Read the Docs`: https://oauthlib.readthedocs.io/en/latest/index.html Interested in making OAuth requests? @@ -84,7 +96,7 @@ Chances are you have run into something annoying that you wish there was documentation for, if you wish to gain eternal fame and glory, and a drink if we have the pleasure to run into eachother, please send a docs pull request =) -.. _`Gitter community`: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link +.. _`Gitter community`: https://gitter.im/oauthlib/Lobby License ------- -- cgit v1.2.1 From f398fdb7b0ac7b0aa11408d11a4358524360ac77 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 18 Mar 2018 10:59:11 +0100 Subject: Fix ReadTheDocs build (#521) --- docs/conf.py | 3 +-- tox.ini | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b1ca34d..017f686 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,8 +14,6 @@ import os import sys -from oauthlib import __version__ as v - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -51,6 +49,7 @@ copyright = u'2012, Idan Gazit and the Python Community' # # The short X.Y version. +from oauthlib import __version__ as v version = v[:3] # The full version, including alpha/beta/rc tags. release = v diff --git a/tox.ini b/tox.ini index 2546bee..3dded41 100644 --- a/tox.ini +++ b/tox.ini @@ -10,8 +10,12 @@ commands=nosetests --with-coverage --cover-erase --cover-package=oauthlib -w tes deps=unittest2 {[testenv]deps} +# tox -e docs to mimick readthedocs build. +# as of today, RTD is using python2.7 and doesn't run "setup.py install" [testenv:docs] +basepython=python2.7 +skipsdist=True deps=sphinx changedir=docs whitelist_externals=make -commands=make html +commands=make clean html -- cgit v1.2.1 From ad61175827cddda8f8cb3cccc14f9f5eb9887ca7 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 18 Mar 2018 11:06:51 +0100 Subject: Fixed "make" command to test upstream with local oauthlib. (#522) --- Makefile | 74 ++++++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/Makefile b/Makefile index 259fe9c..8571a91 100644 --- a/Makefile +++ b/Makefile @@ -1,47 +1,57 @@ -PYS = py27,py34,pypy +# Downstream tests (Don't be evil) +# +# Try and not break the libraries below by running their tests too. +# +# Unfortunately there is no neat way to run downstream tests AFAIK +# Until we have a proper downstream testing system we will +# stick to this Makefile. +#--------------------------- +# HOW TO ADD NEW DOWNSTREAM LIBRARIES +# +# Please specify your library as well as primary contacts. +# Since these contacts will be addressed with Github mentions they +# need to be Github users (for now)(sorry Bitbucket). +# +clean: + rm -rf .tox + rm -rf bottle-oauthlib + rm -rf django-oauth-toolkit + rm -rf flask-oauthlib + rm -rf requests-oauthlib test: - # Test OAuthLib - tox -e "$(PYS)" - # - # Downstream tests (Don't be evil) - # - # Try and not break the libraries below by running their tests too. - # - # Unfortunately there is no neat way to run downstream tests AFAIK - # Until we have a proper downstream testing system we will - # stick to this Makefile. + tox + +bottle: #--------------------------- - # HOW TO ADD NEW DOWNSTREAM LIBRARIES - # - # Please specify your library as well as primary contacts. - # Since these contacts will be addressed with Github mentions they - # need to be Github users (for now)(sorry Bitbucket). - # + # Library thomsonreuters/bottle-oauthlib + # Contacts: Jonathan.Huot + cd bottle-oauthlib 2>/dev/null || git clone https://github.com/thomsonreuters/bottle-oauthlib.git + cd bottle-oauthlib && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox + +flask: #--------------------------- # Library: lepture/flask-oauthLib # Contacts: lepture,widnyana - git clone https://github.com/lepture/flask-oauthlib.git - cd flask-oauthlib && cp ../tox.ini . && sed -i 's/py32,py33,py34,//' tox.ini && sed -i '/mock/a \ Flask-SQLAlchemy' tox.ini && tox -e "$(PYS)" - rm -rf flask-oauthlib + cd flask-oauthlib 2>/dev/null || git clone https://github.com/lepture/flask-oauthlib.git + cd flask-oauthlib && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox + +django: #--------------------------- # Library: evonove/django-oauth-toolkit # Contacts: evonove,masci # (note: has tox.ini already) - git clone https://github.com/evonove/django-oauth-toolkit.git - cd django-oauth-toolkit && tox -e "$(PYS)" - rm -rf django-oauth-toolkit + cd django-oauth-toolkit 2>/dev/null || git clone https://github.com/evonove/django-oauth-toolkit.git + cd django-oauth-toolkit && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && tox -e py27,py35,py36 + +requests: #--------------------------- # Library requests/requests-oauthlib # Contacts: ib-lundgren,lukasa - git clone https://github.com/requests/requests-oauthlib.git - cd requests-oauthlib && cp ../tox.ini . && sed -i '/mock/a \ requests' tox.ini && tox -e "$(PYS)" - rm -rf requests-oauthlib - #--------------------------- - # + cd requests-oauthlib 2>/dev/null || git clone https://github.com/requests/requests-oauthlib.git + cd requests-oauthlib && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../[signedtoken],' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox -pycco: - find oauthlib -name "*.py" -exec pycco -p -s reST {} \; -pycco-clean: - rm -rf docs/oauthlib docs/pycco.css +.DEFAULT_GOAL := all +.PHONY: clean test bottle django flask requests +all: clean test bottle django flask requests -- cgit v1.2.1 From 9855c6b2a030a5307691837ba705000ec8f898f0 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 13 Mar 2018 09:37:29 +0100 Subject: Replace IRC notificatgion with Gitter Hook --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3290c1f..e3c01f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,12 @@ script: tox after_success: coveralls notifications: - irc: irc.freenode.org#oauthlib + webhooks: + urls: + - https://webhooks.gitter.im/e/6008c872bf0ecee344f4 + on_success: change + on_failure: always + on_start: never deploy: provider: pypi user: ib.lundgren -- cgit v1.2.1 From 3e13cd30b29c273a79824ad02f1d2cb8700a5955 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 13 Mar 2018 09:54:41 +0100 Subject: Added Github Releases deploy provider. --- .travis.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index e3c01f1..06506e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,11 +29,17 @@ notifications: on_failure: always on_start: never deploy: - provider: pypi - user: ib.lundgren - password: - secure: PGZF9pRiTGCSwQjk1ddTKF3x4rQ0iAiPbg2uSixyO68uMXRgJjwHhSrNM0OEqtK5YWU5FE5L0DwR1nkrpEJKO4a5q2EOgos+gVoKpJfinoUNOOkjc1VHpqKM0uRf/OKrw1alvWUwqvW8B+DOb9TY5c5VZxQuRL+iwdrtwzFlKls= - distributions: sdist bdist_wheel - on: - tags: true - repo: oauthlib/oauthlib + - provider: pypi + user: ib.lundgren + password: + secure: PGZF9pRiTGCSwQjk1ddTKF3x4rQ0iAiPbg2uSixyO68uMXRgJjwHhSrNM0OEqtK5YWU5FE5L0DwR1nkrpEJKO4a5q2EOgos+gVoKpJfinoUNOOkjc1VHpqKM0uRf/OKrw1alvWUwqvW8B+DOb9TY5c5VZxQuRL+iwdrtwzFlKls= + distributions: sdist bdist_wheel + on: + tags: true + repo: oauthlib/oauthlib + - provider: releases + api_key: "$GITHUB_OAUTH_TOKEN" + skip_cleanup: true + on: + tags: true + repo: oauthlib/oauthlib -- cgit v1.2.1 From 649029506c354818c946a7d139a9a0a8054317ca Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Fri, 9 Mar 2018 20:16:42 +0000 Subject: Update requirements. --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index e4980c7..a4614bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -pyjwt==1.0.0 -blinker==1.3 -cryptography>=0.8.1 +pyjwt==1.6.0 +blinker==1.4 +cryptography>=1.4.0 -- cgit v1.2.1 From ec0e618587b0cf1cda26252fe3aae48dc69a17da Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 13:14:25 +0100 Subject: Fixed pypi credentials since oauthlib move. (#527) --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 06506e7..244862b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,9 +30,9 @@ notifications: on_start: never deploy: - provider: pypi - user: ib.lundgren + user: JonathanHuot password: - secure: PGZF9pRiTGCSwQjk1ddTKF3x4rQ0iAiPbg2uSixyO68uMXRgJjwHhSrNM0OEqtK5YWU5FE5L0DwR1nkrpEJKO4a5q2EOgos+gVoKpJfinoUNOOkjc1VHpqKM0uRf/OKrw1alvWUwqvW8B+DOb9TY5c5VZxQuRL+iwdrtwzFlKls= + secure: "OozNM16flVLvqDoNzmoTENchhS1w0/dEJZvXBQK2KWmh8fyGj2UZus1vkl6bA5V3Yu9MZLYFpDcltl/qraY3Up6iXQpwKz4q+ICygAudYM2kJ5l8ZEe+wy2FikWbD6LkXf5uKIJJnPNSC8AI86ZyxM/XZxbYjj/+jXyJ1YFZwwQ=" distributions: sdist bdist_wheel on: tags: true -- cgit v1.2.1 From e2f40a9ec3159f9ce326623004480f64c2167b76 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 19 Mar 2018 13:36:55 +0000 Subject: Fix Travis config (#529) * Fix indentation in Travis config. * Fill GitHub OAuth key. * Deploy tags from all branches. --- .travis.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 244862b..043f435 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,13 +33,16 @@ deploy: user: JonathanHuot password: secure: "OozNM16flVLvqDoNzmoTENchhS1w0/dEJZvXBQK2KWmh8fyGj2UZus1vkl6bA5V3Yu9MZLYFpDcltl/qraY3Up6iXQpwKz4q+ICygAudYM2kJ5l8ZEe+wy2FikWbD6LkXf5uKIJJnPNSC8AI86ZyxM/XZxbYjj/+jXyJ1YFZwwQ=" - distributions: sdist bdist_wheel - on: - tags: true - repo: oauthlib/oauthlib + distributions: sdist bdist_wheel + on: + tags: true + all_branches: true + repo: oauthlib/oauthlib - provider: releases - api_key: "$GITHUB_OAUTH_TOKEN" + api_key: + secure: "YwPR/xHcqKdBAo8bSfJqAj2aD8zEdVYbbeAoo+cl1OZkRnCC1WnsfXw865wC/gi/sSQUFvdHFGGdy5CY1Iv3E67W2CiJDN0X6AxQPrq50WUzk/pyUyvIBhsHKMU+FDYmRGU2cwOy8jlun9V9fzC2LkA9yyAklisePbIg0I3mhKA=" skip_cleanup: true on: tags: true + all_branches: true repo: oauthlib/oauthlib -- cgit v1.2.1 From 2a1992420970fbd2299a3aae834873c63e43fd6c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 14:49:43 +0100 Subject: Fixed indentation after travis setup --- .travis.yml | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 043f435..041510d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,27 @@ language: python sudo: false cache: pip - matrix: include: - - python: 2.7 - env: TOXENV=py27 - - python: 3.4 - env: TOXENV=py34 - - python: 3.5 - env: TOXENV=py35 - - python: 3.6 - env: TOXENV=py36 - - python: pypy-5.3 - env: TOXENV=pypy - + - python: 2.7 + env: TOXENV=py27 + - python: 3.4 + env: TOXENV=py34 + - python: 3.5 + env: TOXENV=py35 + - python: 3.6 + env: TOXENV=py36 + - python: pypy-5.3 + env: TOXENV=pypy install: - - pip install -U setuptools - - pip install tox coveralls +- pip install -U setuptools +- pip install tox coveralls script: tox - after_success: coveralls notifications: webhooks: urls: - - https://webhooks.gitter.im/e/6008c872bf0ecee344f4 + - https://webhooks.gitter.im/e/6008c872bf0ecee344f4 on_success: change on_failure: always on_start: never -- cgit v1.2.1 From 2128054a130ebdedfdee9b80801bfa4ad257c1ee Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 14:51:09 +0100 Subject: Generated api_key from travis setup releases --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 041510d..0a7d8ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ deploy: repo: oauthlib/oauthlib - provider: releases api_key: - secure: "YwPR/xHcqKdBAo8bSfJqAj2aD8zEdVYbbeAoo+cl1OZkRnCC1WnsfXw865wC/gi/sSQUFvdHFGGdy5CY1Iv3E67W2CiJDN0X6AxQPrq50WUzk/pyUyvIBhsHKMU+FDYmRGU2cwOy8jlun9V9fzC2LkA9yyAklisePbIg0I3mhKA=" + secure: LEzTaeQt4+Sp21t7usmwaEYLThKIGWDNNj04JADMLgfquTeyz5nDu9P8JNlT//G9RNN20oR8w7jZo97Y+JAylq6Hh/I+p/MEzZi8+NwIpObk3n3zJO4witZQQSTEw/6B7qf1/NQQxjQzlYTJjsGXxBps7srviWZmbH6Tz+epA3A= skip_cleanup: true on: tags: true -- cgit v1.2.1 From 43b66d8e60ed44b40b895f7e6d974665ccb43f82 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Fri, 9 Mar 2018 21:03:15 +0000 Subject: Version bump 2.0.7. (cherry picked from commit 67ebd7a) --- CHANGELOG.rst | 17 +++++++++++++++++ oauthlib/__init__.py | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8a20f92..9ced51a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,23 @@ Changelog ========= +2.0.7 (2018-03-09) +------------------ + +* Moved oauthlib into new organization on GitHub. +* Include license file in the generated wheel package. (#494) +* When deploying a release to PyPI, include the wheel distribution. (#496) +* Check access token in self.token dict. (#500) +* Added bottle-oauthlib to docs. (#509) +* Updated docs for organization change. (#515) +* Update repository location in Travis. (#514) +* Replace G+ with Gitter. (#517) + +2.0.6 (2017-10-20) +------------------ + +* 2.0.5 contains breaking changes. + 2.0.5 (2017-10-19) ------------------ diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 459f307..3645010 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -9,8 +9,8 @@ :license: BSD, see LICENSE for details. """ -__author__ = 'Idan Gazit ' -__version__ = '2.0.5' +__author__ = 'The OAuthlib Community' +__version__ = '2.0.7' import logging -- cgit v1.2.1 From 9d398229755e05e07c4d063a167a858236a200c5 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Sun, 18 Mar 2018 10:35:08 +0000 Subject: Update changelog for 2.0.7. (cherry picked from commit e7b906a) --- AUTHORS | 2 ++ CHANGELOG.rst | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 811679e..7d5d9ad 100644 --- a/AUTHORS +++ b/AUTHORS @@ -26,3 +26,5 @@ Juan Fabio García Solero Omer Katz Joel Stevenson Brendan McCollam +Jonathan Huot +Pieter Ennes diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9ced51a..7389af0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -2.0.7 (2018-03-09) +2.0.7 (2018-03-19) ------------------ * Moved oauthlib into new organization on GitHub. @@ -9,9 +9,15 @@ Changelog * When deploying a release to PyPI, include the wheel distribution. (#496) * Check access token in self.token dict. (#500) * Added bottle-oauthlib to docs. (#509) -* Updated docs for organization change. (#515) * Update repository location in Travis. (#514) +* Updated docs for organization change. (#515) * Replace G+ with Gitter. (#517) +* Update requirements. (#518) +* Add shields for Python versions, license and RTD. (#520) +* Fix ReadTheDocs build (#521). +* Fixed "make" command to test upstream with local oauthlib. (#522) +* Replace IRC notification with Gitter Hook. (#523) +* Added Github Releases deploy provider. (#523) 2.0.6 (2017-10-20) ------------------ -- cgit v1.2.1 From d49b9f02a821dca920c89b24540485da3b96bf1e Mon Sep 17 00:00:00 2001 From: Jimmy Thrasibule Date: Fri, 13 Apr 2018 04:27:01 -0400 Subject: Add request argument to confirm_redirect_uri (#504) (#504) --- examples/skeleton_oauth2_web_application_server.py | 2 +- oauthlib/oauth2/rfc6749/grant_types/authorization_code.py | 3 ++- oauthlib/oauth2/rfc6749/request_validator.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/skeleton_oauth2_web_application_server.py b/examples/skeleton_oauth2_web_application_server.py index 8bfd936..e53232f 100644 --- a/examples/skeleton_oauth2_web_application_server.py +++ b/examples/skeleton_oauth2_web_application_server.py @@ -67,7 +67,7 @@ class SkeletonValidator(RequestValidator): # state and user to request.scopes and request.user. pass - def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs): + def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request, *args, **kwargs): # You did save the redirect uri with the authorization code right? pass diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 7bea650..0660263 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -421,7 +421,8 @@ class AuthorizationCodeGrant(GrantTypeBase): # authorization request as described in Section 4.1.1, and their # values MUST be identical. if not self.request_validator.confirm_redirect_uri(request.client_id, request.code, - request.redirect_uri, request.client): + request.redirect_uri, request.client, + request): log.debug('Redirect_uri (%r) invalid for client %r (%r).', request.redirect_uri, request.client_id, request.client) raise errors.MismatchingRedirectURIError(request=request) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 182642e..c0b69a1 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -82,7 +82,7 @@ class RequestValidator(object): """ raise NotImplementedError('Subclasses must implement this method.') - def confirm_redirect_uri(self, client_id, code, redirect_uri, client, + def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request, *args, **kwargs): """Ensure that the authorization process represented by this authorization code began with this 'redirect_uri'. -- cgit v1.2.1 From d21fd53e13c044ad034694ee93e97eb7c4aac101 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Fri, 13 Apr 2018 10:32:01 +0200 Subject: Use secrets module in Python 3.6 and later (#533) The secrets module should be used for generating cryptographically strong random numbers suitable for managing data such as passwords, account authentication, security tokens, and related secrets. In particularly, secrets should be used in preference to the default pseudo-random number generator in the random module, which is designed for modelling and simulation, not security or cryptography. --- AUTHORS | 1 + docs/oauth1/security.rst | 12 +++++++----- oauthlib/common.py | 11 ++++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/AUTHORS b/AUTHORS index 7d5d9ad..f52ce9a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,3 +28,4 @@ Joel Stevenson Brendan McCollam Jonathan Huot Pieter Ennes +Olaf Conradi diff --git a/docs/oauth1/security.rst b/docs/oauth1/security.rst index a1432a9..df1e2a0 100644 --- a/docs/oauth1/security.rst +++ b/docs/oauth1/security.rst @@ -16,11 +16,13 @@ A few important facts regarding OAuth security * **Tokens must be random**, OAuthLib provides a method for generating secure tokens and it's packed into ``oauthlib.common.generate_token``, - use it. If you decide to roll your own, use ``random.SystemRandom`` - which is based on ``os.urandom`` rather than the default ``random`` - based on the effecient but not truly random Mersenne Twister. - Predictable tokens allow attackers to bypass virtually all defences - OAuth provides. + use it. If you decide to roll your own, use ``secrets.SystemRandom`` + for Python 3.6 and later. The ``secrets`` module is designed for + generating cryptographically strong random numbers. For earlier versions + of Python, use ``random.SystemRandom`` which is based on ``os.urandom`` + rather than the default ``random`` based on the effecient but not truly + random Mersenne Twister. Predictable tokens allow attackers to bypass + virtually all defences OAuth provides. * **Timing attacks are real** and more than possible if you host your application inside a shared datacenter. Ensure all ``validate_`` methods diff --git a/oauthlib/common.py b/oauthlib/common.py index afcc09c..f25656f 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -11,11 +11,16 @@ from __future__ import absolute_import, unicode_literals import collections import datetime import logging -import random import re import sys import time +try: + from secrets import randbits + from secrets import SystemRandom +except ImportError: + from random import getrandbits as randbits + from random import SystemRandom try: from urllib import quote as _quote from urllib import unquote as _unquote @@ -202,7 +207,7 @@ def generate_nonce(): .. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 .. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3 """ - return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp()) + return unicode_type(unicode_type(randbits(64)) + generate_timestamp()) def generate_timestamp(): @@ -225,7 +230,7 @@ def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET): and entropy when generating the random characters is important. Which is why SystemRandom is used instead of the default random.choice method. """ - rand = random.SystemRandom() + rand = SystemRandom() return ''.join(rand.choice(chars) for x in range(length)) -- cgit v1.2.1 From 1b3498aeac6f4c57156283e59d340746595d6329 Mon Sep 17 00:00:00 2001 From: paulie4 Date: Fri, 13 Apr 2018 04:39:07 -0400 Subject: Fixed some copy and paste typos (#535) Fixed some copy and paste typos, see issue #532. --- oauthlib/oauth2/rfc6749/clients/service_application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 84ea0e9..7f336bb 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -146,8 +146,8 @@ class ServiceApplicationClient(Client): ' token requests.') claim = { 'iss': issuer or self.issuer, - 'aud': audience or self.issuer, - 'sub': subject or self.issuer, + 'aud': audience or self.audience, + 'sub': subject or self.subject, 'exp': int(expires_at or time.time() + 3600), 'iat': int(issued_at or time.time()), } -- cgit v1.2.1 From 657065d76d59a100ffcacd0954fb2091552dfaa2 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Tue, 8 May 2018 21:14:35 +0100 Subject: Avoid populating spurious token credentials (#542) --- oauthlib/oauth2/rfc6749/clients/base.py | 19 ++++++++++++------- oauthlib/oauth2/rfc6749/clients/mobile_application.py | 2 +- oauthlib/oauth2/rfc6749/clients/web_application.py | 2 +- .../oauth2/rfc6749/clients/test_mobile_application.py | 12 ++++++++++++ tests/oauth2/rfc6749/clients/test_web_application.py | 19 +++++++++++++++++++ 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index a07a5c9..3c5372c 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -111,8 +111,10 @@ class Client(object): self.state_generator = state_generator self.state = state self.redirect_url = redirect_url + self.code = None + self.expires_in = None self._expires_at = None - self._populate_attributes(self.token) + self._populate_token_attributes(self.token) @property def token_types(self): @@ -406,7 +408,7 @@ class Client(object): .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 """ self.token = parse_token_response(body, scope=scope) - self._populate_attributes(self.token) + self._populate_token_attributes(self.token) return self.token def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs): @@ -459,8 +461,14 @@ class Client(object): hash_algorithm=self.mac_algorithm, **kwargs) return uri, headers, body - def _populate_attributes(self, response): - """Add commonly used values such as access_token to self.""" + def _populate_code_attributes(self, response): + """Add attributes from an auth code response to self.""" + + if 'code' in response: + self.code = response.get('code') + + def _populate_token_attributes(self, response): + """Add attributes from a token exchange response to self.""" if 'access_token' in response: self.access_token = response.get('access_token') @@ -478,9 +486,6 @@ class Client(object): if 'expires_at' in response: self._expires_at = int(response.get('expires_at')) - if 'code' in response: - self.code = response.get('code') - if 'mac_key' in response: self.mac_key = response.get('mac_key') diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py index 311aacf..965185d 100644 --- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -168,5 +168,5 @@ class MobileApplicationClient(Client): .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 """ self.token = parse_implicit_response(uri, state=state, scope=scope) - self._populate_attributes(self.token) + self._populate_token_attributes(self.token) return self.token diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index 14b5265..435c0b1 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -172,5 +172,5 @@ class WebApplicationClient(Client): oauthlib.oauth2.rfc6749.errors.MismatchingStateError """ response = parse_authorization_code_response(uri, state=state) - self._populate_attributes(response) + self._populate_code_attributes(response) return response diff --git a/tests/oauth2/rfc6749/clients/test_mobile_application.py b/tests/oauth2/rfc6749/clients/test_mobile_application.py index 309220b..51e4dab 100644 --- a/tests/oauth2/rfc6749/clients/test_mobile_application.py +++ b/tests/oauth2/rfc6749/clients/test_mobile_application.py @@ -69,6 +69,18 @@ class MobileApplicationClientTest(TestCase): uri = client.prepare_request_uri(self.uri, **self.kwargs) self.assertURLEqual(uri, self.uri_kwargs) + def test_populate_attributes(self): + + client = MobileApplicationClient(self.client_id) + + response_uri = (self.response_uri + "&code=EVIL-CODE") + + client.parse_request_uri_response(response_uri, scope=self.scope) + + # We must not accidentally pick up any further security + # credentials at this point. + self.assertIsNone(client.code) + def test_parse_token_response(self): client = MobileApplicationClient(self.client_id) diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 0a80c9a..4ecc3b3 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -117,6 +117,25 @@ class WebApplicationClientTest(TestCase): self.response_uri, state="invalid") + def test_populate_attributes(self): + + client = WebApplicationClient(self.client_id) + + response_uri = (self.response_uri + + "&access_token=EVIL-TOKEN" + "&refresh_token=EVIL-TOKEN" + "&mac_key=EVIL-KEY") + + client.parse_request_uri_response(response_uri, self.state) + + self.assertEqual(client.code, self.code) + + # We must not accidentally pick up any further security + # credentials at this point. + self.assertIsNone(client.access_token) + self.assertIsNone(client.refresh_token) + self.assertIsNone(client.mac_key) + def test_parse_token_response(self): client = WebApplicationClient(self.client_id) -- cgit v1.2.1 From a9d9ba17a0fe04cec5afa1c6ede96f1984ae7334 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Fri, 18 May 2018 19:04:06 +0100 Subject: Backward compatibility fix for requests-oauthlib. (#546) --- oauthlib/oauth2/rfc6749/clients/base.py | 14 ++++++++++---- oauthlib/oauth2/rfc6749/clients/mobile_application.py | 2 +- oauthlib/oauth2/rfc6749/clients/web_application.py | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 3c5372c..07ef894 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -9,6 +9,7 @@ for consuming OAuth 2.0 RFC6749. from __future__ import absolute_import, unicode_literals import time +import warnings from oauthlib.common import generate_token from oauthlib.oauth2.rfc6749 import tokens @@ -114,7 +115,7 @@ class Client(object): self.code = None self.expires_in = None self._expires_at = None - self._populate_token_attributes(self.token) + self.populate_token_attributes(self.token) @property def token_types(self): @@ -408,7 +409,7 @@ class Client(object): .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 """ self.token = parse_token_response(body, scope=scope) - self._populate_token_attributes(self.token) + self.populate_token_attributes(self.token) return self.token def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs): @@ -461,13 +462,18 @@ class Client(object): hash_algorithm=self.mac_algorithm, **kwargs) return uri, headers, body - def _populate_code_attributes(self, response): + def _populate_attributes(self, response): + warnings.warn("Please switch to the public method " + "populate_token_attributes.", DeprecationWarning) + return self.populate_token_attributes(response) + + def populate_code_attributes(self, response): """Add attributes from an auth code response to self.""" if 'code' in response: self.code = response.get('code') - def _populate_token_attributes(self, response): + def populate_token_attributes(self, response): """Add attributes from a token exchange response to self.""" if 'access_token' in response: diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py index 965185d..aa20daa 100644 --- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -168,5 +168,5 @@ class MobileApplicationClient(Client): .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 """ self.token = parse_implicit_response(uri, state=state, scope=scope) - self._populate_token_attributes(self.token) + self.populate_token_attributes(self.token) return self.token diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index 435c0b1..c14a5f8 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -172,5 +172,5 @@ class WebApplicationClient(Client): oauthlib.oauth2.rfc6749.errors.MismatchingStateError """ response = parse_authorization_code_response(uri, state=state) - self._populate_code_attributes(response) + self.populate_code_attributes(response) return response -- cgit v1.2.1 From 360e0c2ca2c97aacf615228534e9b8963f8359c2 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 20 May 2018 07:44:08 +0300 Subject: Don't cover the fallback branch. --- oauthlib/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/signals.py b/oauthlib/signals.py index 2f86650..22d47a4 100644 --- a/oauthlib/signals.py +++ b/oauthlib/signals.py @@ -8,7 +8,7 @@ signals_available = False try: from blinker import Namespace signals_available = True -except ImportError: +except ImportError: # noqa class Namespace(object): def signal(self, name, doc=None): return _FakeSignal(name, doc) -- cgit v1.2.1 From 6fc8ba83dcf9b644ab26d8b41cd0f8d74624dabd Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 20 May 2018 07:53:18 +0300 Subject: Added .coveragerc. --- .coveragerc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..c2c282e --- /dev/null +++ b/.coveragerc @@ -0,0 +1,21 @@ +[run] +branch = 1 +cover_pylib = 0 +include=*oauthlib/* +omit = oauthlib.tests.* + +[report] +omit = + */python?.?/* + */site-packages/* + */pypy/* +[report] +exclude_lines = + pragma: no cover + def __repr__ + if __debug__: + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: + noqa -- cgit v1.2.1 From abb9f8fdf96ab0467eb9ca6d1fb15bfbe87c4207 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 20 May 2018 07:55:10 +0300 Subject: Fix .coveragerc. --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index c2c282e..70666c7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,7 +9,6 @@ omit = */python?.?/* */site-packages/* */pypy/* -[report] exclude_lines = pragma: no cover def __repr__ -- cgit v1.2.1 From d7a9cf556e6794c0debb1af1d444e98375d0577e Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 20 May 2018 08:00:11 +0300 Subject: Ignore Python 2.7 fallback branch. --- oauthlib/oauth1/rfc5849/parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth1/rfc5849/parameters.py b/oauthlib/oauth1/rfc5849/parameters.py index 2f068a7..db4400e 100644 --- a/oauthlib/oauth1/rfc5849/parameters.py +++ b/oauthlib/oauth1/rfc5849/parameters.py @@ -15,7 +15,7 @@ from . import utils try: from urlparse import urlparse, urlunparse -except ImportError: +except ImportError: # noqa from urllib.parse import urlparse, urlunparse -- cgit v1.2.1 From a306b12b98b5d2aaf469b89b956db0df050823e7 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Mon, 21 May 2018 07:15:22 +0200 Subject: Add test coverage (#544) * Add testcase for prepare_token_request() * Add testcase for InsecureTransportError in add_token() * Fix typo in testcase of add_token() for MAC token type * Add testcase for TokenExpiredError in add_token() * Add testcase for prepare_request_body without private key * Add testcase for optional kwargs in prepare_request_body() --- tests/oauth2/rfc6749/clients/test_base.py | 72 +++++++++++++++++++++- .../rfc6749/clients/test_service_application.py | 70 ++++++++++++++++++++- 2 files changed, 137 insertions(+), 5 deletions(-) diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index c788bc1..d48a944 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, unicode_literals import datetime from oauthlib import common -from oauthlib.oauth2 import Client, InsecureTransportError +from oauthlib.oauth2 import Client, InsecureTransportError, TokenExpiredError from oauthlib.oauth2.rfc6749 import utils from oauthlib.oauth2.rfc6749.clients import AUTH_HEADER, BODY, URI_QUERY @@ -51,10 +51,26 @@ class ClientTest(TestCase): self.assertFormBodyEqual(body, self.body) self.assertEqual(headers, self.bearer_header) + # Non-HTTPS + insecure_uri = 'http://example.com/path?query=world' + client = Client(self.client_id, access_token=self.access_token, token_type="Bearer") + self.assertRaises(InsecureTransportError, client.add_token, insecure_uri, + body=self.body, + headers=self.headers) + # Missing access token client = Client(self.client_id) self.assertRaises(ValueError, client.add_token, self.uri) + # Expired token + expired = 523549800 + expired_token = { + 'expires_at': expired, + } + client = Client(self.client_id, token=expired_token, access_token=self.access_token, token_type="Bearer") + self.assertRaises(TokenExpiredError, client.add_token, self.uri, + body=self.body, headers=self.headers) + # The default token placement, bearer in auth header client = Client(self.client_id, access_token=self.access_token) uri, headers, body = client.add_token(self.uri, body=self.body, @@ -150,8 +166,26 @@ class ClientTest(TestCase): self.assertEqual(uri, self.uri) self.assertEqual(body, self.body) self.assertEqual(headers, self.mac_00_header) + # Non-HTTPS + insecure_uri = 'http://example.com/path?query=world' + self.assertRaises(InsecureTransportError, client.add_token, insecure_uri, + body=self.body, + headers=self.headers, + issue_time=datetime.datetime.now()) + # Expired Token + expired = 523549800 + expired_token = { + 'expires_at': expired, + } + client = Client(self.client_id, token=expired_token, token_type="MAC", + access_token=self.access_token, mac_key=self.mac_key, + mac_algorithm="hmac-sha-1") + self.assertRaises(TokenExpiredError, client.add_token, self.uri, + body=self.body, + headers=self.headers, + issue_time=datetime.datetime.now()) - # Add the Authorization header (draft 00) + # Add the Authorization header (draft 01) client = Client(self.client_id, token_type="MAC", access_token=self.access_token, mac_key=self.mac_key, mac_algorithm="hmac-sha-1") @@ -160,7 +194,24 @@ class ClientTest(TestCase): self.assertEqual(uri, self.uri) self.assertEqual(body, self.body) self.assertEqual(headers, self.mac_01_header) - + # Non-HTTPS + insecure_uri = 'http://example.com/path?query=world' + self.assertRaises(InsecureTransportError, client.add_token, insecure_uri, + body=self.body, + headers=self.headers, + draft=1) + # Expired Token + expired = 523549800 + expired_token = { + 'expires_at': expired, + } + client = Client(self.client_id, token=expired_token, token_type="MAC", + access_token=self.access_token, mac_key=self.mac_key, + mac_algorithm="hmac-sha-1") + self.assertRaises(TokenExpiredError, client.add_token, self.uri, + body=self.body, + headers=self.headers, + draft=1) def test_revocation_request(self): client = Client(self.client_id) @@ -208,6 +259,21 @@ class ClientTest(TestCase): # NotImplementedError self.assertRaises(NotImplementedError, client.prepare_authorization_request, auth_url) + def test_prepare_token_request(self): + redirect_url = 'https://example.com/callback/' + scopes = 'read' + token_url = 'https://example.com/token/' + state = 'fake_state' + + client = Client(self.client_id, scope=scopes, state=state) + + # Non-HTTPS + self.assertRaises(InsecureTransportError, + client.prepare_token_request, 'http://example.com/token/') + + # NotImplementedError + self.assertRaises(NotImplementedError, client.prepare_token_request, token_url) + def test_prepare_refresh_token_request(self): client = Client(self.client_id) diff --git a/tests/oauth2/rfc6749/clients/test_service_application.py b/tests/oauth2/rfc6749/clients/test_service_application.py index 2dc633a..dc337cf 100644 --- a/tests/oauth2/rfc6749/clients/test_service_application.py +++ b/tests/oauth2/rfc6749/clients/test_service_application.py @@ -89,8 +89,8 @@ mfvGGg3xNjTMO7IdrwIDAQAB audience=self.audience, body=self.body) r = Request('https://a.b', body=body) - self.assertEqual(r.isnot, 'empty') - self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type) + self.assertEqual(r.isnot, 'empty') + self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type) claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256']) @@ -98,6 +98,72 @@ mfvGGg3xNjTMO7IdrwIDAQAB # audience verification is handled during decode now self.assertEqual(claim['sub'], self.subject) self.assertEqual(claim['iat'], int(t.return_value)) + self.assertNotIn('nbf', claim) + self.assertNotIn('jti', claim) + + # Missing issuer parameter + self.assertRaises(ValueError, client.prepare_request_body, + issuer=None, subject=self.subject, audience=self.audience, body=self.body) + + # Missing subject parameter + self.assertRaises(ValueError, client.prepare_request_body, + issuer=self.issuer, subject=None, audience=self.audience, body=self.body) + + # Missing audience parameter + self.assertRaises(ValueError, client.prepare_request_body, + issuer=self.issuer, subject=self.subject, audience=None, body=self.body) + + # Optional kwargs + not_before = time() - 3600 + jwt_id = '8zd15df4s35f43sd' + body = client.prepare_request_body(issuer=self.issuer, + subject=self.subject, + audience=self.audience, + body=self.body, + not_before=not_before, + jwt_id=jwt_id) + + r = Request('https://a.b', body=body) + self.assertEqual(r.isnot, 'empty') + self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type) + + claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256']) + + self.assertEqual(claim['iss'], self.issuer) + # audience verification is handled during decode now + self.assertEqual(claim['sub'], self.subject) + self.assertEqual(claim['iat'], int(t.return_value)) + self.assertEqual(claim['nbf'], not_before) + self.assertEqual(claim['jti'], jwt_id) + + @patch('time.time') + def test_request_body_no_initial_private_key(self, t): + t.return_value = time() + self.token['expires_at'] = self.token['expires_in'] + t.return_value + + client = ServiceApplicationClient( + self.client_id, private_key=None) + + # Basic with private key provided + body = client.prepare_request_body(issuer=self.issuer, + subject=self.subject, + audience=self.audience, + body=self.body, + private_key=self.private_key) + r = Request('https://a.b', body=body) + self.assertEqual(r.isnot, 'empty') + self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type) + + claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256']) + + self.assertEqual(claim['iss'], self.issuer) + # audience verification is handled during decode now + self.assertEqual(claim['sub'], self.subject) + self.assertEqual(claim['iat'], int(t.return_value)) + + # No private key provided + self.assertRaises(ValueError, client.prepare_request_body, + issuer=self.issuer, subject=self.subject, audience=self.audience, body=self.body) @patch('time.time') def test_parse_token_response(self, t): -- cgit v1.2.1 From 6da09f284593546daac545d625f68014d7464c39 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 23 May 2018 16:59:22 +0200 Subject: Deploy only when building python36 with tox Avoid multiple deploy steps and lead to failures (e.g. errors "already deployed") --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0a7d8ad..dd72d5c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,7 @@ deploy: on: tags: true all_branches: true + condition: $TOXENV = py36 repo: oauthlib/oauthlib - provider: releases api_key: @@ -42,4 +43,5 @@ deploy: on: tags: true all_branches: true + condition: $TOXENV = py36 repo: oauthlib/oauthlib -- cgit v1.2.1 From 789220fc5b450ed72899d87961eef155fbd22fc6 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 23 Apr 2018 21:47:51 +0100 Subject: Prepare 2.1.0 release. (cherry picked from commit 5c76855) --- CHANGELOG.rst | 9 +++++++++ oauthlib/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7389af0..a8e1941 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog ========= +2.1.0 (2018-05-21) +------------------ + +* Fixed some copy and paste typos (#535) +* Use secrets module in Python 3.6 and later (#533) +* Add request argument to confirm_redirect_uri (#504) +* Avoid populating spurious token credentials (#542) +* Make populate attributes API public (#546) + 2.0.7 (2018-03-19) ------------------ diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 3645010..3393efe 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -10,7 +10,7 @@ """ __author__ = 'The OAuthlib Community' -__version__ = '2.0.7' +__version__ = '2.1.0' import logging -- cgit v1.2.1 From 27702f40753f88fc5bbf15128dac15758d4bc29a Mon Sep 17 00:00:00 2001 From: Mattia Procopio Date: Sat, 26 May 2018 21:33:41 +0200 Subject: Check that the Bearer header is properly formatted (#491) --- oauthlib/oauth2/rfc6749/tokens.py | 40 +++++++++++------- tests/oauth2/rfc6749/test_tokens.py | 81 +++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 15 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 4ae20e0..a7491f4 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -220,6 +220,24 @@ def signed_token_generator(private_pem, **kwargs): return signed_token_generator +def get_token_from_header(request): + """ + Helper function to extract a token from the request header. + :param request: The request object + :return: Return the token or None if the Authorization header is malformed. + """ + token = None + + if 'Authorization' in request.headers: + split_header = request.headers.get('Authorization').split() + if len(split_header) == 2 and split_header[0] == 'Bearer': + token = split_header[1] + else: + token = request.access_token + + return token + + class TokenBase(object): def __call__(self, request, refresh_token=False): @@ -286,16 +304,12 @@ class BearerToken(TokenBase): return token def validate_request(self, request): - token = None - if 'Authorization' in request.headers: - token = request.headers.get('Authorization')[7:] - else: - token = request.access_token + token = get_token_from_header(request) return self.request_validator.validate_bearer_token( token, request.scopes, request) def estimate_type(self, request): - if request.headers.get('Authorization', '').startswith('Bearer'): + if request.headers.get('Authorization', '').split(' ')[0] == 'Bearer': return 9 elif request.access_token is not None: return 5 @@ -331,17 +345,13 @@ class JWTToken(TokenBase): 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 + token = get_token_from_header(request) 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): + split_header = request.headers.get('Authorization', '').split() + + if len(split_header) == 2 and split_header[0] == 'Bearer' and split_header[1].startswith('ey') and split_header[1].count('.') in (2, 4): return 10 - else: - return 0 + return 0 diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index 570afb0..ecac03e 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals import mock +from oauthlib.common import Request from oauthlib.oauth2.rfc6749.tokens import * from ...unittest import TestCase @@ -61,9 +62,22 @@ class TokenTest(TestCase): bearer_headers = { 'Authorization': 'Bearer vF9dft4qmT' } + fake_bearer_headers = [ + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BeavervF9dft4qmT'}, + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BearerF9dft4qmT'}, + {'Authorization': 'Bearer vF9d ft4qmT'}, + ] + valid_header_with_multiple_spaces = {'Authorization': 'Bearer vF9dft4qmT'} bearer_body = 'access_token=vF9dft4qmT' bearer_uri = 'http://server.example.com/resource?access_token=vF9dft4qmT' + def _mocked_validate_bearer_token(self, token, scopes, request): + if not token: + return False + return True + def test_prepare_mac_header(self): """Verify mac signatures correctness @@ -83,8 +97,57 @@ class TokenTest(TestCase): self.assertEqual(prepare_bearer_body(self.token), self.bearer_body) self.assertEqual(prepare_bearer_uri(self.token, uri=self.uri), self.bearer_uri) + def test_fake_bearer_is_not_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + for fake_header in self.fake_bearer_headers: + request = Request('/', headers=fake_header) + result = BearerToken(request_validator=request_validator).validate_request(request) + + self.assertFalse(result) + + def test_header_with_multispaces_is_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + request = Request('/', headers=self.valid_header_with_multiple_spaces) + result = BearerToken(request_validator=request_validator).validate_request(request) + + self.assertTrue(result) + + def test_estimate_type_with_fake_header_returns_type_0(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + for fake_header in self.fake_bearer_headers: + request = Request('/', headers=fake_header) + result = BearerToken(request_validator=request_validator).estimate_type(request) + + if fake_header['Authorization'].count(' ') == 2 and \ + fake_header['Authorization'].split()[0] == 'Bearer': + # If we're dealing with the header containing 2 spaces, it will be recognized + # as a Bearer valid header, the token itself will be invalid by the way. + self.assertEqual(result, 9) + else: + self.assertEqual(result, 0) + class JWTTokenTestCase(TestCase): + fake_bearer_headers = [ + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BeavervF9dft4qmT'}, + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BearerF9dft4qmT'}, + {'Authorization': 'Bearer vF9df t4qmT'}, + ] + + valid_header_with_multiple_spaces = {'Authorization': 'Bearer vF9dft4qmT'} + + def _mocked_validate_bearer_token(self, token, scopes, request): + if not token: + return False + return True def test_create_token_callable_expires_in(self): """ @@ -180,6 +243,24 @@ class JWTTokenTestCase(TestCase): request.scopes, request) + def test_fake_bearer_is_not_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_jwt_bearer_token = self._mocked_validate_bearer_token + + for fake_header in self.fake_bearer_headers: + request = Request('/', headers=fake_header) + result = JWTToken(request_validator=request_validator).validate_request(request) + + self.assertFalse(result) + + def test_header_with_multiple_spaces_is_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_jwt_bearer_token = self._mocked_validate_bearer_token + request = Request('/', headers=self.valid_header_with_multiple_spaces) + result = JWTToken(request_validator=request_validator).validate_request(request) + + self.assertTrue(result) + def test_estimate_type(self): """ Estimate type results for a jwt token -- cgit v1.2.1 From fedc1d1b740a0407ec59152750bbbd9dc736b51d Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sun, 27 May 2018 03:38:05 +0800 Subject: Add missing NotImplementedError (#499) --- oauthlib/oauth2/rfc6749/clients/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 07ef894..406832d 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -143,6 +143,7 @@ class Client(object): def parse_request_uri_response(self, *args, **kwargs): """Abstract method used to parse redirection responses.""" + raise NotImplementedError("Must be implemented by inheriting classes.") def add_token(self, uri, http_method='GET', body=None, headers=None, token_placement=None, **kwargs): -- cgit v1.2.1 From a102731c88f496b557dedd4024fb9b82801d134a Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 20 May 2018 07:42:31 +0300 Subject: Remove Python 2.6 compatibility code. --- oauthlib/__init__.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 3393efe..b7586d2 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -8,18 +8,10 @@ :copyright: (c) 2011 by Idan Gazit. :license: BSD, see LICENSE for details. """ +import logging +from logging import NullHandler __author__ = 'The OAuthlib Community' __version__ = '2.1.0' - -import logging -try: # Python 2.7+ - from logging import NullHandler -except ImportError: - class NullHandler(logging.Handler): - - def emit(self, record): - pass - logging.getLogger('oauthlib').addHandler(NullHandler()) -- cgit v1.2.1 From d5a4d5ea0eab04ddddefac7d1e7a4902fc469286 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Tue, 5 Jun 2018 11:33:21 -0300 Subject: OpenID Connect split (#525) * Add command to clean up builds to makefile * Fix docs strings for endpoints pre_configured * Chnage grant_types.openid_connect to include a deprecation warning be a backward compatible * Fix doc string for rfc6749.request_validator * Remove unused import * Change import to be explicity * Move JWTTokenTestCase to openid.connect.core.test_token * Move JWTToken to oauthlib.openid.connect.core.tokens * Move to openid connect test * Move openid connect exceptions to its own file * Remove openid connect from oauth2 server * Remove JWTToken from oauth tokens * Remove grant_types.openid_connect file * Add oauthlib/openid estructure and tests --- Makefile | 17 +- oauthlib/oauth2/__init__.py | 2 +- .../oauth2/rfc6749/endpoints/pre_configured.py | 43 +- oauthlib/oauth2/rfc6749/errors.py | 123 ++---- oauthlib/oauth2/rfc6749/grant_types/__init__.py | 8 - .../oauth2/rfc6749/grant_types/openid_connect.py | 451 --------------------- oauthlib/oauth2/rfc6749/request_validator.py | 4 +- oauthlib/oauth2/rfc6749/tokens.py | 40 -- oauthlib/openid/__init__.py | 0 oauthlib/openid/connect/__init__.py | 0 oauthlib/openid/connect/core/__init__.py | 0 .../connect/core/endpoints/pre_configured.py | 103 +++++ oauthlib/openid/connect/core/exceptions.py | 152 +++++++ .../openid/connect/core/grant_types/__init__.py | 17 + .../connect/core/grant_types/authorization_code.py | 24 ++ oauthlib/openid/connect/core/grant_types/base.py | 283 +++++++++++++ .../openid/connect/core/grant_types/dispatchers.py | 86 ++++ .../openid/connect/core/grant_types/exceptions.py | 32 ++ oauthlib/openid/connect/core/grant_types/hybrid.py | 36 ++ .../openid/connect/core/grant_types/implicit.py | 28 ++ oauthlib/openid/connect/core/request_validator.py | 188 +++++++++ oauthlib/openid/connect/core/tokens.py | 54 +++ .../rfc6749/endpoints/test_claims_handling.py | 105 ----- .../test_openid_connect_params_handling.py | 85 ---- .../rfc6749/grant_types/test_openid_connect.py | 403 ------------------ tests/oauth2/rfc6749/test_server.py | 99 +++-- tests/oauth2/rfc6749/test_tokens.py | 203 +--------- tests/openid/__init__.py | 0 tests/openid/connect/__init__.py | 0 tests/openid/connect/core/__init__.py | 0 .../connect/core/endpoints/test_claims_handling.py | 109 +++++ .../test_openid_connect_params_handling.py | 85 ++++ .../core/grant_types/test_authorization_code.py | 153 +++++++ .../connect/core/grant_types/test_dispatchers.py | 125 ++++++ .../openid/connect/core/grant_types/test_hybrid.py | 13 + .../connect/core/grant_types/test_implicit.py | 148 +++++++ .../openid/connect/core/test_request_validator.py | 52 +++ tests/openid/connect/core/test_server.py | 178 ++++++++ tests/openid/connect/core/test_tokens.py | 133 ++++++ 39 files changed, 2105 insertions(+), 1477 deletions(-) delete mode 100644 oauthlib/oauth2/rfc6749/grant_types/openid_connect.py create mode 100644 oauthlib/openid/__init__.py create mode 100644 oauthlib/openid/connect/__init__.py create mode 100644 oauthlib/openid/connect/core/__init__.py create mode 100644 oauthlib/openid/connect/core/endpoints/pre_configured.py create mode 100644 oauthlib/openid/connect/core/exceptions.py create mode 100644 oauthlib/openid/connect/core/grant_types/__init__.py create mode 100644 oauthlib/openid/connect/core/grant_types/authorization_code.py create mode 100644 oauthlib/openid/connect/core/grant_types/base.py create mode 100644 oauthlib/openid/connect/core/grant_types/dispatchers.py create mode 100644 oauthlib/openid/connect/core/grant_types/exceptions.py create mode 100644 oauthlib/openid/connect/core/grant_types/hybrid.py create mode 100644 oauthlib/openid/connect/core/grant_types/implicit.py create mode 100644 oauthlib/openid/connect/core/request_validator.py create mode 100644 oauthlib/openid/connect/core/tokens.py delete mode 100644 tests/oauth2/rfc6749/endpoints/test_claims_handling.py delete mode 100644 tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py delete mode 100644 tests/oauth2/rfc6749/grant_types/test_openid_connect.py create mode 100644 tests/openid/__init__.py create mode 100644 tests/openid/connect/__init__.py create mode 100644 tests/openid/connect/core/__init__.py create mode 100644 tests/openid/connect/core/endpoints/test_claims_handling.py create mode 100644 tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py create mode 100644 tests/openid/connect/core/grant_types/test_authorization_code.py create mode 100644 tests/openid/connect/core/grant_types/test_dispatchers.py create mode 100644 tests/openid/connect/core/grant_types/test_hybrid.py create mode 100644 tests/openid/connect/core/grant_types/test_implicit.py create mode 100644 tests/openid/connect/core/test_request_validator.py create mode 100644 tests/openid/connect/core/test_server.py create mode 100644 tests/openid/connect/core/test_tokens.py diff --git a/Makefile b/Makefile index 8571a91..f9cc4ab 100644 --- a/Makefile +++ b/Makefile @@ -12,13 +12,27 @@ # Since these contacts will be addressed with Github mentions they # need to be Github users (for now)(sorry Bitbucket). # -clean: +clean: clean-eggs clean-build + @find . -iname '*.pyc' -delete + @find . -iname '*.pyo' -delete + @find . -iname '*~' -delete + @find . -iname '*.swp' -delete + @find . -iname '__pycache__' -delete rm -rf .tox rm -rf bottle-oauthlib rm -rf django-oauth-toolkit rm -rf flask-oauthlib rm -rf requests-oauthlib +clean-eggs: + @find . -name '*.egg' -print0|xargs -0 rm -rf -- + @rm -rf .eggs/ + +clean-build: + @rm -fr build/ + @rm -fr dist/ + @rm -fr *.egg-info + test: tox @@ -51,7 +65,6 @@ requests: cd requests-oauthlib 2>/dev/null || git clone https://github.com/requests/requests-oauthlib.git cd requests-oauthlib && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../[signedtoken],' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox - .DEFAULT_GOAL := all .PHONY: clean test bottle django flask requests all: clean test bottle django flask requests diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index dc7b431..303c6a1 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -24,7 +24,7 @@ from .rfc6749.endpoints import WebApplicationServer from .rfc6749.endpoints import MobileApplicationServer from .rfc6749.endpoints import LegacyApplicationServer from .rfc6749.endpoints import BackendApplicationServer -from .rfc6749.errors import AccessDeniedError, AccountSelectionRequired, ConsentRequired, FatalClientError, FatalOpenIDClientError, InsecureTransportError, InteractionRequired, InvalidClientError, InvalidClientIdError, InvalidGrantError, InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, InvalidScopeError, LoginRequired, MismatchingRedirectURIError, MismatchingStateError, MissingClientIdError, MissingCodeError, MissingRedirectURIError, MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, OAuth2Error, OpenIDClientError, ServerError, TemporarilyUnavailableError, TokenExpiredError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError +from .rfc6749.errors import AccessDeniedError, OAuth2Error, FatalClientError, InsecureTransportError, InvalidClientError, InvalidClientIdError, InvalidGrantError, InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError, MissingClientIdError, MissingCodeError, MissingRedirectURIError, MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, ServerError, TemporarilyUnavailableError, TokenExpiredError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError from .rfc6749.grant_types import AuthorizationCodeGrant from .rfc6749.grant_types import ImplicitGrant from .rfc6749.grant_types import ResourceOwnerPasswordCredentialsGrant diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 66af516..e2cc9db 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -1,22 +1,19 @@ # -*- coding: utf-8 -*- """ -oauthlib.oauth2.rfc6749 -~~~~~~~~~~~~~~~~~~~~~~~ +oauthlib.oauth2.rfc6749.endpoints.pre_configured +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This module is an implementation of various logic needed -for consuming and providing OAuth 2.0 RFC6749. +This module is an implementation of various endpoints needed +for providing OAuth 2.0 RFC6749 servers. """ from __future__ import absolute_import, unicode_literals -from ..grant_types import (AuthCodeGrantDispatcher, AuthorizationCodeGrant, - AuthTokenGrantDispatcher, +from ..grant_types import (AuthorizationCodeGrant, ClientCredentialsGrant, - ImplicitTokenGrantDispatcher, ImplicitGrant, - OpenIDConnectAuthCode, OpenIDConnectImplicit, - OpenIDConnectHybrid, + ImplicitGrant, RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant) -from ..tokens import BearerToken, JWTToken +from ..tokens import BearerToken from .authorization import AuthorizationEndpoint from .introspect import IntrospectEndpoint from .resource import ResourceEndpoint @@ -51,46 +48,28 @@ class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, request_validator) credentials_grant = ClientCredentialsGrant(request_validator) 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) - 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) - - # 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_choice, - 'id_token': openid_connect_implicit, - 'id_token token': openid_connect_implicit, - 'code token': openid_connect_hybrid, - 'code id_token': openid_connect_hybrid, - 'code id_token token': openid_connect_hybrid, + 'code': auth_grant, + 'token': implicit_grant, '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': token_grant_choice, + 'authorization_code': auth_grant, 'password': password_grant, 'client_credentials': credentials_grant, 'refresh_token': refresh_grant, }, default_token_type=bearer) ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': bearer, 'JWT': jwt}) + token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 1d5e98d..5a0cca2 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -274,106 +274,6 @@ class UnsupportedTokenTypeError(OAuth2Error): error = 'unsupported_token_type' -class FatalOpenIDClientError(FatalClientError): - pass - - -class OpenIDClientError(OAuth2Error): - pass - - -class InteractionRequired(OpenIDClientError): - """ - The Authorization Server requires End-User interaction to proceed. - - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface for End-User interaction. - """ - error = 'interaction_required' - status_code = 401 - - -class LoginRequired(OpenIDClientError): - """ - The Authorization Server requires End-User authentication. - - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface for End-User authentication. - """ - error = 'login_required' - status_code = 401 - - -class AccountSelectionRequired(OpenIDClientError): - """ - The End-User is REQUIRED to select a session at the Authorization Server. - - The End-User MAY be authenticated at the Authorization Server with - different associated accounts, but the End-User did not select a session. - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface to prompt for a session to - use. - """ - error = 'account_selection_required' - - -class ConsentRequired(OpenIDClientError): - """ - The Authorization Server requires End-User consent. - - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface for End-User consent. - """ - error = 'consent_required' - status_code = 401 - - -class InvalidRequestURI(OpenIDClientError): - """ - The request_uri in the Authorization Request returns an error or - contains invalid data. - """ - error = 'invalid_request_uri' - description = 'The request_uri in the Authorization Request returns an ' \ - 'error or contains invalid data.' - - -class InvalidRequestObject(OpenIDClientError): - """ - The request parameter contains an invalid Request Object. - """ - error = 'invalid_request_object' - description = 'The request parameter contains an invalid Request Object.' - - -class RequestNotSupported(OpenIDClientError): - """ - The OP does not support use of the request parameter. - """ - error = 'request_not_supported' - description = 'The request parameter is not supported.' - - -class RequestURINotSupported(OpenIDClientError): - """ - The OP does not support use of the request_uri parameter. - """ - error = 'request_uri_not_supported' - description = 'The request_uri parameter is not supported.' - - -class RegistrationNotSupported(OpenIDClientError): - """ - The OP does not support use of the registration parameter. - """ - error = 'registration_not_supported' - description = 'The registration parameter is not supported.' - - class InvalidTokenError(OAuth2Error): """ The access token provided is expired, revoked, malformed, or @@ -402,6 +302,29 @@ class InsufficientScopeError(OAuth2Error): "the access token.") +class ConsentRequired(OAuth2Error): + """ + The Authorization Server requires End-User consent. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User consent. + """ + error = 'consent_required' + status_code = 401 + +class LoginRequired(OAuth2Error): + """ + The Authorization Server requires End-User authentication. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User authentication. + """ + error = 'login_required' + status_code = 401 + + def raise_from_error(error, params=None): import inspect import sys diff --git a/oauthlib/oauth2/rfc6749/grant_types/__init__.py b/oauthlib/oauth2/rfc6749/grant_types/__init__.py index 2e4bfe4..2ec8e4f 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/__init__.py +++ b/oauthlib/oauth2/rfc6749/grant_types/__init__.py @@ -10,11 +10,3 @@ from .implicit import ImplicitGrant from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant from .client_credentials import ClientCredentialsGrant from .refresh_token import RefreshTokenGrant -from .openid_connect import OpenIDConnectBase -from .openid_connect import OpenIDConnectAuthCode -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/openid_connect.py b/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py deleted file mode 100644 index 4371b28..0000000 --- a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py +++ /dev/null @@ -1,451 +0,0 @@ -# -*- coding: utf-8 -*- -""" -oauthlib.oauth2.rfc6749.grant_types.openid_connect -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -""" -from __future__ import absolute_import, unicode_literals - -import datetime -import logging -from json import loads - -from ..errors import ConsentRequired, InvalidRequestError, LoginRequired -from ..request_validator import RequestValidator -from .authorization_code import AuthorizationCodeGrant -from .implicit import ImplicitGrant - -log = logging.getLogger(__name__) - - -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(OIDCNoPrompt, self).__init__(msg) - - -class AuthCodeGrantDispatcher(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_auth_grant=None, oidc_auth_grant=None): - self.default_auth_grant = default_auth_grant - self.oidc_auth_grant = oidc_auth_grant - - def _handler_for_request(self, request): - handler = self.default_auth_grant - - if request.scopes and "openid" in request.scopes: - handler = self.oidc_auth_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 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 - # proxy_target grant type handler, which will usually be either - # the standard OAuth2 AuthCode or Implicit grant types. - def __getattr__(self, attr): - return getattr(self.proxy_target, attr) - - def __setattr__(self, attr, value): - proxied_attrs = set(('refresh_token', 'response_types')) - if attr in proxied_attrs: - setattr(self.proxy_target, attr, value) - else: - super(OpenIDConnectBase, self).__setattr__(attr, value) - - def validate_authorization_request(self, request): - """Validates the OpenID Connect authorization request parameters. - - :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) - - 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 - if request.claims and not isinstance(request.claims, dict): - # specific claims are requested during the Authorization Request and may be requested for inclusion - # in either the id_token or the UserInfo endpoint response - # see http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter - try: - request.claims = loads(request.claims) - except Exception as ex: - raise InvalidRequestError(description="Malformed claims parameter", - uri="http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter") - - def add_id_token(self, token, token_handler, request): - # Treat it as normal OAuth 2 auth code request if openid is not present - if not request.scopes or 'openid' not in request.scopes: - return token - - # Only add an id token on auth/token step if asked for. - if request.response_type and 'id_token' not in request.response_type: - return token - - if 'state' not in token: - token['state'] = request.state - - if request.max_age: - d = datetime.datetime.utcnow() - token['auth_time'] = d.isoformat("T") + "Z" - - # TODO: acr claims (probably better handled by server code using oauthlib in get_id_token) - - token['id_token'] = self.request_validator.get_id_token(token, token_handler, request) - - return token - - def openid_authorization_validator(self, request): - """Perform OpenID Connect specific authorization request validation. - - nonce - OPTIONAL. String value used to associate a Client session with - an ID Token, and to mitigate replay attacks. The value is - passed through unmodified from the Authentication Request to - the ID Token. Sufficient entropy MUST be present in the nonce - values used to prevent attackers from guessing values - - display - OPTIONAL. ASCII string value that specifies how the - Authorization Server displays the authentication and consent - user interface pages to the End-User. The defined values are: - - page - The Authorization Server SHOULD display the - authentication and consent UI consistent with a full User - Agent page view. If the display parameter is not specified, - this is the default display mode. - - popup - The Authorization Server SHOULD display the - authentication and consent UI consistent with a popup User - Agent window. The popup User Agent window should be of an - appropriate size for a login-focused dialog and should not - obscure the entire window that it is popping up over. - - touch - The Authorization Server SHOULD display the - authentication and consent UI consistent with a device that - leverages a touch interface. - - wap - The Authorization Server SHOULD display the - authentication and consent UI consistent with a "feature - phone" type display. - - The Authorization Server MAY also attempt to detect the - capabilities of the User Agent and present an appropriate - display. - - 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. This can be used as a method to - check for existing authentication and/or consent. - - login - The Authorization Server SHOULD prompt the End-User - for reauthentication. If it cannot reauthenticate the - End-User, it MUST return an error, typically - login_required. - - consent - The Authorization Server SHOULD prompt the - End-User for consent before returning information to the - Client. If it cannot obtain consent, it MUST return an - error, typically consent_required. - - select_account - The Authorization Server SHOULD prompt the - End-User to select a user account. This enables an End-User - who has multiple accounts at the Authorization Server to - select amongst the multiple accounts that they might have - current sessions for. If it cannot obtain an account - selection choice made by the End-User, it MUST return an - error, typically account_selection_required. - - The prompt parameter can be used by the Client to make sure - that the End-User is still present for the current session or - to bring attention to the request. If this parameter contains - none with any other value, an error is returned. - - max_age - OPTIONAL. Maximum Authentication Age. Specifies the allowable - elapsed time in seconds since the last time the End-User was - actively authenticated by the OP. If the elapsed time is - greater than this value, the OP MUST attempt to actively - re-authenticate the End-User. (The max_age request parameter - corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] max_auth_age - request parameter.) When max_age is used, the ID Token returned - MUST include an auth_time Claim Value. - - ui_locales - OPTIONAL. End-User's preferred languages and scripts for the - user interface, represented as a space-separated list of BCP47 - [RFC5646] language tag values, ordered by preference. For - instance, the value "fr-CA fr en" represents a preference for - French as spoken in Canada, then French (without a region - designation), followed by English (without a region - designation). An error SHOULD NOT result if some or all of the - requested locales are not supported by the OpenID Provider. - - id_token_hint - OPTIONAL. ID Token previously issued by the Authorization - Server being passed as a hint about the End-User's current or - past authenticated session with the Client. If the End-User - identified by the ID Token is logged in or is logged in by the - request, then the Authorization Server returns a positive - response; otherwise, it SHOULD return an error, such as - login_required. When possible, an id_token_hint SHOULD be - present when prompt=none is used and an invalid_request error - MAY be returned if it is not; however, the server SHOULD - respond successfully when possible, even if it is not present. - The Authorization Server need not be listed as an audience of - the ID Token when it is used as an id_token_hint value. If the - ID Token received by the RP from the OP is encrypted, to use it - as an id_token_hint, the Client MUST decrypt the signed ID - Token contained within the encrypted ID Token. The Client MAY - re-encrypt the signed ID token to the Authentication Server - using a key that enables the server to decrypt the ID Token, - and use the re-encrypted ID token as the id_token_hint value. - - login_hint - OPTIONAL. Hint to the Authorization Server about the login - identifier the End-User might use to log in (if necessary). - This hint can be used by an RP if it first asks the End-User - for their e-mail address (or other identifier) and then wants - to pass that value as a hint to the discovered authorization - service. It is RECOMMENDED that the hint value match the value - used for discovery. This value MAY also be a phone number in - the format specified for the phone_number Claim. The use of - this parameter is left to the OP's discretion. - - acr_values - OPTIONAL. Requested Authentication Context Class Reference - values. Space-separated string that specifies the acr values - that the Authorization Server is being requested to use for - processing this Authentication Request, with the values - appearing in order of preference. The Authentication Context - Class satisfied by the authentication performed is returned as - the acr Claim Value, as specified in Section 2. The acr Claim - is requested as a Voluntary Claim by this parameter. - """ - - # Treat it as normal OAuth 2 auth code request if openid is not present - if not request.scopes or 'openid' not in request.scopes: - return {} - - prompt = request.prompt if request.prompt else [] - if hasattr(prompt, 'split'): - prompt = prompt.strip().split() - prompt = set(prompt) - - if 'none' in prompt: - - if len(prompt) > 1: - msg = "Prompt none is mutually exclusive with other values." - raise InvalidRequestError(request=request, description=msg) - - # prompt other than 'none' should be handled by the server code that - # uses oauthlib - if not request.id_token_hint: - msg = "Prompt is set to none yet id_token_hint is missing." - raise InvalidRequestError(request=request, description=msg) - - if not self.request_validator.validate_silent_login(request): - raise LoginRequired(request=request) - - if not self.request_validator.validate_silent_authorization(request): - raise ConsentRequired(request=request) - - self._inflate_claims(request) - - if not self.request_validator.validate_user_match( - request.id_token_hint, request.scopes, request.claims, request): - msg = "Session user does not match client supplied user." - raise LoginRequired(request=request, description=msg) - - request_info = { - 'display': request.display, - 'nonce': request.nonce, - 'prompt': prompt, - 'ui_locales': request.ui_locales.split() if request.ui_locales else [], - 'id_token_hint': request.id_token_hint, - 'login_hint': request.login_hint, - 'claims': request.claims - } - - return request_info - - def openid_implicit_authorization_validator(self, request): - """Additional validation when following the implicit flow. - """ - # Undefined in OpenID Connect, fall back to OAuth2 definition. - if request.response_type == 'token': - return {} - - # Treat it as normal OAuth 2 auth code request if openid is not present - if not request.scopes or 'openid' not in request.scopes: - return {} - - # REQUIRED. String value used to associate a Client session with an ID - # Token, and to mitigate replay attacks. The value is passed through - # unmodified from the Authentication Request to the ID Token. - # Sufficient entropy MUST be present in the nonce values used to - # prevent attackers from guessing values. For implementation notes, see - # Section 15.5.2. - if not request.nonce: - desc = 'Request is missing mandatory nonce parameter.' - raise InvalidRequestError(request=request, description=desc) - - return {} - - -class OpenIDConnectAuthCode(OpenIDConnectBase): - - def __init__(self, request_validator=None, **kwargs): - self.proxy_target = AuthorizationCodeGrant( - request_validator=request_validator, **kwargs) - self.custom_validators.post_auth.append( - self.openid_authorization_validator) - self.register_token_modifier(self.add_id_token) - - -class OpenIDConnectImplicit(OpenIDConnectBase): - - def __init__(self, request_validator=None, **kwargs): - self.proxy_target = ImplicitGrant( - request_validator=request_validator, **kwargs) - self.register_response_type('id_token') - self.register_response_type('id_token token') - self.custom_validators.post_auth.append( - self.openid_authorization_validator) - self.custom_validators.post_auth.append( - self.openid_implicit_authorization_validator) - self.register_token_modifier(self.add_id_token) - - -class OpenIDConnectHybrid(OpenIDConnectBase): - - def __init__(self, request_validator=None, **kwargs): - self.request_validator = request_validator or RequestValidator() - - self.proxy_target = AuthorizationCodeGrant( - request_validator=request_validator, **kwargs) - # All hybrid response types should be fragment-encoded. - self.proxy_target.default_response_mode = "fragment" - self.register_response_type('code id_token') - self.register_response_type('code token') - self.register_response_type('code id_token token') - self.custom_validators.post_auth.append( - self.openid_authorization_validator) - # Hybrid flows can return the id_token from the authorization - # endpoint as part of the 'code' response - self.register_code_modifier(self.add_token) - self.register_code_modifier(self.add_id_token) - self.register_token_modifier(self.add_id_token) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 56ecc3d..92edba6 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -oauthlib.oauth2.rfc6749.grant_types -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +oauthlib.oauth2.rfc6749.request_validator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """ from __future__ import absolute_import, unicode_literals diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index a7491f4..1d2b5eb 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -315,43 +315,3 @@ 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 = get_token_from_header(request) - return self.request_validator.validate_jwt_bearer_token( - token, request.scopes, request) - - def estimate_type(self, request): - split_header = request.headers.get('Authorization', '').split() - - if len(split_header) == 2 and split_header[0] == 'Bearer' and split_header[1].startswith('ey') and split_header[1].count('.') in (2, 4): - return 10 - return 0 diff --git a/oauthlib/openid/__init__.py b/oauthlib/openid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oauthlib/openid/connect/__init__.py b/oauthlib/openid/connect/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oauthlib/openid/connect/core/__init__.py b/oauthlib/openid/connect/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py new file mode 100644 index 0000000..3bcd24d --- /dev/null +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.endpoints.pre_configured +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various endpoints needed +for providing OpenID Connect servers. +""" +from __future__ import absolute_import, unicode_literals + +from ..grant_types import ( + AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, + ClientCredentialsGrant, + ImplicitGrant as OAuth2ImplicitGrant, + RefreshTokenGrant, + ResourceOwnerPasswordCredentialsGrant +) + +from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant +from oauthlib.openid.connect.core.grant_types.dispatchers import ( + AuthorizationCodeGrantDispatcher, + ImplicitTokenGrantDispatcher, + AuthorizationTokenGrantDispatcher +) +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant +from oauthlib.openid.connect.core.tokens import JWTToken + +from ..tokens import BearerToken +from .authorization import AuthorizationEndpoint +from .resource import ResourceEndpoint +from .revocation import RevocationEndpoint +from .token import TokenEndpoint + + +class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, + RevocationEndpoint): + + """An all-in-one endpoint featuring all four major grant types.""" + + def __init__(self, request_validator, token_expires_in=None, + token_generator=None, refresh_token_generator=None, + *args, **kwargs): + """Construct a new all-grants-in-one server. + + :param request_validator: An implementation of + oauthlib.oauth2.RequestValidator. + :param token_expires_in: An int or a function to generate a token + expiration offset (in seconds) given a + oauthlib.common.Request object. + :param token_generator: A function to generate a token from a request. + :param refresh_token_generator: A function to generate a token from a + request for the refresh token. + :param kwargs: Extra parameters to pass to authorization-, + token-, resource-, and revocation-endpoint constructors. + """ + auth_grant = OAuth2AuthorizationCodeGrant(request_validator) + implicit_grant = OAuth2ImplicitGrant(request_validator) + password_grant = ResourceOwnerPasswordCredentialsGrant( + request_validator) + credentials_grant = ClientCredentialsGrant(request_validator) + refresh_grant = RefreshTokenGrant(request_validator) + openid_connect_auth = AuthorizationCodeGrant(request_validator) + openid_connect_implicit = ImplicitGrant(request_validator) + openid_connect_hybrid = HybridGrant(request_validator) + + 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 = AuthorizationCodeGrantDispatcher(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_choice, + 'id_token': openid_connect_implicit, + 'id_token token': openid_connect_implicit, + '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 = AuthorizationTokenGrantDispatcher(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': token_grant_choice, + 'password': password_grant, + 'client_credentials': credentials_grant, + 'refresh_token': refresh_grant, + }, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer, 'JWT': jwt}) + RevocationEndpoint.__init__(self, request_validator) diff --git a/oauthlib/openid/connect/core/exceptions.py b/oauthlib/openid/connect/core/exceptions.py new file mode 100644 index 0000000..8b08d21 --- /dev/null +++ b/oauthlib/openid/connect/core/exceptions.py @@ -0,0 +1,152 @@ +# coding=utf-8 +""" +oauthlib.oauth2.rfc6749.errors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Error used both by OAuth 2 clients and providers to represent the spec +defined error responses for all four core grant types. +""" +from __future__ import unicode_literals + +from oauthlib.oauth2.rfc6749.errors import FatalClientError, OAuth2Error + + +class FatalOpenIDClientError(FatalClientError): + pass + + +class OpenIDClientError(OAuth2Error): + pass + + +class InteractionRequired(OpenIDClientError): + """ + The Authorization Server requires End-User interaction to proceed. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User interaction. + """ + error = 'interaction_required' + status_code = 401 + + +class LoginRequired(OpenIDClientError): + """ + The Authorization Server requires End-User authentication. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User authentication. + """ + error = 'login_required' + status_code = 401 + + +class AccountSelectionRequired(OpenIDClientError): + """ + The End-User is REQUIRED to select a session at the Authorization Server. + + The End-User MAY be authenticated at the Authorization Server with + different associated accounts, but the End-User did not select a session. + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface to prompt for a session to + use. + """ + error = 'account_selection_required' + + +class ConsentRequired(OpenIDClientError): + """ + The Authorization Server requires End-User consent. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User consent. + """ + error = 'consent_required' + status_code = 401 + + +class InvalidRequestURI(OpenIDClientError): + """ + The request_uri in the Authorization Request returns an error or + contains invalid data. + """ + error = 'invalid_request_uri' + description = 'The request_uri in the Authorization Request returns an ' \ + 'error or contains invalid data.' + + +class InvalidRequestObject(OpenIDClientError): + """ + The request parameter contains an invalid Request Object. + """ + error = 'invalid_request_object' + description = 'The request parameter contains an invalid Request Object.' + + +class RequestNotSupported(OpenIDClientError): + """ + The OP does not support use of the request parameter. + """ + error = 'request_not_supported' + description = 'The request parameter is not supported.' + + +class RequestURINotSupported(OpenIDClientError): + """ + The OP does not support use of the request_uri parameter. + """ + error = 'request_uri_not_supported' + description = 'The request_uri parameter is not supported.' + + +class RegistrationNotSupported(OpenIDClientError): + """ + The OP does not support use of the registration parameter. + """ + error = 'registration_not_supported' + description = 'The registration parameter is not supported.' + + +class InvalidTokenError(OAuth2Error): + """ + The access token provided is expired, revoked, malformed, or + invalid for other reasons. The resource SHOULD respond with + the HTTP 401 (Unauthorized) status code. The client MAY + request a new access token and retry the protected resource + request. + """ + error = 'invalid_token' + status_code = 401 + description = ("The access token provided is expired, revoked, malformed, " + "or invalid for other reasons.") + + +class InsufficientScopeError(OAuth2Error): + """ + The request requires higher privileges than provided by the + access token. The resource server SHOULD respond with the HTTP + 403 (Forbidden) status code and MAY include the "scope" + attribute with the scope necessary to access the protected + resource. + """ + error = 'insufficient_scope' + status_code = 403 + description = ("The request requires higher privileges than provided by " + "the access token.") + + +def raise_from_error(error, params=None): + import inspect + import sys + kwargs = { + 'description': params.get('error_description'), + 'uri': params.get('error_uri'), + 'state': params.get('state') + } + for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): + if cls.error == error: + raise cls(**kwargs) diff --git a/oauthlib/openid/connect/core/grant_types/__init__.py b/oauthlib/openid/connect/core/grant_types/__init__.py new file mode 100644 index 0000000..7fc183d --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import unicode_literals, absolute_import + +from .authorization_code import AuthorizationCodeGrant +from .implicit import ImplicitGrant +from .base import GrantTypeBase +from .hybrid import HybridGrant +from .exceptions import OIDCNoPrompt +from oauthlib.openid.connect.core.grant_types.dispatchers import ( + AuthorizationCodeGrantDispatcher, + ImplicitTokenGrantDispatcher, + AuthorizationTokenGrantDispatcher +) diff --git a/oauthlib/openid/connect/core/grant_types/authorization_code.py b/oauthlib/openid/connect/core/grant_types/authorization_code.py new file mode 100644 index 0000000..b0b1015 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/authorization_code.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant + +from .base import GrantTypeBase + +log = logging.getLogger(__name__) + + +class AuthorizationCodeGrant(GrantTypeBase): + + def __init__(self, request_validator=None, **kwargs): + self.proxy_target = OAuth2AuthorizationCodeGrant( + request_validator=request_validator, **kwargs) + self.custom_validators.post_auth.append( + self.openid_authorization_validator) + self.register_token_modifier(self.add_id_token) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py new file mode 100644 index 0000000..2bb48b1 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -0,0 +1,283 @@ +from .exceptions import OIDCNoPrompt + +import datetime +import logging +from json import loads + +from oauthlib.oauth2.rfc6749.errors import ConsentRequired, InvalidRequestError, LoginRequired + +log = logging.getLogger(__name__) + + +class GrantTypeBase(object): + + # Just proxy the majority of method calls through to the + # proxy_target grant type handler, which will usually be either + # the standard OAuth2 AuthCode or Implicit grant types. + def __getattr__(self, attr): + return getattr(self.proxy_target, attr) + + def __setattr__(self, attr, value): + proxied_attrs = set(('refresh_token', 'response_types')) + if attr in proxied_attrs: + setattr(self.proxy_target, attr, value) + else: + super(OpenIDConnectBase, self).__setattr__(attr, value) + + def validate_authorization_request(self, request): + """Validates the OpenID Connect authorization request parameters. + + :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) + + 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 + if request.claims and not isinstance(request.claims, dict): + # specific claims are requested during the Authorization Request and may be requested for inclusion + # in either the id_token or the UserInfo endpoint response + # see http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter + try: + request.claims = loads(request.claims) + except Exception as ex: + raise InvalidRequestError(description="Malformed claims parameter", + uri="http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter") + + def add_id_token(self, token, token_handler, request): + # Treat it as normal OAuth 2 auth code request if openid is not present + if not request.scopes or 'openid' not in request.scopes: + return token + + # Only add an id token on auth/token step if asked for. + if request.response_type and 'id_token' not in request.response_type: + return token + + if 'state' not in token: + token['state'] = request.state + + if request.max_age: + d = datetime.datetime.utcnow() + token['auth_time'] = d.isoformat("T") + "Z" + + # TODO: acr claims (probably better handled by server code using oauthlib in get_id_token) + + token['id_token'] = self.request_validator.get_id_token(token, token_handler, request) + + return token + + def openid_authorization_validator(self, request): + """Perform OpenID Connect specific authorization request validation. + + nonce + OPTIONAL. String value used to associate a Client session with + an ID Token, and to mitigate replay attacks. The value is + passed through unmodified from the Authentication Request to + the ID Token. Sufficient entropy MUST be present in the nonce + values used to prevent attackers from guessing values + + display + OPTIONAL. ASCII string value that specifies how the + Authorization Server displays the authentication and consent + user interface pages to the End-User. The defined values are: + + page - The Authorization Server SHOULD display the + authentication and consent UI consistent with a full User + Agent page view. If the display parameter is not specified, + this is the default display mode. + + popup - The Authorization Server SHOULD display the + authentication and consent UI consistent with a popup User + Agent window. The popup User Agent window should be of an + appropriate size for a login-focused dialog and should not + obscure the entire window that it is popping up over. + + touch - The Authorization Server SHOULD display the + authentication and consent UI consistent with a device that + leverages a touch interface. + + wap - The Authorization Server SHOULD display the + authentication and consent UI consistent with a "feature + phone" type display. + + The Authorization Server MAY also attempt to detect the + capabilities of the User Agent and present an appropriate + display. + + 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. This can be used as a method to + check for existing authentication and/or consent. + + login - The Authorization Server SHOULD prompt the End-User + for reauthentication. If it cannot reauthenticate the + End-User, it MUST return an error, typically + login_required. + + consent - The Authorization Server SHOULD prompt the + End-User for consent before returning information to the + Client. If it cannot obtain consent, it MUST return an + error, typically consent_required. + + select_account - The Authorization Server SHOULD prompt the + End-User to select a user account. This enables an End-User + who has multiple accounts at the Authorization Server to + select amongst the multiple accounts that they might have + current sessions for. If it cannot obtain an account + selection choice made by the End-User, it MUST return an + error, typically account_selection_required. + + The prompt parameter can be used by the Client to make sure + that the End-User is still present for the current session or + to bring attention to the request. If this parameter contains + none with any other value, an error is returned. + + max_age + OPTIONAL. Maximum Authentication Age. Specifies the allowable + elapsed time in seconds since the last time the End-User was + actively authenticated by the OP. If the elapsed time is + greater than this value, the OP MUST attempt to actively + re-authenticate the End-User. (The max_age request parameter + corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] max_auth_age + request parameter.) When max_age is used, the ID Token returned + MUST include an auth_time Claim Value. + + ui_locales + OPTIONAL. End-User's preferred languages and scripts for the + user interface, represented as a space-separated list of BCP47 + [RFC5646] language tag values, ordered by preference. For + instance, the value "fr-CA fr en" represents a preference for + French as spoken in Canada, then French (without a region + designation), followed by English (without a region + designation). An error SHOULD NOT result if some or all of the + requested locales are not supported by the OpenID Provider. + + id_token_hint + OPTIONAL. ID Token previously issued by the Authorization + Server being passed as a hint about the End-User's current or + past authenticated session with the Client. If the End-User + identified by the ID Token is logged in or is logged in by the + request, then the Authorization Server returns a positive + response; otherwise, it SHOULD return an error, such as + login_required. When possible, an id_token_hint SHOULD be + present when prompt=none is used and an invalid_request error + MAY be returned if it is not; however, the server SHOULD + respond successfully when possible, even if it is not present. + The Authorization Server need not be listed as an audience of + the ID Token when it is used as an id_token_hint value. If the + ID Token received by the RP from the OP is encrypted, to use it + as an id_token_hint, the Client MUST decrypt the signed ID + Token contained within the encrypted ID Token. The Client MAY + re-encrypt the signed ID token to the Authentication Server + using a key that enables the server to decrypt the ID Token, + and use the re-encrypted ID token as the id_token_hint value. + + login_hint + OPTIONAL. Hint to the Authorization Server about the login + identifier the End-User might use to log in (if necessary). + This hint can be used by an RP if it first asks the End-User + for their e-mail address (or other identifier) and then wants + to pass that value as a hint to the discovered authorization + service. It is RECOMMENDED that the hint value match the value + used for discovery. This value MAY also be a phone number in + the format specified for the phone_number Claim. The use of + this parameter is left to the OP's discretion. + + acr_values + OPTIONAL. Requested Authentication Context Class Reference + values. Space-separated string that specifies the acr values + that the Authorization Server is being requested to use for + processing this Authentication Request, with the values + appearing in order of preference. The Authentication Context + Class satisfied by the authentication performed is returned as + the acr Claim Value, as specified in Section 2. The acr Claim + is requested as a Voluntary Claim by this parameter. + """ + + # Treat it as normal OAuth 2 auth code request if openid is not present + if not request.scopes or 'openid' not in request.scopes: + return {} + + prompt = request.prompt if request.prompt else [] + if hasattr(prompt, 'split'): + prompt = prompt.strip().split() + prompt = set(prompt) + + if 'none' in prompt: + + if len(prompt) > 1: + msg = "Prompt none is mutually exclusive with other values." + raise InvalidRequestError(request=request, description=msg) + + # prompt other than 'none' should be handled by the server code that + # uses oauthlib + if not request.id_token_hint: + msg = "Prompt is set to none yet id_token_hint is missing." + raise InvalidRequestError(request=request, description=msg) + + if not self.request_validator.validate_silent_login(request): + raise LoginRequired(request=request) + + if not self.request_validator.validate_silent_authorization(request): + raise ConsentRequired(request=request) + + self._inflate_claims(request) + + if not self.request_validator.validate_user_match( + request.id_token_hint, request.scopes, request.claims, request): + msg = "Session user does not match client supplied user." + raise LoginRequired(request=request, description=msg) + + request_info = { + 'display': request.display, + 'nonce': request.nonce, + 'prompt': prompt, + 'ui_locales': request.ui_locales.split() if request.ui_locales else [], + 'id_token_hint': request.id_token_hint, + 'login_hint': request.login_hint, + 'claims': request.claims + } + + return request_info + + def openid_implicit_authorization_validator(self, request): + """Additional validation when following the implicit flow. + """ + # Undefined in OpenID Connect, fall back to OAuth2 definition. + if request.response_type == 'token': + return {} + + # Treat it as normal OAuth 2 auth code request if openid is not present + if not request.scopes or 'openid' not in request.scopes: + return {} + + # REQUIRED. String value used to associate a Client session with an ID + # Token, and to mitigate replay attacks. The value is passed through + # unmodified from the Authentication Request to the ID Token. + # Sufficient entropy MUST be present in the nonce values used to + # prevent attackers from guessing values. For implementation notes, see + # Section 15.5.2. + if not request.nonce: + desc = 'Request is missing mandatory nonce parameter.' + raise InvalidRequestError(request=request, description=desc) + + return {} + + +OpenIDConnectBase = GrantTypeBase diff --git a/oauthlib/openid/connect/core/grant_types/dispatchers.py b/oauthlib/openid/connect/core/grant_types/dispatchers.py new file mode 100644 index 0000000..2c33406 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/dispatchers.py @@ -0,0 +1,86 @@ +import logging +log = logging.getLogger(__name__) + + +class AuthorizationCodeGrantDispatcher(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_auth_grant=None, oidc_auth_grant=None): + self.default_auth_grant = default_auth_grant + self.oidc_auth_grant = oidc_auth_grant + + def _handler_for_request(self, request): + handler = self.default_auth_grant + + if request.scopes and "openid" in request.scopes: + handler = self.oidc_auth_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 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 AuthorizationTokenGrantDispatcher(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) diff --git a/oauthlib/openid/connect/core/grant_types/exceptions.py b/oauthlib/openid/connect/core/grant_types/exceptions.py new file mode 100644 index 0000000..809f1b3 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/exceptions.py @@ -0,0 +1,32 @@ +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(OIDCNoPrompt, self).__init__(msg) diff --git a/oauthlib/openid/connect/core/grant_types/hybrid.py b/oauthlib/openid/connect/core/grant_types/hybrid.py new file mode 100644 index 0000000..54669ae --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/hybrid.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant + +from .base import GrantTypeBase +from ..request_validator import RequestValidator + +log = logging.getLogger(__name__) + + +class HybridGrant(GrantTypeBase): + + def __init__(self, request_validator=None, **kwargs): + self.request_validator = request_validator or RequestValidator() + + self.proxy_target = OAuth2AuthorizationCodeGrant( + request_validator=request_validator, **kwargs) + # All hybrid response types should be fragment-encoded. + self.proxy_target.default_response_mode = "fragment" + self.register_response_type('code id_token') + self.register_response_type('code token') + self.register_response_type('code id_token token') + self.custom_validators.post_auth.append( + self.openid_authorization_validator) + # Hybrid flows can return the id_token from the authorization + # endpoint as part of the 'code' response + self.register_code_modifier(self.add_token) + self.register_code_modifier(self.add_id_token) + self.register_token_modifier(self.add_id_token) diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py new file mode 100644 index 0000000..0eaa5b3 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/implicit.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from .base import GrantTypeBase + +from oauthlib.oauth2.rfc6749.grant_types.implicit import ImplicitGrant as OAuth2ImplicitGrant + +log = logging.getLogger(__name__) + + +class ImplicitGrant(GrantTypeBase): + + def __init__(self, request_validator=None, **kwargs): + self.proxy_target = OAuth2ImplicitGrant( + request_validator=request_validator, **kwargs) + self.register_response_type('id_token') + self.register_response_type('id_token token') + self.custom_validators.post_auth.append( + self.openid_authorization_validator) + self.custom_validators.post_auth.append( + self.openid_implicit_authorization_validator) + self.register_token_modifier(self.add_id_token) diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py new file mode 100644 index 0000000..f3bcbdb --- /dev/null +++ b/oauthlib/openid/connect/core/request_validator.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.request_validator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from oauthlib.oauth2.rfc6749.request_validator import RequestValidator as OAuth2RequestValidator + +log = logging.getLogger(__name__) + + +class RequestValidator(OAuth2RequestValidator): + + 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 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. + + In addition to the standard OAuth2 request properties, the request may also contain + these OIDC specific properties which are useful to this method: + + - nonce, if workflow is implicit or hybrid and it was provided + - claims, if provided to the original Authorization Code request + + The token parameter is a dict which may contain an ``access_token`` entry, in which + case the resulting ID Token *should* include a calculated ``at_hash`` claim. + + Similarly, when the request parameter has a ``code`` property defined, the ID Token + *should* include a calculated ``c_hash`` claim. + + http://openid.net/specs/openid-connect-core-1_0.html (sections `3.1.3.6`_, `3.2.2.10`_, `3.3.2.11`_) + + .. _`3.1.3.6`: http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken + .. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken + .. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken + + :param token: A Bearer token dict + :param token_handler: the token handler (BearerToken class) + :param request: the HTTP Request (oauthlib.common.Request) + :return: The ID Token (a JWS signed JWT) + """ + # 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_silent_authorization(self, request): + """Ensure the logged in user has authorized silent OpenID authorization. + + Silent OpenID authorization allows access tokens and id tokens to be + granted to clients without any user prompt or interaction. + + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_silent_login(self, request): + """Ensure session user has authorized silent OpenID login. + + If no user is logged in or has not authorized silent login, this + method should return False. + + If the user is logged in but associated with multiple accounts and + not selected which one to link to the token then this method should + raise an oauthlib.oauth2.AccountSelectionRequired error. + + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_user_match(self, id_token_hint, scopes, claims, request): + """Ensure client supplied user id hint matches session user. + + If the sub claim or id_token_hint is supplied then the session + user must match the given ID. + + :param id_token_hint: User identifier string. + :param scopes: List of OAuth 2 scopes and OpenID claims (strings). + :param claims: OpenID Connect claims dict. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + raise NotImplementedError('Subclasses must implement this method.') diff --git a/oauthlib/openid/connect/core/tokens.py b/oauthlib/openid/connect/core/tokens.py new file mode 100644 index 0000000..6b68891 --- /dev/null +++ b/oauthlib/openid/connect/core/tokens.py @@ -0,0 +1,54 @@ +""" +authlib.openid.connect.core.tokens +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module contains methods for adding JWT tokens to requests. +""" +from __future__ import absolute_import, unicode_literals + + +from oauthlib.oauth2.rfc6749.tokens import TokenBase, random_token_generator + + +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/tests/oauth2/rfc6749/endpoints/test_claims_handling.py b/tests/oauth2/rfc6749/endpoints/test_claims_handling.py deleted file mode 100644 index ff72673..0000000 --- a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Ensure OpenID Connect Authorization Request 'claims' are preserved across authorization. - -The claims parameter is an optional query param for the Authorization Request endpoint - but if it is provided and is valid it needs to be deserialized (from urlencoded JSON) - and persisted with the authorization code itself, then in the subsequent Access Token - request the claims should be transferred (via the oauthlib request) to be persisted - with the Access Token when it is created. -""" -from __future__ import absolute_import, unicode_literals - -import mock - -from oauthlib.oauth2 import InvalidRequestError, RequestValidator, Server - -from ....unittest import TestCase -from .test_utils import get_fragment_credentials, get_query_credentials - - -class TestClaimsHandling(TestCase): - - DEFAULT_REDIRECT_URI = 'http://i.b./path' - - def set_scopes(self, scopes): - def set_request_scopes(client_id, code, client, request): - request.scopes = scopes - return True - return set_request_scopes - - def set_user(self, request): - request.user = 'foo' - request.client_id = 'bar' - request.client = mock.MagicMock() - request.client.client_id = 'mocked' - return True - - def set_client(self, request): - request.client = mock.MagicMock() - request.client.client_id = 'mocked' - return True - - def save_claims_with_code(self, client_id, code, request, *args, **kwargs): - # a real validator would save the claims with the code during save_authorization_code() - self.claims_from_auth_code_request = request.claims - self.scopes = request.scopes.split() - - def retrieve_claims_saved_with_code(self, client_id, code, client, request, *args, **kwargs): - request.claims = self.claims_from_auth_code_request - request.scopes = self.scopes - - return True - - def save_claims_with_bearer_token(self, token, request, *args, **kwargs): - # a real validator would save the claims with the access token during save_bearer_token() - self.claims_saved_with_bearer_token = request.claims - - def setUp(self): - self.validator = mock.MagicMock(spec=RequestValidator) - self.validator.get_default_redirect_uri.return_value = TestClaimsHandling.DEFAULT_REDIRECT_URI - self.validator.authenticate_client.side_effect = self.set_client - - self.validator.save_authorization_code.side_effect = self.save_claims_with_code - self.validator.validate_code.side_effect = self.retrieve_claims_saved_with_code - self.validator.save_token.side_effect = self.save_claims_with_bearer_token - - self.server = Server(self.validator) - - def test_claims_stored_on_code_creation(self): - - claims = { - "id_token": { - "claim_1": None, - "claim_2": { - "essential": True - } - }, - "userinfo": { - "claim_3": { - "essential": True - }, - "claim_4": None - } - } - - claims_urlquoted='%7B%22id_token%22%3A%20%7B%22claim_2%22%3A%20%7B%22essential%22%3A%20true%7D%2C%20%22claim_1%22%3A%20null%7D%2C%20%22userinfo%22%3A%20%7B%22claim_4%22%3A%20null%2C%20%22claim_3%22%3A%20%7B%22essential%22%3A%20true%7D%7D%7D' - uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=%s' - - h, b, s = self.server.create_authorization_response(uri % claims_urlquoted, scopes='openid test_scope') - - self.assertDictEqual(self.claims_from_auth_code_request, claims) - - code = get_query_credentials(h['Location'])['code'][0] - token_uri = 'http://example.com/path' - _, body, _ = self.server.create_token_response(token_uri, - 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) - - def test_invalid_claims(self): - uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=this-is-not-json' - - h, b, s = self.server.create_authorization_response(uri, scopes='openid test_scope') - error = get_query_credentials(h['Location'])['error'][0] - error_desc = get_query_credentials(h['Location'])['error_description'][0] - self.assertEqual(error, 'invalid_request') - self.assertEqual(error_desc, "Malformed claims parameter") diff --git a/tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py b/tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py deleted file mode 100644 index 89431b6..0000000 --- a/tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py +++ /dev/null @@ -1,85 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import mock - -from oauthlib.oauth2 import InvalidRequestError -from oauthlib.oauth2.rfc6749.endpoints.authorization import \ - AuthorizationEndpoint -from oauthlib.oauth2.rfc6749.grant_types import OpenIDConnectAuthCode -from oauthlib.oauth2.rfc6749.tokens import BearerToken - -from ....unittest import TestCase - -try: - from urllib.parse import urlencode -except ImportError: - from urllib import urlencode - - - - -class OpenIDConnectEndpointTest(TestCase): - - def setUp(self): - self.mock_validator = mock.MagicMock() - self.mock_validator.authenticate_client.side_effect = self.set_client - grant = OpenIDConnectAuthCode(request_validator=self.mock_validator) - bearer = BearerToken(self.mock_validator) - self.endpoint = AuthorizationEndpoint(grant, bearer, - response_types={'code': grant}) - params = { - 'prompt': 'consent', - 'display': 'touch', - 'nonce': 'abcd', - 'state': 'abc', - 'redirect_uri': 'https://a.b/cb', - 'response_type': 'code', - 'client_id': 'abcdef', - 'scope': 'hello openid' - } - self.url = 'http://a.b/path?' + urlencode(params) - - def set_client(self, request): - request.client = mock.MagicMock() - request.client.client_id = 'mocked' - return True - - @mock.patch('oauthlib.common.generate_token') - def test_authorization_endpoint_handles_prompt(self, generate_token): - generate_token.return_value = "MOCK_CODE" - # In the GET view: - scopes, creds = self.endpoint.validate_authorization_request(self.url) - # In the POST view: - creds['scopes'] = scopes - h, b, s = self.endpoint.create_authorization_response(self.url, - credentials=creds) - expected = 'https://a.b/cb?state=abc&code=MOCK_CODE' - self.assertURLEqual(h['Location'], expected) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - def test_prompt_none_exclusiveness(self): - """ - Test that prompt=none can't be used with another prompt value. - """ - params = { - 'prompt': 'none consent', - 'state': 'abc', - 'redirect_uri': 'https://a.b/cb', - 'response_type': 'code', - 'client_id': 'abcdef', - 'scope': 'hello openid' - } - url = 'http://a.b/path?' + urlencode(params) - with self.assertRaises(InvalidRequestError): - self.endpoint.validate_authorization_request(url) - - def test_oidc_params_preservation(self): - """ - Test that the nonce parameter is passed through. - """ - scopes, creds = self.endpoint.validate_authorization_request(self.url) - - self.assertEqual(creds['prompt'], {'consent'}) - self.assertEqual(creds['nonce'], 'abcd') - self.assertEqual(creds['display'], 'touch') diff --git a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py b/tests/oauth2/rfc6749/grant_types/test_openid_connect.py deleted file mode 100644 index 573d491..0000000 --- a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py +++ /dev/null @@ -1,403 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import json - -import mock - -from oauthlib.common import Request -from oauthlib.oauth2.rfc6749.grant_types import (AuthTokenGrantDispatcher, - AuthorizationCodeGrant, - ImplicitGrant, - ImplicitTokenGrantDispatcher, - OIDCNoPrompt, - OpenIDConnectAuthCode, - OpenIDConnectHybrid, - OpenIDConnectImplicit) -from oauthlib.oauth2.rfc6749.tokens import BearerToken - -from ....unittest import TestCase -from .test_authorization_code import AuthorizationCodeGrantTest -from .test_implicit import ImplicitGrantTest - - -class OpenIDAuthCodeInterferenceTest(AuthorizationCodeGrantTest): - """Test that OpenID don't interfere with normal OAuth 2 flows.""" - - def setUp(self): - 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.""" - - def setUp(self): - super(OpenIDImplicitInterferenceTest, self).setUp() - self.auth = OpenIDConnectImplicit(request_validator=self.mock_validator) - - -class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): - """Test that OpenID don't interfere with normal OAuth 2 flows.""" - - def setUp(self): - super(OpenIDHybridInterferenceTest, self).setUp() - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - - -def get_id_token_mock(token, token_handler, request): - return "MOCKED_TOKEN" - - -class OpenIDAuthCodeTest(TestCase): - - def setUp(self): - self.request = Request('http://a.b/path') - self.request.scopes = ('hello', 'openid') - self.request.expires_in = 1800 - self.request.client_id = 'abcdef' - self.request.code = '1234' - self.request.response_type = 'code' - self.request.grant_type = 'authorization_code' - self.request.redirect_uri = 'https://a.b/cb' - self.request.state = 'abc' - - self.mock_validator = mock.MagicMock() - self.mock_validator.authenticate_client.side_effect = self.set_client - self.mock_validator.get_id_token.side_effect = get_id_token_mock - self.auth = OpenIDConnectAuthCode(request_validator=self.mock_validator) - - self.url_query = 'https://a.b/cb?code=abc&state=abc' - self.url_fragment = 'https://a.b/cb#code=abc&state=abc' - - def set_client(self, request): - request.client = mock.MagicMock() - request.client.client_id = 'mocked' - return True - - @mock.patch('oauthlib.common.generate_token') - def test_authorization(self, generate_token): - - scope, info = self.auth.validate_authorization_request(self.request) - - generate_token.return_value = 'abc' - bearer = BearerToken(self.mock_validator) - self.request.response_mode = 'query' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_query) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - 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) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - @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) - - # prompt == none requires id token hint - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - 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_query) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - # Test alernative 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) - - # 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 - 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 - 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 - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=login_required', h['Location']) - - def set_scopes(self, client_id, code, client, request): - request.scopes = self.request.scopes - request.state = self.request.state - request.user = 'bob' - return True - - def test_create_token_response(self): - self.request.response_type = None - self.mock_validator.validate_code.side_effect = self.set_scopes - - bearer = BearerToken(self.mock_validator) - - h, token, s = self.auth.create_token_response(self.request, bearer) - token = json.loads(token) - self.assertEqual(self.mock_validator.save_token.call_count, 1) - self.assertIn('access_token', token) - self.assertIn('refresh_token', token) - self.assertIn('expires_in', token) - self.assertIn('scope', token) - self.assertIn('id_token', token) - self.assertIn('openid', token['scope']) - - self.mock_validator.reset_mock() - - self.request.scopes = ('hello', 'world') - h, token, s = self.auth.create_token_response(self.request, bearer) - token = json.loads(token) - self.assertEqual(self.mock_validator.save_token.call_count, 1) - self.assertIn('access_token', token) - self.assertIn('refresh_token', token) - self.assertIn('expires_in', token) - self.assertIn('scope', token) - self.assertNotIn('id_token', token) - self.assertNotIn('openid', token['scope']) - - -class OpenIDImplicitTest(TestCase): - - def setUp(self): - self.request = Request('http://a.b/path') - self.request.scopes = ('hello', 'openid') - self.request.expires_in = 1800 - self.request.client_id = 'abcdef' - self.request.response_type = 'id_token token' - self.request.redirect_uri = 'https://a.b/cb' - self.request.nonce = 'zxc' - self.request.state = 'abc' - - self.mock_validator = mock.MagicMock() - self.mock_validator.get_id_token.side_effect = get_id_token_mock - self.auth = OpenIDConnectImplicit(request_validator=self.mock_validator) - - token = 'MOCKED_TOKEN' - self.url_query = 'https://a.b/cb?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#state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - - @mock.patch('oauthlib.common.generate_token') - def test_authorization(self, generate_token): - scope, info = self.auth.validate_authorization_request(self.request) - - generate_token.return_value = 'abc' - bearer = BearerToken(self.mock_validator) - - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.response_type = 'id_token' - token = 'MOCKED_TOKEN' - url = 'https://a.b/cb#state=abc&id_token=%s' % token - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], url, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.nonce = None - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - @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) - - # prompt == none requires id token hint - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - 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.assertEqual(b, None) - self.assertEqual(s, 302) - - # Test alernative response modes - self.request.response_mode = 'query' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_query) - - # 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 - 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 - 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 - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=login_required', h['Location']) - - -class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeTokenTest, self).setUp() - self.request.response_type = 'code token' - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - 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): - super(OpenIDHybridCodeIdTokenTest, self).setUp() - self.request.response_type = 'code id_token' - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - token = 'MOCKED_TOKEN' - 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): - super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() - self.request.response_type = 'code id_token token' - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - 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 da303ce..bc7a2b7 100644 --- a/tests/oauth2/rfc6749/test_server.py +++ b/tests/oauth2/rfc6749/test_server.py @@ -3,21 +3,17 @@ from __future__ import absolute_import, unicode_literals import json -import jwt import mock from oauthlib import common from oauthlib.oauth2.rfc6749 import errors, tokens from oauthlib.oauth2.rfc6749.endpoints import Server -from oauthlib.oauth2.rfc6749.endpoints.authorization import \ - AuthorizationEndpoint +from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint from oauthlib.oauth2.rfc6749.endpoints.resource import ResourceEndpoint from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint from oauthlib.oauth2.rfc6749.grant_types import (AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant, - OpenIDConnectAuthCode, - OpenIDConnectImplicit, ResourceOwnerPasswordCredentialsGrant) from ...unittest import TestCase @@ -29,40 +25,34 @@ class AuthorizationEndpointTest(TestCase): self.mock_validator = mock.MagicMock() self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) auth_code = AuthorizationCodeGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) auth_code.save_authorization_code = mock.MagicMock() implicit = ImplicitGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) implicit.save_token = mock.MagicMock() - openid_connect_auth = OpenIDConnectAuthCode(self.mock_validator) - openid_connect_implicit = OpenIDConnectImplicit(self.mock_validator) - response_types = { - 'code': auth_code, - 'token': implicit, - - '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, - 'none': auth_code + 'code': auth_code, + 'token': implicit, + 'none': auth_code } self.expires_in = 1800 - token = tokens.BearerToken(self.mock_validator, - expires_in=self.expires_in) + token = tokens.BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) self.endpoint = AuthorizationEndpoint( - default_response_type='code', - default_token_type=token, - response_types=response_types) + default_response_type='code', + default_token_type=token, + response_types=response_types + ) @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?code=abc&state=xyz') @@ -71,7 +61,7 @@ class AuthorizationEndpointTest(TestCase): uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me#access_token=abc&expires_in=' + str(self.expires_in) + '&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True) @@ -79,7 +69,7 @@ class AuthorizationEndpointTest(TestCase): uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them&state=xyz' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?state=xyz', parse_fragment=True) self.assertEqual(body, None) @@ -99,9 +89,9 @@ class AuthorizationEndpointTest(TestCase): uri = 'http://i.b/l?client_id=me&scope=all+of+them' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' self.mock_validator.validate_request = mock.MagicMock( - side_effect=errors.InvalidRequestError()) + side_effect=errors.InvalidRequestError()) headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.') @@ -109,9 +99,9 @@ class AuthorizationEndpointTest(TestCase): uri = 'http://i.b/l?response_type=invalid&client_id=me&scope=all+of+them' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' self.mock_validator.validate_request = mock.MagicMock( - side_effect=errors.UnsupportedResponseTypeError()) + side_effect=errors.UnsupportedResponseTypeError()) headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?error=unsupported_response_type') @@ -129,27 +119,32 @@ class TokenEndpointTest(TestCase): self.mock_validator.authenticate_client.side_effect = set_user self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) auth_code = AuthorizationCodeGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) password = ResourceOwnerPasswordCredentialsGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) client = ClientCredentialsGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) supported_types = { - 'authorization_code': auth_code, - 'password': password, - 'client_credentials': client, + 'authorization_code': auth_code, + 'password': password, + 'client_credentials': client, } self.expires_in = 1800 - token = tokens.BearerToken(self.mock_validator, - expires_in=self.expires_in) - self.endpoint = TokenEndpoint('authorization_code', - default_token_type=token, grant_types=supported_types) + token = tokens.BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) + self.endpoint = TokenEndpoint( + 'authorization_code', + default_token_type=token, + grant_types=supported_types + ) @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' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, @@ -176,7 +171,7 @@ class TokenEndpointTest(TestCase): def test_password_grant(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, @@ -190,7 +185,7 @@ class TokenEndpointTest(TestCase): def test_client_grant(self): body = 'grant_type=client_credentials&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, @@ -281,7 +276,7 @@ twIDAQAB def test_authorization_grant(self): 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=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -295,7 +290,7 @@ twIDAQAB 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=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -310,7 +305,7 @@ twIDAQAB def test_password_grant(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -325,7 +320,7 @@ twIDAQAB def test_scopes_and_user_id_stored_in_access_token(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) access_token = json.loads(body)['access_token'] @@ -338,7 +333,7 @@ twIDAQAB def test_client_grant(self): body = 'grant_type=client_credentials&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -366,8 +361,10 @@ class ResourceEndpointTest(TestCase): self.mock_validator = mock.MagicMock() self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) token = tokens.BearerToken(request_validator=self.mock_validator) - self.endpoint = ResourceEndpoint(default_token='Bearer', - token_types={'Bearer': token}) + self.endpoint = ResourceEndpoint( + default_token='Bearer', + token_types={'Bearer': token} + ) def test_defaults(self): uri = 'http://a.b/path?some=query' diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index ecac03e..061754f 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -1,9 +1,11 @@ from __future__ import absolute_import, unicode_literals -import mock - -from oauthlib.common import Request -from oauthlib.oauth2.rfc6749.tokens import * +from oauthlib.oauth2.rfc6749.tokens import ( + prepare_mac_header, + prepare_bearer_headers, + prepare_bearer_body, + prepare_bearer_uri, +) from ...unittest import TestCase @@ -96,196 +98,3 @@ 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) - - def test_fake_bearer_is_not_validated(self): - request_validator = mock.MagicMock() - request_validator.validate_bearer_token = self._mocked_validate_bearer_token - - for fake_header in self.fake_bearer_headers: - request = Request('/', headers=fake_header) - result = BearerToken(request_validator=request_validator).validate_request(request) - - self.assertFalse(result) - - def test_header_with_multispaces_is_validated(self): - request_validator = mock.MagicMock() - request_validator.validate_bearer_token = self._mocked_validate_bearer_token - - request = Request('/', headers=self.valid_header_with_multiple_spaces) - result = BearerToken(request_validator=request_validator).validate_request(request) - - self.assertTrue(result) - - def test_estimate_type_with_fake_header_returns_type_0(self): - request_validator = mock.MagicMock() - request_validator.validate_bearer_token = self._mocked_validate_bearer_token - - for fake_header in self.fake_bearer_headers: - request = Request('/', headers=fake_header) - result = BearerToken(request_validator=request_validator).estimate_type(request) - - if fake_header['Authorization'].count(' ') == 2 and \ - fake_header['Authorization'].split()[0] == 'Bearer': - # If we're dealing with the header containing 2 spaces, it will be recognized - # as a Bearer valid header, the token itself will be invalid by the way. - self.assertEqual(result, 9) - else: - self.assertEqual(result, 0) - - -class JWTTokenTestCase(TestCase): - fake_bearer_headers = [ - {'Authorization': 'Beaver vF9dft4qmT'}, - {'Authorization': 'BeavervF9dft4qmT'}, - {'Authorization': 'Beaver vF9dft4qmT'}, - {'Authorization': 'BearerF9dft4qmT'}, - {'Authorization': 'Bearer vF9df t4qmT'}, - ] - - valid_header_with_multiple_spaces = {'Authorization': 'Bearer vF9dft4qmT'} - - def _mocked_validate_bearer_token(self, token, scopes, request): - if not token: - return False - return True - - 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_fake_bearer_is_not_validated(self): - request_validator = mock.MagicMock() - request_validator.validate_jwt_bearer_token = self._mocked_validate_bearer_token - - for fake_header in self.fake_bearer_headers: - request = Request('/', headers=fake_header) - result = JWTToken(request_validator=request_validator).validate_request(request) - - self.assertFalse(result) - - def test_header_with_multiple_spaces_is_validated(self): - request_validator = mock.MagicMock() - request_validator.validate_jwt_bearer_token = self._mocked_validate_bearer_token - request = Request('/', headers=self.valid_header_with_multiple_spaces) - result = JWTToken(request_validator=request_validator).validate_request(request) - - self.assertTrue(result) - - 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) diff --git a/tests/openid/__init__.py b/tests/openid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/openid/connect/__init__.py b/tests/openid/connect/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/openid/connect/core/__init__.py b/tests/openid/connect/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/openid/connect/core/endpoints/test_claims_handling.py b/tests/openid/connect/core/endpoints/test_claims_handling.py new file mode 100644 index 0000000..37a7cdd --- /dev/null +++ b/tests/openid/connect/core/endpoints/test_claims_handling.py @@ -0,0 +1,109 @@ +"""Ensure OpenID Connect Authorization Request 'claims' are preserved across authorization. + +The claims parameter is an optional query param for the Authorization Request endpoint + but if it is provided and is valid it needs to be deserialized (from urlencoded JSON) + and persisted with the authorization code itself, then in the subsequent Access Token + request the claims should be transferred (via the oauthlib request) to be persisted + with the Access Token when it is created. +""" +from __future__ import absolute_import, unicode_literals + +import mock + +from oauthlib.oauth2 import RequestValidator + +from oauthlib.oauth2.rfc6749.endpoints.pre_configured import Server + +from ....unittest import TestCase +from .test_utils import get_query_credentials + + +class TestClaimsHandling(TestCase): + + DEFAULT_REDIRECT_URI = 'http://i.b./path' + + def set_scopes(self, scopes): + def set_request_scopes(client_id, code, client, request): + request.scopes = scopes + return True + return set_request_scopes + + def set_user(self, request): + request.user = 'foo' + request.client_id = 'bar' + request.client = mock.MagicMock() + request.client.client_id = 'mocked' + return True + + def set_client(self, request): + request.client = mock.MagicMock() + request.client.client_id = 'mocked' + return True + + def save_claims_with_code(self, client_id, code, request, *args, **kwargs): + # a real validator would save the claims with the code during save_authorization_code() + self.claims_from_auth_code_request = request.claims + self.scopes = request.scopes.split() + + def retrieve_claims_saved_with_code(self, client_id, code, client, request, *args, **kwargs): + request.claims = self.claims_from_auth_code_request + request.scopes = self.scopes + + return True + + def save_claims_with_bearer_token(self, token, request, *args, **kwargs): + # a real validator would save the claims with the access token during save_bearer_token() + self.claims_saved_with_bearer_token = request.claims + + def setUp(self): + self.validator = mock.MagicMock(spec=RequestValidator) + self.validator.get_default_redirect_uri.return_value = TestClaimsHandling.DEFAULT_REDIRECT_URI + self.validator.authenticate_client.side_effect = self.set_client + + self.validator.save_authorization_code.side_effect = self.save_claims_with_code + self.validator.validate_code.side_effect = self.retrieve_claims_saved_with_code + self.validator.save_token.side_effect = self.save_claims_with_bearer_token + + self.server = Server(self.validator) + + def test_claims_stored_on_code_creation(self): + + claims = { + "id_token": { + "claim_1": None, + "claim_2": { + "essential": True + } + }, + "userinfo": { + "claim_3": { + "essential": True + }, + "claim_4": None + } + } + + claims_urlquoted = '%7B%22id_token%22%3A%20%7B%22claim_2%22%3A%20%7B%22essential%22%3A%20true%7D%2C%20%22claim_1%22%3A%20null%7D%2C%20%22userinfo%22%3A%20%7B%22claim_4%22%3A%20null%2C%20%22claim_3%22%3A%20%7B%22essential%22%3A%20true%7D%7D%7D' + uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=%s' + + h, b, s = self.server.create_authorization_response(uri % claims_urlquoted, scopes='openid test_scope') + + self.assertDictEqual(self.claims_from_auth_code_request, claims) + + code = get_query_credentials(h['Location'])['code'][0] + token_uri = 'http://example.com/path' + _, body, _ = self.server.create_token_response( + token_uri, + 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) + + def test_invalid_claims(self): + uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=this-is-not-json' + + h, b, s = self.server.create_authorization_response(uri, scopes='openid test_scope') + error = get_query_credentials(h['Location'])['error'][0] + error_desc = get_query_credentials(h['Location'])['error_description'][0] + self.assertEqual(error, 'invalid_request') + self.assertEqual(error_desc, "Malformed claims parameter") diff --git a/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py b/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py new file mode 100644 index 0000000..89431b6 --- /dev/null +++ b/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py @@ -0,0 +1,85 @@ +from __future__ import absolute_import, unicode_literals + +import mock + +from oauthlib.oauth2 import InvalidRequestError +from oauthlib.oauth2.rfc6749.endpoints.authorization import \ + AuthorizationEndpoint +from oauthlib.oauth2.rfc6749.grant_types import OpenIDConnectAuthCode +from oauthlib.oauth2.rfc6749.tokens import BearerToken + +from ....unittest import TestCase + +try: + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode + + + + +class OpenIDConnectEndpointTest(TestCase): + + def setUp(self): + self.mock_validator = mock.MagicMock() + self.mock_validator.authenticate_client.side_effect = self.set_client + grant = OpenIDConnectAuthCode(request_validator=self.mock_validator) + bearer = BearerToken(self.mock_validator) + self.endpoint = AuthorizationEndpoint(grant, bearer, + response_types={'code': grant}) + params = { + 'prompt': 'consent', + 'display': 'touch', + 'nonce': 'abcd', + 'state': 'abc', + 'redirect_uri': 'https://a.b/cb', + 'response_type': 'code', + 'client_id': 'abcdef', + 'scope': 'hello openid' + } + self.url = 'http://a.b/path?' + urlencode(params) + + def set_client(self, request): + request.client = mock.MagicMock() + request.client.client_id = 'mocked' + return True + + @mock.patch('oauthlib.common.generate_token') + def test_authorization_endpoint_handles_prompt(self, generate_token): + generate_token.return_value = "MOCK_CODE" + # In the GET view: + scopes, creds = self.endpoint.validate_authorization_request(self.url) + # In the POST view: + creds['scopes'] = scopes + h, b, s = self.endpoint.create_authorization_response(self.url, + credentials=creds) + expected = 'https://a.b/cb?state=abc&code=MOCK_CODE' + self.assertURLEqual(h['Location'], expected) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + def test_prompt_none_exclusiveness(self): + """ + Test that prompt=none can't be used with another prompt value. + """ + params = { + 'prompt': 'none consent', + 'state': 'abc', + 'redirect_uri': 'https://a.b/cb', + 'response_type': 'code', + 'client_id': 'abcdef', + 'scope': 'hello openid' + } + url = 'http://a.b/path?' + urlencode(params) + with self.assertRaises(InvalidRequestError): + self.endpoint.validate_authorization_request(url) + + def test_oidc_params_preservation(self): + """ + Test that the nonce parameter is passed through. + """ + scopes, creds = self.endpoint.validate_authorization_request(self.url) + + self.assertEqual(creds['prompt'], {'consent'}) + self.assertEqual(creds['nonce'], 'abcd') + self.assertEqual(creds['display'], 'touch') diff --git a/tests/openid/connect/core/grant_types/test_authorization_code.py b/tests/openid/connect/core/grant_types/test_authorization_code.py new file mode 100644 index 0000000..1bad120 --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_authorization_code.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import json + +import mock + +from oauthlib.common import Request +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 ....unittest import TestCase +from ....oauth2.rfc6749.grant_types.test_authorization_code import AuthorizationCodeGrantTest + + +def get_id_token_mock(token, token_handler, request): + return "MOCKED_TOKEN" + + +class OpenIDAuthCodeInterferenceTest(AuthorizationCodeGrantTest): + """Test that OpenID don't interfere with normal OAuth 2 flows.""" + + def setUp(self): + super(OpenIDAuthCodeInterferenceTest, self).setUp() + self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator) + + +class OpenIDAuthCodeTest(TestCase): + + def setUp(self): + self.request = Request('http://a.b/path') + self.request.scopes = ('hello', 'openid') + self.request.expires_in = 1800 + self.request.client_id = 'abcdef' + self.request.code = '1234' + self.request.response_type = 'code' + self.request.grant_type = 'authorization_code' + self.request.redirect_uri = 'https://a.b/cb' + self.request.state = 'abc' + + self.mock_validator = mock.MagicMock() + self.mock_validator.authenticate_client.side_effect = self.set_client + self.mock_validator.get_id_token.side_effect = get_id_token_mock + self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator) + + self.url_query = 'https://a.b/cb?code=abc&state=abc' + self.url_fragment = 'https://a.b/cb#code=abc&state=abc' + + def set_client(self, request): + request.client = mock.MagicMock() + request.client.client_id = 'mocked' + return True + + @mock.patch('oauthlib.common.generate_token') + def test_authorization(self, generate_token): + + scope, info = self.auth.validate_authorization_request(self.request) + + generate_token.return_value = 'abc' + bearer = BearerToken(self.mock_validator) + self.request.response_mode = 'query' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_query) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + 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) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + @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) + + # prompt == none requires id token hint + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + 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_query) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + # Test alernative 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) + + # 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 + 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 + 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 + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=login_required', h['Location']) + + def set_scopes(self, client_id, code, client, request): + request.scopes = self.request.scopes + request.state = self.request.state + request.user = 'bob' + return True + + def test_create_token_response(self): + self.request.response_type = None + self.mock_validator.validate_code.side_effect = self.set_scopes + + bearer = BearerToken(self.mock_validator) + + h, token, s = self.auth.create_token_response(self.request, bearer) + token = json.loads(token) + self.assertEqual(self.mock_validator.save_token.call_count, 1) + self.assertIn('access_token', token) + self.assertIn('refresh_token', token) + self.assertIn('expires_in', token) + self.assertIn('scope', token) + self.assertIn('id_token', token) + self.assertIn('openid', token['scope']) + + self.mock_validator.reset_mock() + + self.request.scopes = ('hello', 'world') + h, token, s = self.auth.create_token_response(self.request, bearer) + token = json.loads(token) + self.assertEqual(self.mock_validator.save_token.call_count, 1) + self.assertIn('access_token', token) + self.assertIn('refresh_token', token) + self.assertIn('expires_in', token) + self.assertIn('scope', token) + self.assertNotIn('id_token', token) + self.assertNotIn('openid', token['scope']) diff --git a/tests/openid/connect/core/grant_types/test_dispatchers.py b/tests/openid/connect/core/grant_types/test_dispatchers.py new file mode 100644 index 0000000..f90ec46 --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_dispatchers.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals +import mock + +from oauthlib.common import Request + +from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.dispatchers import ( + ImplicitTokenGrantDispatcher, + AuthorizationTokenGrantDispatcher +) + +from oauthlib.oauth2.rfc6749.grant_types import ( + AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, + ImplicitGrant as OAuth2ImplicitGrant, +) + + +from ....unittest import TestCase + + +class ImplicitTokenGrantDispatcherTest(TestCase): + def setUp(self): + self.request = Request('http://a.b/path') + request_validator = mock.MagicMock() + implicit_grant = OAuth2ImplicitGrant(request_validator) + openid_connect_implicit = ImplicitGrant(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, ImplicitGrant)) + + 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, ImplicitGrant)) + + 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 = OAuth2AuthorizationCodeGrant(self.request_validator) + self.openid_connect_auth = OAuth2AuthorizationCodeGrant(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 = AuthorizationTokenGrantDispatcher( + 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, AuthorizationCodeGrant)) + 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 = AuthorizationTokenGrantDispatcher( + 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, OAuth2AuthorizationCodeGrant)) + 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 = AuthorizationTokenGrantDispatcher( + 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, OAuth2AuthorizationCodeGrant)) + self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) diff --git a/tests/openid/connect/core/grant_types/test_hybrid.py b/tests/openid/connect/core/grant_types/test_hybrid.py new file mode 100644 index 0000000..531ae7f --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_hybrid.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant + +from ....oauth2.rfc6749.grant_types.test_authorization_code import AuthorizationCodeGrantTest + + +class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): + """Test that OpenID don't interfere with normal OAuth 2 flows.""" + + def setUp(self): + super(OpenIDHybridInterferenceTest, self).setUp() + self.auth = HybridGrant(request_validator=self.mock_validator) diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py new file mode 100644 index 0000000..56247d9 --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import mock + +from oauthlib.common import Request + +from oauthlib.oauth2.rfc6749.tokens import BearerToken + +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant +from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt + +from ....unittest import TestCase +from .test_authorization_code import get_id_token_mock, OpenIDAuthCodeTest + +from ....oauth2.rfc6749.grant_types.test_implicit import ImplicitGrantTest + + +class OpenIDImplicitInterferenceTest(ImplicitGrantTest): + """Test that OpenID don't interfere with normal OAuth 2 flows.""" + + def setUp(self): + super(OpenIDImplicitInterferenceTest, self).setUp() + self.auth = ImplicitGrant(request_validator=self.mock_validator) + + +class OpenIDImplicitTest(TestCase): + + def setUp(self): + self.request = Request('http://a.b/path') + self.request.scopes = ('hello', 'openid') + self.request.expires_in = 1800 + self.request.client_id = 'abcdef' + self.request.response_type = 'id_token token' + self.request.redirect_uri = 'https://a.b/cb' + self.request.nonce = 'zxc' + self.request.state = 'abc' + + self.mock_validator = mock.MagicMock() + self.mock_validator.get_id_token.side_effect = get_id_token_mock + self.auth = ImplicitGrant(request_validator=self.mock_validator) + + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?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#state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_authorization(self, generate_token): + scope, info = self.auth.validate_authorization_request(self.request) + + generate_token.return_value = 'abc' + bearer = BearerToken(self.mock_validator) + + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.response_type = 'id_token' + token = 'MOCKED_TOKEN' + url = 'https://a.b/cb#state=abc&id_token=%s' % token + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], url, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.nonce = None + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + @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) + + # prompt == none requires id token hint + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + 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.assertEqual(b, None) + self.assertEqual(s, 302) + + # Test alernative response modes + self.request.response_mode = 'query' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_query) + + # 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 + 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 + 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 + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=login_required', h['Location']) + + +class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeTokenTest, self).setUp() + self.request.response_type = 'code token' + self.auth = HybridGrant(request_validator=self.mock_validator) + 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): + super(OpenIDHybridCodeIdTokenTest, self).setUp() + self.request.response_type = 'code id_token' + self.auth = HybridGrant(request_validator=self.mock_validator) + token = 'MOCKED_TOKEN' + 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): + super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() + self.request.response_type = 'code id_token token' + self.auth = HybridGrant(request_validator=self.mock_validator) + 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 diff --git a/tests/openid/connect/core/test_request_validator.py b/tests/openid/connect/core/test_request_validator.py new file mode 100644 index 0000000..14a7c23 --- /dev/null +++ b/tests/openid/connect/core/test_request_validator.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +from oauthlib.openid.connect.core.request_validator import RequestValidator + +from ....unittest import TestCase + + +class RequestValidatorTest(TestCase): + + def test_method_contracts(self): + v = RequestValidator() + self.assertRaises( + NotImplementedError, + v.get_authorization_code_scopes, + 'client_id', 'code', 'redirect_uri', 'request' + ) + self.assertRaises( + NotImplementedError, + v.get_jwt_bearer_token, + 'token', 'token_handler', 'request' + ) + self.assertRaises( + NotImplementedError, + v.get_id_token, + 'token', 'token_handler', 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_jwt_bearer_token, + 'token', 'scopes', 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_id_token, + 'token', 'scopes', 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_silent_authorization, + 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_silent_login, + 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_user_match, + 'id_token_hint', 'scopes', 'claims', 'request' + ) diff --git a/tests/openid/connect/core/test_server.py b/tests/openid/connect/core/test_server.py new file mode 100644 index 0000000..83290db --- /dev/null +++ b/tests/openid/connect/core/test_server.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import json + +import mock + +from oauthlib.oauth2.rfc6749 import errors +from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint +from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint +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.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant + +from ....unittest import TestCase + + +class AuthorizationEndpointTest(TestCase): + + def setUp(self): + self.mock_validator = mock.MagicMock() + self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) + auth_code = AuthorizationCodeGrant(request_validator=self.mock_validator) + auth_code.save_authorization_code = mock.MagicMock() + implicit = ImplicitGrant( + request_validator=self.mock_validator) + implicit.save_token = mock.MagicMock() + hybrid = HybridGrant(self.mock_validator) + + response_types = { + 'code': auth_code, + 'token': implicit, + 'id_token': implicit, + 'id_token token': implicit, + 'code token': hybrid, + 'code id_token': hybrid, + 'code token id_token': hybrid, + 'none': auth_code + } + self.expires_in = 1800 + token = BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) + self.endpoint = AuthorizationEndpoint( + default_response_type='code', + default_token_type=token, + response_types=response_types + ) + + # TODO: Add hybrid grant test + + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') + def test_authorization_grant(self): + uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?code=abc&state=xyz') + + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') + def test_implicit_grant(self): + uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me#access_token=abc&expires_in=' + str(self.expires_in) + '&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True) + + def test_none_grant(self): + uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them&state=xyz' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?state=xyz', parse_fragment=True) + self.assertEqual(body, None) + self.assertEqual(status_code, 302) + + # and without the state parameter + uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me', parse_fragment=True) + self.assertEqual(body, None) + self.assertEqual(status_code, 302) + + def test_missing_type(self): + uri = 'http://i.b/l?client_id=me&scope=all+of+them' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + self.mock_validator.validate_request = mock.MagicMock( + side_effect=errors.InvalidRequestError()) + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.') + + def test_invalid_type(self): + uri = 'http://i.b/l?response_type=invalid&client_id=me&scope=all+of+them' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + self.mock_validator.validate_request = mock.MagicMock( + side_effect=errors.UnsupportedResponseTypeError()) + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?error=unsupported_response_type') + + +class TokenEndpointTest(TestCase): + + def setUp(self): + def set_user(request): + request.user = mock.MagicMock() + request.client = mock.MagicMock() + request.client.client_id = 'mocked_client_id' + return True + + self.mock_validator = mock.MagicMock() + self.mock_validator.authenticate_client.side_effect = set_user + self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) + auth_code = AuthorizationCodeGrant( + request_validator=self.mock_validator) + supported_types = { + 'authorization_code': auth_code, + } + self.expires_in = 1800 + token = BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) + self.endpoint = TokenEndpoint( + 'authorization_code', + default_token_type=token, + grant_types=supported_types + ) + + @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' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + token = { + 'token_type': 'Bearer', + 'expires_in': self.expires_in, + 'access_token': 'abc', + 'refresh_token': 'abc', + 'scope': 'all of them', + 'state': 'xyz' + } + self.assertEqual(json.loads(body), token) + + body = 'grant_type=authorization_code&code=abc&state=xyz' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + token = { + 'token_type': 'Bearer', + 'expires_in': self.expires_in, + 'access_token': 'abc', + 'refresh_token': 'abc', + 'state': 'xyz' + } + self.assertEqual(json.loads(body), token) + + def test_missing_type(self): + _, body, _ = self.endpoint.create_token_response('', body='') + token = {'error': 'unsupported_grant_type'} + self.assertEqual(json.loads(body), token) + + def test_invalid_type(self): + body = 'grant_type=invalid' + _, body, _ = self.endpoint.create_token_response('', body=body) + token = {'error': 'unsupported_grant_type'} + self.assertEqual(json.loads(body), token) diff --git a/tests/openid/connect/core/test_tokens.py b/tests/openid/connect/core/test_tokens.py new file mode 100644 index 0000000..12c75f1 --- /dev/null +++ b/tests/openid/connect/core/test_tokens.py @@ -0,0 +1,133 @@ +from __future__ import absolute_import, unicode_literals + +import mock + +from oauthlib.openid.connect.core.tokens import JWTToken + +from ....unittest import TestCase + + +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) -- cgit v1.2.1 From 5b9b752f68d3a7963cb5b85cf5f9570490eacf7a Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 30 Jun 2018 14:55:59 -0700 Subject: Update all pypi.python.org URLs to pypi.org (#555) --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index b477e41..6741a75 100644 --- a/README.rst +++ b/README.rst @@ -11,10 +11,10 @@ logic for Python 2.7 and 3.4+.* :target: https://coveralls.io/r/oauthlib/oauthlib :alt: Coveralls .. image:: https://img.shields.io/pypi/pyversions/oauthlib.svg - :target: https://pypi.python.org/pypi/oauthlib + :target: https://pypi.org/project/oauthlib/ :alt: Download from PyPi .. image:: https://img.shields.io/pypi/l/oauthlib.svg - :target: https://pypi.python.org/pypi/oauthlib + :target: https://pypi.org/project/oauthlib/ :alt: License .. image:: https://img.shields.io/readthedocs/oauthlib.svg :target: https://oauthlib.readthedocs.io/en/latest/index.html -- cgit v1.2.1 From 481a4ec2e29530541ff8985cce938ece7a661562 Mon Sep 17 00:00:00 2001 From: claweyenuk <39317519+claweyenuk@users.noreply.github.com> Date: Sat, 30 Jun 2018 15:04:02 -0700 Subject: Update save_bearer_token docs to mention how the token is passed in as a reference (#556) --- oauthlib/oauth2/rfc6749/request_validator.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 92edba6..bf1515d 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -332,7 +332,14 @@ class RequestValidator(object): } Note that while "scope" is a string-separated list of authorized scopes, - the original list is still available in request.scopes + the original list is still available in request.scopes. + + The token dict is passed as a reference so any changes made to the dictionary + will go back to the user. If additional information must return to the client + user, and it is only possible to get this information after writing the token + to storage, it should be added to the token dictionary. If the token + dictionary must be modified but the changes should not go back to the user, + a copy of the dictionary must be made before making the changes. Also note that if an Authorization Code grant request included a valid claims parameter (for OpenID Connect) then the request.claims property will contain -- cgit v1.2.1 From 3eaf962311dfbc566dbfa66a988e0331b91184be Mon Sep 17 00:00:00 2001 From: Seth Davis Date: Sat, 30 Jun 2018 18:09:26 -0400 Subject: Remove handling of nonstandard parameter "expires" (#506) --- oauthlib/oauth2/rfc6749/parameters.py | 7 ++----- tests/oauth2/rfc6749/test_parameters.py | 11 ----------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 0107933..9ea8c44 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -362,16 +362,13 @@ def parse_token_response(body, scope=None): # https://github.com/oauthlib/oauthlib/issues/267 params = dict(urlparse.parse_qsl(body)) - for key in ('expires_in', 'expires'): - if key in params: # cast a couple things to int + for key in ('expires_in',): + if key in params: # cast things to int params[key] = int(params[key]) if 'scope' in params: params['scope'] = scope_to_list(params['scope']) - if 'expires' in params: - params['expires_in'] = params.pop('expires') - if 'expires_in' in params: params['expires_at'] = time.time() + int(params['expires_in']) diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index 2a9cbe8..6ba98c0 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -115,13 +115,6 @@ class ParameterTests(TestCase): ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",' ' "example_parameter": "example_value" }') - json_expires = ('{ "access_token": "2YotnFZFEjr1zCsicMWpAA",' - ' "token_type": "example",' - ' "expires": 3600,' - ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",' - ' "example_parameter": "example_value",' - ' "scope":"abc def"}') - json_dict = { 'access_token': '2YotnFZFEjr1zCsicMWpAA', 'token_type': 'example', @@ -264,7 +257,3 @@ class ParameterTests(TestCase): finally: signals.scope_changed.disconnect(record_scope_change) del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] - - def test_token_response_with_expires(self): - """Verify fallback for alternate spelling of expires_in. """ - self.assertEqual(parse_token_response(self.json_expires), self.json_dict) -- cgit v1.2.1 From cfcbe99477a5d392175970f9c2e16b7d8ce138fb Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 2 Jul 2018 10:18:55 +0100 Subject: The id_token_hint parameter isn't required by the OIDC spec. (#559) --- oauthlib/openid/connect/core/grant_types/base.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index 2bb48b1..fa578a5 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -225,12 +225,6 @@ class GrantTypeBase(object): msg = "Prompt none is mutually exclusive with other values." raise InvalidRequestError(request=request, description=msg) - # prompt other than 'none' should be handled by the server code that - # uses oauthlib - if not request.id_token_hint: - msg = "Prompt is set to none yet id_token_hint is missing." - raise InvalidRequestError(request=request, description=msg) - if not self.request_validator.validate_silent_login(request): raise LoginRequired(request=request) -- cgit v1.2.1 From f991b5759a4728fd99d36f98e2dd171b300fb7c2 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jul 2018 15:21:58 +0200 Subject: Added flask-dance tests, see #553 --- Makefile | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f9cc4ab..64fdc8e 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ clean: clean-eggs clean-build @find . -iname '__pycache__' -delete rm -rf .tox rm -rf bottle-oauthlib + rm -rf dance rm -rf django-oauth-toolkit rm -rf flask-oauthlib rm -rf requests-oauthlib @@ -65,6 +66,13 @@ requests: cd requests-oauthlib 2>/dev/null || git clone https://github.com/requests/requests-oauthlib.git cd requests-oauthlib && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../[signedtoken],' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox +dance: + #--------------------------- + # Library singingwolfboy/flask-dance + # Contacts: singingwolfboy + cd flask-dance 2>/dev/null || git clone https://github.com/singingwolfboy/flask-dance.git + cd flask-dance && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox + .DEFAULT_GOAL := all -.PHONY: clean test bottle django flask requests -all: clean test bottle django flask requests +.PHONY: clean test bottle dance django flask requests +all: clean test bottle dance django flask requests -- cgit v1.2.1 From fbacd77b602e4c60f8da2413c150fa7f20b2f83c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 30 Jul 2018 13:43:00 +0200 Subject: Added htmlcov to help increase coverage locally --- .gitignore | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4515c8f..683f357 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ pip-log.txt .coverage .tox coverage +htmlcov* #Translations *.mo diff --git a/tox.ini b/tox.ini index 3dded41..8f3345e 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = py27,py34,py35,py36,pypy,docs [testenv] deps= -rrequirements-test.txt -commands=nosetests --with-coverage --cover-erase --cover-package=oauthlib -w tests +commands=nosetests --with-coverage --cover-html --cover-html-dir={toxinidir}/htmlcov-{envname} --cover-erase --cover-package=oauthlib -w tests [testenv:py27] deps=unittest2 -- cgit v1.2.1