From 5a1e7483749229e01442cfb969916a14a2078789 Mon Sep 17 00:00:00 2001 From: Nikos Sklikas Date: Mon, 22 Mar 2021 16:39:22 +0200 Subject: [PATCH 1/5] Fix RefreshTokenGrant modifiers The RefreshTokenGrant modifiers now take the same arguments as the AuthorizationCodeGrant modifiers --- oauthlib/oauth2/rfc6749/grant_types/refresh_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index 8698a3d5..f801de4a 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -63,7 +63,7 @@ def create_token_response(self, request, token_handler): refresh_token=self.issue_new_refresh_tokens) for modifier in self._token_modifiers: - token = modifier(token) + token = modifier(token, token_handler, request) self.request_validator.save_token(token, request) From 595bf5f98ab785aa64840ed469fb1b9dc09bdb9e Mon Sep 17 00:00:00 2001 From: Nikos Sklikas Date: Mon, 22 Mar 2021 16:38:38 +0200 Subject: [PATCH 2/5] Add support for refreshing ID Tokens --- .../connect/core/grant_types/__init__.py | 1 + .../connect/core/grant_types/refresh_token.py | 36 +++++++ .../core/grant_types/test_refresh_token.py | 99 +++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 oauthlib/openid/connect/core/grant_types/refresh_token.py create mode 100644 tests/openid/connect/core/grant_types/test_refresh_token.py diff --git a/oauthlib/openid/connect/core/grant_types/__init__.py b/oauthlib/openid/connect/core/grant_types/__init__.py index 887a5850..8dad5f60 100644 --- a/oauthlib/openid/connect/core/grant_types/__init__.py +++ b/oauthlib/openid/connect/core/grant_types/__init__.py @@ -10,3 +10,4 @@ ) from .hybrid import HybridGrant from .implicit import ImplicitGrant +from .refresh_token import RefreshTokenGrant diff --git a/oauthlib/openid/connect/core/grant_types/refresh_token.py b/oauthlib/openid/connect/core/grant_types/refresh_token.py new file mode 100644 index 00000000..386b57cd --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/refresh_token.py @@ -0,0 +1,36 @@ +""" +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +import logging + +from oauthlib.oauth2.rfc6749.grant_types.refresh_token import ( + RefreshTokenGrant as OAuth2RefreshTokenGrant, +) + +from .base import GrantTypeBase + +log = logging.getLogger(__name__) + + +class RefreshTokenGrant(GrantTypeBase): + + def __init__(self, refresh_id_token=True, request_validator=None, **kwargs): + self.refresh_id_token = refresh_id_token + self.proxy_target = OAuth2RefreshTokenGrant( + request_validator=request_validator, **kwargs) + self.register_token_modifier(self.add_id_token) + + def add_id_token(self, token, token_handler, request): + """ + Construct an initial version of id_token, and let the + request_validator sign or encrypt it. + + The authorization_code version of this method is used to + retrieve the nonce accordingly to the code storage. + """ + # Treat it as normal OAuth 2 auth code request if openid is not present + if not self.refresh_id_token: + return token + + return super().add_id_token(token, token_handler, request) diff --git a/tests/openid/connect/core/grant_types/test_refresh_token.py b/tests/openid/connect/core/grant_types/test_refresh_token.py new file mode 100644 index 00000000..c19de188 --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_refresh_token.py @@ -0,0 +1,99 @@ +import json +from unittest import mock + +from oauthlib.common import Request +from oauthlib.oauth2.rfc6749.tokens import BearerToken +from oauthlib.openid.connect.core.grant_types import RefreshTokenGrant + +from tests.oauth2.rfc6749.grant_types.test_refresh_token import ( + RefreshTokenGrantTest, +) +from tests.unittest import TestCase + + +def get_id_token_mock(token, token_handler, request): + return "MOCKED_TOKEN" + + +class OpenIDRefreshTokenInterferenceTest(RefreshTokenGrantTest): + """Test that OpenID don't interfere with normal OAuth 2 flows.""" + + def setUp(self): + super().setUp() + self.auth = RefreshTokenGrant(request_validator=self.mock_validator) + + +class OpenIDRefreshTokenTest(TestCase): + + def setUp(self): + self.request = Request('http://a.b/path') + self.request.grant_type = 'refresh_token' + self.request.refresh_token = 'lsdkfhj230' + self.request.scope = ('hello', 'openid') + self.mock_validator = mock.MagicMock() + + 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 = RefreshTokenGrant(request_validator=self.mock_validator) + + def set_client(self, request): + request.client = mock.MagicMock() + request.client.client_id = 'mocked' + return True + + def test_refresh_id_token(self): + self.mock_validator.get_original_scopes.return_value = [ + 'hello', 'openid' + ] + bearer = BearerToken(self.mock_validator) + + headers, body, status_code = self.auth.create_token_response( + self.request, bearer + ) + + token = json.loads(body) + self.assertEqual(self.mock_validator.save_token.call_count, 1) + self.assertIn('access_token', token) + self.assertIn('refresh_token', token) + self.assertIn('id_token', token) + self.assertIn('token_type', token) + self.assertIn('expires_in', token) + self.assertEqual(token['scope'], 'hello openid') + + def test_refresh_id_token_false(self): + self.auth.refresh_id_token = False + self.mock_validator.get_original_scopes.return_value = [ + 'hello', 'openid' + ] + bearer = BearerToken(self.mock_validator) + + headers, body, status_code = self.auth.create_token_response( + self.request, bearer + ) + + token = json.loads(body) + self.assertEqual(self.mock_validator.save_token.call_count, 1) + self.assertIn('access_token', token) + self.assertIn('refresh_token', token) + self.assertIn('token_type', token) + self.assertIn('expires_in', token) + self.assertEqual(token['scope'], 'hello openid') + self.assertNotIn('id_token', token) + + def test_refresh_token_without_openid_scope(self): + self.request.scope = "hello" + bearer = BearerToken(self.mock_validator) + + headers, body, status_code = self.auth.create_token_response( + self.request, bearer + ) + + token = json.loads(body) + self.assertEqual(self.mock_validator.save_token.call_count, 1) + self.assertIn('access_token', token) + self.assertIn('refresh_token', token) + self.assertIn('token_type', token) + self.assertIn('expires_in', token) + self.assertNotIn('id_token', token) + self.assertEqual(token['scope'], 'hello') From 5af989ced0f5e81a34edc0bca1ea429ffbaceed9 Mon Sep 17 00:00:00 2001 From: Nikos Sklikas Date: Mon, 22 Mar 2021 16:40:36 +0200 Subject: [PATCH 3/5] Update CHANGELOG --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 79f241d7..21c91593 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,9 @@ OAuth2.0 Provider - Bugfixes * #753: Fix acceptance of valid IPv6 addresses in URI validation +OAuth2.0 Provider - Features + * #751: OIDC add support of refreshing ID Tokens + OAuth2.0 Client - Bugfixes * #730: Base OAuth2 Client now has a consistent way of managing the `scope`: it consistently @@ -25,6 +28,8 @@ OAuth2.0 Provider - Bugfixes * #746: OpenID Connect Hybrid - fix nonce not passed to add_id_token * #756: Different prompt values are now handled according to spec (e.g. prompt=none) * #759: OpenID Connect - fix Authorization: Basic parsing + * #751: The RefreshTokenGrant modifiers now take the same arguments as the + AuthorizationCodeGrant modifiers (`token`, `token_handler`, `request`). General * #716: improved skeleton validator for public vs private client From cebec2b075600e88c3fdcf554125ecf086e1b500 Mon Sep 17 00:00:00 2001 From: Nikos Sklikas Date: Wed, 7 Apr 2021 14:53:33 +0300 Subject: [PATCH 4/5] Add docs --- docs/oauth2/oidc/refresh_token.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/oauth2/oidc/refresh_token.rst diff --git a/docs/oauth2/oidc/refresh_token.rst b/docs/oauth2/oidc/refresh_token.rst new file mode 100644 index 00000000..01d2d7f1 --- /dev/null +++ b/docs/oauth2/oidc/refresh_token.rst @@ -0,0 +1,6 @@ +OpenID Authorization Code +------------------------- + +.. autoclass:: oauthlib.openid.connect.core.grant_types.RefreshTokenGrant + :members: + :inherited-members: From f6b625886d03f1582a7a99317e84c57d03895339 Mon Sep 17 00:00:00 2001 From: Nikos Sklikas Date: Wed, 2 Jun 2021 11:12:32 +0300 Subject: [PATCH 5/5] Move refresh_id_token to validator function --- .../openid/connect/core/grant_types/refresh_token.py | 6 ++---- oauthlib/openid/connect/core/request_validator.py | 12 ++++++++++++ .../connect/core/grant_types/test_refresh_token.py | 8 +++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/oauthlib/openid/connect/core/grant_types/refresh_token.py b/oauthlib/openid/connect/core/grant_types/refresh_token.py index 386b57cd..43e4499c 100644 --- a/oauthlib/openid/connect/core/grant_types/refresh_token.py +++ b/oauthlib/openid/connect/core/grant_types/refresh_token.py @@ -15,8 +15,7 @@ class RefreshTokenGrant(GrantTypeBase): - def __init__(self, refresh_id_token=True, request_validator=None, **kwargs): - self.refresh_id_token = refresh_id_token + def __init__(self, request_validator=None, **kwargs): self.proxy_target = OAuth2RefreshTokenGrant( request_validator=request_validator, **kwargs) self.register_token_modifier(self.add_id_token) @@ -29,8 +28,7 @@ def add_id_token(self, token, token_handler, request): The authorization_code version of this method is used to retrieve the nonce accordingly to the code storage. """ - # Treat it as normal OAuth 2 auth code request if openid is not present - if not self.refresh_id_token: + if not self.request_validator.refresh_id_token(request): return token return super().add_id_token(token, token_handler, request) diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index e8f334b0..47c4cd94 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -306,3 +306,15 @@ def get_userinfo_claims(self, request): Method is used by: UserInfoEndpoint """ + + def refresh_id_token(self, request): + """Whether the id token should be refreshed. Default, True + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :rtype: True or False + + Method is used by: + RefreshTokenGrant + """ + return True diff --git a/tests/openid/connect/core/grant_types/test_refresh_token.py b/tests/openid/connect/core/grant_types/test_refresh_token.py index c19de188..8126e1b8 100644 --- a/tests/openid/connect/core/grant_types/test_refresh_token.py +++ b/tests/openid/connect/core/grant_types/test_refresh_token.py @@ -60,9 +60,12 @@ def test_refresh_id_token(self): self.assertIn('token_type', token) self.assertIn('expires_in', token) self.assertEqual(token['scope'], 'hello openid') + self.mock_validator.refresh_id_token.assert_called_once_with( + self.request + ) def test_refresh_id_token_false(self): - self.auth.refresh_id_token = False + self.mock_validator.refresh_id_token.return_value = False self.mock_validator.get_original_scopes.return_value = [ 'hello', 'openid' ] @@ -80,6 +83,9 @@ def test_refresh_id_token_false(self): self.assertIn('expires_in', token) self.assertEqual(token['scope'], 'hello openid') self.assertNotIn('id_token', token) + self.mock_validator.refresh_id_token.assert_called_once_with( + self.request + ) def test_refresh_token_without_openid_scope(self): self.request.scope = "hello"