From cb6af4b44da264613250cb3d99be420dbeb8e268 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 20 Feb 2019 14:30:03 +0100 Subject: Fix 652: removed "state" from /token response. Fix OIDC /token flow where &state=None was always returned, and fix OAuth2.0 /token flow where &state=foobar was returned if &state=foobar was present in the token request. Remove "save_token" from create_token() signature cuz it was not used internally. Deprecated the option to let upstream libraries have a chance to remove it, if ever used. --- CHANGELOG.rst | 8 +++++ .../rfc6749/grant_types/authorization_code.py | 4 ++- .../rfc6749/grant_types/client_credentials.py | 3 +- oauthlib/oauth2/rfc6749/grant_types/implicit.py | 5 ++- .../oauth2/rfc6749/grant_types/refresh_token.py | 3 +- .../resource_owner_password_credentials.py | 3 +- oauthlib/oauth2/rfc6749/tokens.py | 18 +++++----- oauthlib/openid/connect/core/grant_types/base.py | 3 -- .../openid/connect/core/grant_types/implicit.py | 5 +++ oauthlib/openid/connect/core/tokens.py | 2 +- .../endpoints/test_credentials_preservation.py | 12 ------- tests/oauth2/rfc6749/test_server.py | 39 +++++++++++++++------- tests/openid/connect/core/test_server.py | 16 +++++---- 13 files changed, 72 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2cc0dd3..9e0efda 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog ========= +TBD +------------------ +* #652: Fixed OIDC /token response which wrongly returned "&state=None" + +3.0.1 (2019-01-24) +------------------ +* Fixed OAuth2.0 regression introduced in 3.0.0: Revocation with Basic auth no longer possible #644 + 3.0.0 (2019-01-01) ------------------ OAuth2.0 Provider - outstanding Features diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 6463391..5f03d9c 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -305,9 +305,11 @@ class AuthorizationCodeGrant(GrantTypeBase): headers.update(e.headers) return headers, e.json, e.status_code - token = token_handler.create_token(request, refresh_token=self.refresh_token, save_token=False) + token = token_handler.create_token(request, refresh_token=self.refresh_token) + for modifier in self._token_modifiers: token = modifier(token, token_handler, request) + self.request_validator.save_token(token, request) self.request_validator.invalidate_authorization_code( request.client_id, request.code, request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index c966795..7e50857 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -76,10 +76,11 @@ class ClientCredentialsGrant(GrantTypeBase): headers.update(e.headers) return headers, e.json, e.status_code - token = token_handler.create_token(request, refresh_token=False, save_token=False) + token = token_handler.create_token(request, refresh_token=False) for modifier in self._token_modifiers: token = modifier(token) + self.request_validator.save_token(token, request) log.debug('Issuing token to client id %r (%r), %r.', diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index d6de906..48bae7a 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -237,10 +237,13 @@ class ImplicitGrant(GrantTypeBase): # "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(): - token = token_handler.create_token(request, refresh_token=False, save_token=False) + token = token_handler.create_token(request, refresh_token=False) else: token = {} + if request.state is not None: + token['state'] = request.state + for modifier in self._token_modifiers: token = modifier(token, token_handler, request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index bd519e8..fc61d65 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -64,10 +64,11 @@ class RefreshTokenGrant(GrantTypeBase): return headers, e.json, e.status_code token = token_handler.create_token(request, - refresh_token=self.issue_new_refresh_tokens, save_token=False) + refresh_token=self.issue_new_refresh_tokens) for modifier in self._token_modifiers: token = modifier(token) + self.request_validator.save_token(token, request) log.debug('Issuing new token to client id %r (%r), %r.', 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 f765d91..5929afb 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -104,10 +104,11 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): headers.update(e.headers) return headers, e.json, e.status_code - token = token_handler.create_token(request, self.refresh_token, save_token=False) + token = token_handler.create_token(request, self.refresh_token) for modifier in self._token_modifiers: token = modifier(token) + self.request_validator.save_token(token, request) log.debug('Issuing token %r to client id %r (%r) and username %s.', diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index d78df09..44a9a97 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -12,6 +12,7 @@ from __future__ import absolute_import, unicode_literals import hashlib import hmac from binascii import b2a_base64 +import warnings from oauthlib import common from oauthlib.common import add_params_to_qs, add_params_to_uri, unicode_type @@ -296,15 +297,18 @@ class BearerToken(TokenBase): ) self.expires_in = expires_in or 3600 - def create_token(self, request, refresh_token=False, save_token=True): + def create_token(self, request, refresh_token=False, **kwargs): """ Create a BearerToken, by default without refresh token. - + :param request: OAuthlib request. :type request: oauthlib.common.Request :param refresh_token: - :param save_token: """ + if "save_token" in kwargs: + warnings.warn("`save_token` has been deprecated, it was not used internally." + "If you do, use `request_validator.save_token()` instead.", + DeprecationWarning) if callable(self.expires_in): expires_in = self.expires_in(request) @@ -325,9 +329,6 @@ class BearerToken(TokenBase): if request.scopes is not None: token['scope'] = ' '.join(request.scopes) - if request.state is not None: - token['state'] = request.state - if refresh_token: if (request.refresh_token and not self.request_validator.rotate_refresh_token(request)): @@ -336,10 +337,7 @@ class BearerToken(TokenBase): token['refresh_token'] = self.refresh_token_generator(request) token.update(request.extra_credentials or {}) - token = OAuth2Token(token) - if save_token: - self.request_validator.save_bearer_token(token, request) - return token + return OAuth2Token(token) def validate_request(self, request): """ diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index fa578a5..05cdd37 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -58,9 +58,6 @@ class GrantTypeBase(object): 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" diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py index 0eaa5b3..0a6fcb7 100644 --- a/oauthlib/openid/connect/core/grant_types/implicit.py +++ b/oauthlib/openid/connect/core/grant_types/implicit.py @@ -26,3 +26,8 @@ class ImplicitGrant(GrantTypeBase): self.custom_validators.post_auth.append( self.openid_implicit_authorization_validator) self.register_token_modifier(self.add_id_token) + + def add_id_token(self, token, token_handler, request): + if 'state' not in token: + token['state'] = request.state + return super(ImplicitGrant, self).add_id_token(token, token_handler, request) diff --git a/oauthlib/openid/connect/core/tokens.py b/oauthlib/openid/connect/core/tokens.py index 6b68891..b67cdf2 100644 --- a/oauthlib/openid/connect/core/tokens.py +++ b/oauthlib/openid/connect/core/tokens.py @@ -25,7 +25,7 @@ class JWTToken(TokenBase): ) self.expires_in = expires_in or 3600 - def create_token(self, request, refresh_token=False, save_token=False): + def create_token(self, request, refresh_token=False): """Create a JWT Token, using requestvalidator method.""" if callable(self.expires_in): diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py index 1a2f66b..c77d18e 100644 --- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py +++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py @@ -42,18 +42,6 @@ class PreservationTest(TestCase): def test_state_preservation(self): auth_uri = 'http://example.com/path?state=xyz&client_id=abc&response_type=' - token_uri = 'http://example.com/path' - - # authorization grant - h, _, s = self.web.create_authorization_response( - auth_uri + 'code', scopes=['random']) - self.assertEqual(s, 302) - self.assertIn('Location', h) - code = get_query_credentials(h['Location'])['code'][0] - self.validator.validate_code.side_effect = self.set_state('xyz') - _, body, _ = self.web.create_token_response(token_uri, - body='grant_type=authorization_code&code=%s' % code) - self.assertEqual(json.loads(body)['state'], 'xyz') # implicit grant h, _, s = self.mobile.create_authorization_response( diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py index b623a9b..2c6ecff 100644 --- a/tests/oauth2/rfc6749/test_server.py +++ b/tests/oauth2/rfc6749/test_server.py @@ -144,7 +144,7 @@ class TokenEndpointTest(TestCase): @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 = 'grant_type=authorization_code&code=abc&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( '', body=body) token = { @@ -152,23 +152,27 @@ class TokenEndpointTest(TestCase): 'expires_in': self.expires_in, 'access_token': 'abc', 'refresh_token': 'abc', - 'scope': 'all of them', - 'state': 'xyz' + 'scope': 'all of them' } self.assertEqual(json.loads(body), token) - body = 'grant_type=authorization_code&code=abc&state=xyz' + body = 'grant_type=authorization_code&code=abc' 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' + 'refresh_token': 'abc' } self.assertEqual(json.loads(body), token) + # try with additional custom variables + body = 'grant_type=authorization_code&code=abc&state=foobar' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + self.assertEqual(json.loads(body), token) + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_password_grant(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' @@ -277,7 +281,7 @@ twIDAQAB @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') 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' + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( '', body=body) body = json.loads(body) @@ -286,12 +290,11 @@ twIDAQAB 'expires_in': self.expires_in, 'access_token': body['access_token'], 'refresh_token': 'abc', - 'scope': 'all of them', - 'state': 'xyz' + 'scope': 'all of them' } self.assertEqual(body, token) - body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&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' headers, body, status_code = self.endpoint.create_token_response( '', body=body) body = json.loads(body) @@ -299,8 +302,20 @@ twIDAQAB 'token_type': 'Bearer', 'expires_in': self.expires_in, 'access_token': body['access_token'], - 'refresh_token': 'abc', - 'state': 'xyz' + 'refresh_token': 'abc' + } + self.assertEqual(body, token) + + # try with additional custom variables + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&state=foobar' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + body = json.loads(body) + token = { + 'token_type': 'Bearer', + 'expires_in': self.expires_in, + 'access_token': body['access_token'], + 'refresh_token': 'abc' } self.assertEqual(body, token) diff --git a/tests/openid/connect/core/test_server.py b/tests/openid/connect/core/test_server.py index ffab7b0..756c9d0 100644 --- a/tests/openid/connect/core/test_server.py +++ b/tests/openid/connect/core/test_server.py @@ -143,7 +143,7 @@ class TokenEndpointTest(TestCase): @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 = 'grant_type=authorization_code&code=abc&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( '', body=body) token = { @@ -151,23 +151,27 @@ class TokenEndpointTest(TestCase): 'expires_in': self.expires_in, 'access_token': 'abc', 'refresh_token': 'abc', - 'scope': 'all of them', - 'state': 'xyz' + 'scope': 'all of them' } self.assertEqual(json.loads(body), token) - body = 'grant_type=authorization_code&code=abc&state=xyz' + body = 'grant_type=authorization_code&code=abc' 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' + 'refresh_token': 'abc' } self.assertEqual(json.loads(body), token) + # ignore useless fields + body = 'grant_type=authorization_code&code=abc&state=foobar' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + self.assertEqual(json.loads(body), token) + def test_missing_type(self): _, body, _ = self.endpoint.create_token_response('', body='') token = {'error': 'unsupported_grant_type'} -- cgit v1.2.1 From 66d7c0035a8d33109ffaec9c8a620dd40255f99d Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 21 Feb 2019 10:01:29 +0100 Subject: Add clarity to the deprecation warning --- oauthlib/oauth2/rfc6749/tokens.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 44a9a97..7973923 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -306,8 +306,8 @@ class BearerToken(TokenBase): :param refresh_token: """ if "save_token" in kwargs: - warnings.warn("`save_token` has been deprecated, it was not used internally." - "If you do, use `request_validator.save_token()` instead.", + warnings.warn("`save_token` has been deprecated, it was not called internally." + "If you do, call `request_validator.save_token()` instead.", DeprecationWarning) if callable(self.expires_in): -- cgit v1.2.1 From c17a4a25a71b3b342ad522427c23038f417fb22e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 21 Feb 2019 10:16:55 +0100 Subject: Add authorization "state" preservation back for AuthCode --- tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py index c77d18e..c0cf86d 100644 --- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py +++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py @@ -43,6 +43,13 @@ class PreservationTest(TestCase): def test_state_preservation(self): auth_uri = 'http://example.com/path?state=xyz&client_id=abc&response_type=' + # authorization grant + h, _, s = self.web.create_authorization_response( + auth_uri + 'code', scopes=['random']) + self.assertEqual(s, 302) + self.assertIn('Location', h) + self.assertEqual(get_query_credentials(h['Location'])['state'][0], 'xyz') + # implicit grant h, _, s = self.mobile.create_authorization_response( auth_uri + 'token', scopes=['random']) -- cgit v1.2.1 From 87972cc4346a4c7b698fa1f026c76ddf717dec6b Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 21 Feb 2019 10:17:23 +0100 Subject: Removed useless set_state internal function Does not have purpose for /token request --- tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py index c0cf86d..e7c66b6 100644 --- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py +++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py @@ -29,12 +29,6 @@ class PreservationTest(TestCase): self.web = WebApplicationServer(self.validator) self.mobile = MobileApplicationServer(self.validator) - def set_state(self, state): - def set_request_state(client_id, code, client, request): - request.state = state - return True - return set_request_state - def set_client(self, request): request.client = mock.MagicMock() request.client.client_id = 'mocked' @@ -128,7 +122,7 @@ class PreservationTest(TestCase): # was not given in the authorization AND not in the token request. self.validator.confirm_redirect_uri.return_value = True code = get_query_credentials(h['Location'])['code'][0] - self.validator.validate_code.side_effect = self.set_state('xyz') + self.validator.validate_code.return_value = True _, body, s = self.web.create_token_response(token_uri, body='grant_type=authorization_code&code=%s' % code) self.assertEqual(s, 200) -- cgit v1.2.1 From ff6844524b3fe28e7122a8177fb7c2e0993d5162 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 15:36:13 +0100 Subject: Change to 3.0.2-dev as long as master is in "dev" --- CHANGELOG.rst | 2 +- oauthlib/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9e0efda..dbd8c3f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -TBD +3.0.2 (TBD) ------------------ * #652: Fixed OIDC /token response which wrongly returned "&state=None" diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index b23102c..8eb82a6 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,6 +12,6 @@ import logging from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.0.1' +__version__ = '3.0.2-dev' logging.getLogger('oauthlib').addHandler(NullHandler()) -- cgit v1.2.1 From 0d423ac7af419b69530cd05ab786527d941b4ffb Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 20:57:56 +0100 Subject: OIDC: Raise error=invalid_request when nonce is mandatory Until now, only OIDC implicit was raising an error, but OIDC hybrid contain a couple of mandatory nonce, too. --- oauthlib/openid/connect/core/grant_types/base.py | 23 --------- oauthlib/openid/connect/core/grant_types/hybrid.py | 25 +++++++++ .../openid/connect/core/grant_types/implicit.py | 23 ++++++++- .../connect/core/grant_types/test_implicit.py | 60 +++++++++++++++++++--- 4 files changed, 99 insertions(+), 32 deletions(-) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index 05cdd37..4f5c944 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -247,28 +247,5 @@ class GrantTypeBase(object): 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/hybrid.py b/oauthlib/openid/connect/core/grant_types/hybrid.py index 54669ae..685fa08 100644 --- a/oauthlib/openid/connect/core/grant_types/hybrid.py +++ b/oauthlib/openid/connect/core/grant_types/hybrid.py @@ -8,6 +8,7 @@ from __future__ import absolute_import, unicode_literals import logging from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant +from oauthlib.oauth2.rfc6749.errors import InvalidRequestError from .base import GrantTypeBase from ..request_validator import RequestValidator @@ -34,3 +35,27 @@ class HybridGrant(GrantTypeBase): self.register_code_modifier(self.add_token) self.register_code_modifier(self.add_id_token) self.register_token_modifier(self.add_id_token) + + def openid_authorization_validator(self, request): + """Additional validation when following the Authorization Code flow. + """ + request_info = super(HybridGrant, self).openid_authorization_validator(request) + if not request_info: # returns immediately if OAuth2.0 + return request_info + + # REQUIRED if the Response Type of the request is `code + # id_token` or `code id_token token` and OPTIONAL when the + # Response Type of the request is `code token`. It is a 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 request.response_type in ["code id_token", "code id_token token"]: + if not request.nonce: + raise InvalidRequestError( + request=request, + description='Request is missing mandatory nonce parameter.' + ) + return request_info diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py index 0a6fcb7..d3797b2 100644 --- a/oauthlib/openid/connect/core/grant_types/implicit.py +++ b/oauthlib/openid/connect/core/grant_types/implicit.py @@ -10,6 +10,7 @@ import logging from .base import GrantTypeBase from oauthlib.oauth2.rfc6749.grant_types.implicit import ImplicitGrant as OAuth2ImplicitGrant +from oauthlib.oauth2.rfc6749.errors import InvalidRequestError log = logging.getLogger(__name__) @@ -23,11 +24,29 @@ class ImplicitGrant(GrantTypeBase): 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) def add_id_token(self, token, token_handler, request): if 'state' not in token: token['state'] = request.state return super(ImplicitGrant, self).add_id_token(token, token_handler, request) + + def openid_authorization_validator(self, request): + """Additional validation when following the implicit flow. + """ + request_info = super(ImplicitGrant, self).openid_authorization_validator(request) + if not request_info: # returns immediately if OAuth2.0 + return request_info + + # 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: + raise InvalidRequestError( + request=request, + description='Request is missing mandatory nonce parameter.' + ) + return request_info diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py index 7ab198a..54fd8b9 100644 --- a/tests/openid/connect/core/grant_types/test_implicit.py +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, unicode_literals import mock from oauthlib.common import Request +from oauthlib.oauth2.rfc6749 import errors from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant @@ -30,8 +31,8 @@ class OpenIDImplicitTest(TestCase): 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.request.nonce = 'xyz' self.mock_validator = mock.MagicMock() self.mock_validator.get_id_token.side_effect = get_id_token_mock @@ -61,12 +62,6 @@ class OpenIDImplicitTest(TestCase): 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' @@ -105,16 +100,41 @@ class OpenIDImplicitTest(TestCase): h, b, s = self.auth.create_authorization_response(self.request, bearer) self.assertIn('error=login_required', h['Location']) + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + 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) + class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): def setUp(self): super(OpenIDHybridCodeTokenTest, self).setUp() self.request.response_type = 'code token' + self.request.nonce = None 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' + @mock.patch('oauthlib.common.generate_token') + def test_optional_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = 'xyz' + scope, info = self.auth.validate_authorization_request(self.request) + + 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) + class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): @@ -122,11 +142,24 @@ class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): super(OpenIDHybridCodeIdTokenTest, self).setUp() self.mock_validator.get_code_challenge.return_value = None self.request.response_type = 'code id_token' + self.request.nonce = 'zxc' 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 + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + 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) + class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): @@ -134,7 +167,20 @@ class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() self.mock_validator.get_code_challenge.return_value = None self.request.response_type = 'code id_token token' + self.request.nonce = 'xyz' 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 + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + 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) -- cgit v1.2.1 From ad7b15428bde9eaa55bbc0ca0ce338342740a7c9 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:34:31 +0100 Subject: Add nonce auth request check for authorization_code --- .../connect/core/grant_types/test_authorization_code.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/openid/connect/core/grant_types/test_authorization_code.py b/tests/openid/connect/core/grant_types/test_authorization_code.py index c3c7824..89401ab 100644 --- a/tests/openid/connect/core/grant_types/test_authorization_code.py +++ b/tests/openid/connect/core/grant_types/test_authorization_code.py @@ -40,6 +40,7 @@ class OpenIDAuthCodeTest(TestCase): self.request.grant_type = 'authorization_code' self.request.redirect_uri = 'https://a.b/cb' self.request.state = 'abc' + self.request.nonce = None self.mock_validator = mock.MagicMock() self.mock_validator.authenticate_client.side_effect = self.set_client @@ -148,3 +149,16 @@ class OpenIDAuthCodeTest(TestCase): self.assertIn('scope', token) self.assertNotIn('id_token', token) self.assertNotIn('openid', token['scope']) + + @mock.patch('oauthlib.common.generate_token') + def test_optional_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = 'xyz' + scope, info = self.auth.validate_authorization_request(self.request) + + 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) -- cgit v1.2.1 From 2d9a89c23d0e0088ac84606e28be51f59f9fa12c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:34:48 +0100 Subject: Add nonce mandatory check for "id_token" response_type --- .../connect/core/grant_types/test_implicit.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py index 54fd8b9..948edd3 100644 --- a/tests/openid/connect/core/grant_types/test_implicit.py +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -113,6 +113,27 @@ class OpenIDImplicitTest(TestCase): self.assertEqual(s, 302) +class OpenIDImplicitNoAccessTokenTest(OpenIDImplicitTest): + def setUp(self): + super(OpenIDImplicitNoAccessTokenTest, self).setUp() + self.request.response_type = 'id_token' + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?state=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#state=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + 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) + + class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): def setUp(self): -- cgit v1.2.1 From 3ccaeb128156a2ab4519f177bf8811c89513862a Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:36:14 +0100 Subject: Move HybridGrant test into its respective file. --- .../openid/connect/core/grant_types/test_hybrid.py | 75 +++++++++++++++++++++ .../connect/core/grant_types/test_implicit.py | 76 +--------------------- 2 files changed, 76 insertions(+), 75 deletions(-) diff --git a/tests/openid/connect/core/grant_types/test_hybrid.py b/tests/openid/connect/core/grant_types/test_hybrid.py index 6eb8037..8964053 100644 --- a/tests/openid/connect/core/grant_types/test_hybrid.py +++ b/tests/openid/connect/core/grant_types/test_hybrid.py @@ -4,6 +4,8 @@ from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant from tests.oauth2.rfc6749.grant_types.test_authorization_code import \ AuthorizationCodeGrantTest +from .test_authorization_code import OpenIDAuthCodeTest + class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): @@ -12,3 +14,76 @@ class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): def setUp(self): super(OpenIDHybridInterferenceTest, self).setUp() self.auth = HybridGrant(request_validator=self.mock_validator) + + +class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeTokenTest, self).setUp() + self.request.response_type = 'code token' + self.request.nonce = None + 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' + + @mock.patch('oauthlib.common.generate_token') + def test_optional_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = 'xyz' + scope, info = self.auth.validate_authorization_request(self.request) + + 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) + + +class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeIdTokenTest, self).setUp() + self.mock_validator.get_code_challenge.return_value = None + self.request.response_type = 'code id_token' + self.request.nonce = 'zxc' + 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 + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + 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) + + +class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() + self.mock_validator.get_code_challenge.return_value = None + self.request.response_type = 'code id_token token' + self.request.nonce = 'xyz' + 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 + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + 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) diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py index 948edd3..1ee805c 100644 --- a/tests/openid/connect/core/grant_types/test_implicit.py +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -7,11 +7,10 @@ from oauthlib.common import Request from oauthlib.oauth2.rfc6749 import errors from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt -from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant from tests.oauth2.rfc6749.grant_types.test_implicit import ImplicitGrantTest from tests.unittest import TestCase -from .test_authorization_code import get_id_token_mock, OpenIDAuthCodeTest +from .test_authorization_code import get_id_token_mock class OpenIDImplicitInterferenceTest(ImplicitGrantTest): @@ -132,76 +131,3 @@ class OpenIDImplicitNoAccessTokenTest(OpenIDImplicitTest): self.assertIn('error=invalid_request', h['Location']) self.assertEqual(b, None) self.assertEqual(s, 302) - - -class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeTokenTest, self).setUp() - self.request.response_type = 'code token' - self.request.nonce = None - 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' - - @mock.patch('oauthlib.common.generate_token') - def test_optional_nonce(self, generate_token): - generate_token.return_value = 'abc' - self.request.nonce = 'xyz' - scope, info = self.auth.validate_authorization_request(self.request) - - 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) - - -class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeIdTokenTest, self).setUp() - self.mock_validator.get_code_challenge.return_value = None - self.request.response_type = 'code id_token' - self.request.nonce = 'zxc' - 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 - - @mock.patch('oauthlib.common.generate_token') - def test_required_nonce(self, generate_token): - generate_token.return_value = 'abc' - self.request.nonce = None - self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) - - 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) - - -class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() - self.mock_validator.get_code_challenge.return_value = None - self.request.response_type = 'code id_token token' - self.request.nonce = 'xyz' - 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 - - @mock.patch('oauthlib.common.generate_token') - def test_required_nonce(self, generate_token): - generate_token.return_value = 'abc' - self.request.nonce = None - self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) - - 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) -- cgit v1.2.1 From 1ef4209f71d6abe20e71a65a4b9d1141205b087e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:45:05 +0100 Subject: Added missing import after test moved --- tests/openid/connect/core/grant_types/test_hybrid.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/openid/connect/core/grant_types/test_hybrid.py b/tests/openid/connect/core/grant_types/test_hybrid.py index 8964053..0aa0add 100644 --- a/tests/openid/connect/core/grant_types/test_hybrid.py +++ b/tests/openid/connect/core/grant_types/test_hybrid.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals -from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant +import mock + +from oauthlib.oauth2.rfc6749 import errors +from oauthlib.oauth2.rfc6749.tokens import BearerToken +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant from tests.oauth2.rfc6749.grant_types.test_authorization_code import \ AuthorizationCodeGrantTest from .test_authorization_code import OpenIDAuthCodeTest - class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): """Test that OpenID don't interfere with normal OAuth 2 flows.""" -- cgit v1.2.1 From 4e945e9bcc847e77b7da37279853b853c4579cf7 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:57:53 +0100 Subject: Notifications must be sent for every build I hope fixing the longstanding issue mentionned at https://github.com/oauthlib/oauthlib/issues/582. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c7978d7..f2e68a6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ notifications: urls: - https://coveralls.io/webhook - https://webhooks.gitter.im/e/6008c872bf0ecee344f4 - on_success: change + on_success: always on_failure: always on_start: never deploy: -- cgit v1.2.1 From 5b2bfd5d5de6525fbe12bbbf0dbe77a7640a6c09 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 4 Jul 2019 11:19:38 +0200 Subject: Update for 3.0.2 --- CHANGELOG.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dbd8c3f..c5346eb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,9 +1,12 @@ Changelog ========= -3.0.2 (TBD) +3.0.2 (2019-07-04) ------------------ +* #650: Fixed space encoding in base string URI used in the signature base string. * #652: Fixed OIDC /token response which wrongly returned "&state=None" +* #654: Doc: The value `state` must not be stored by the AS, only returned in /authorize response. +* #656: Fixed OIDC "nonce" checks: raise errors when it's mandatory 3.0.1 (2019-01-24) ------------------ -- cgit v1.2.1 From 9e824cfb0eb36b4d23ab73171b821b1a74ec659c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 4 Jul 2019 11:22:12 +0200 Subject: Bump version --- oauthlib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 8eb82a6..089977c 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,6 +12,6 @@ import logging from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.0.2-dev' +__version__ = '3.0.2' logging.getLogger('oauthlib').addHandler(NullHandler()) -- cgit v1.2.1 From 0a9fd41faed16e15e04d6bfeef2b532d090f05bf Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 19 Jul 2019 12:42:43 +0200 Subject: Bump version --- oauthlib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index f1457a9..c7d19a1 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,7 +12,7 @@ import logging from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.0.2' +__version__ = '3.1.0-dev' logging.getLogger('oauthlib').addHandler(NullHandler()) -- cgit v1.2.1