Repository: oauthlib/oauthlib

Issue:
Refresh id tokens
**Describe the feature**
It is not possible with the current implementation to issue ID tokens on refresh.

**Additional context**

The refresh token modifiers (https://github.com/oauthlib/oauthlib/blob/master/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py#L66) take only 1 argument while the authorization code modifiers take 3 (https://github.com/oauthlib/oauthlib/blob/master/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py#L308). 

The modifier function `add_id_token` used to add id tokens in the OIDC token response is not compatible with the refresh token's modifier interface. 

I see no reason to have that behavior. If there are no objections I can go ahead and create a PR that will harmonize the modifier arguments and add ID tokens to the refresh token responses for OIDC out of the box.

Comments:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 79f241d..21c9159 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
diff --git a/docs/oauth2/oidc/refresh_token.rst b/docs/oauth2/oidc/refresh_token.rst
new file mode 100644
index 0000000..01d2d7f
--- /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:
diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
index 8698a3d..f801de4 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
@@ -63,7 +63,7 @@ class RefreshTokenGrant(GrantTypeBase):
                                            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)
 
diff --git a/oauthlib/openid/connect/core/grant_types/__init__.py b/oauthlib/openid/connect/core/grant_types/__init__.py
index 887a585..8dad5f6 100644
--- a/oauthlib/openid/connect/core/grant_types/__init__.py
+++ b/oauthlib/openid/connect/core/grant_types/__init__.py
@@ -10,3 +10,4 @@ from .dispatchers import (
 )
 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 0000000..43e4499
--- /dev/null
+++ b/oauthlib/openid/connect/core/grant_types/refresh_token.py
@@ -0,0 +1,34 @@
+"""
+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, request_validator=None, **kwargs):
+        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.
+        """
+        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 e8f334b..47c4cd9 100644
--- a/oauthlib/openid/connect/core/request_validator.py
+++ b/oauthlib/openid/connect/core/request_validator.py
@@ -306,3 +306,15 @@ class RequestValidator(OAuth2RequestValidator):
         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


Test patch:
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 0000000..8126e1b
--- /dev/null
+++ b/tests/openid/connect/core/grant_types/test_refresh_token.py
@@ -0,0 +1,105 @@
+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')
+        self.mock_validator.refresh_id_token.assert_called_once_with(
+            self.request
+        )
+
+    def test_refresh_id_token_false(self):
+        self.mock_validator.refresh_id_token.return_value = 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)
+        self.mock_validator.refresh_id_token.assert_called_once_with(
+            self.request
+        )
+
+    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')