Repository: jazzband/django-oauth-toolkit

Issue:
Grace period incompatible with token rotation
This has been discussed on #631, but I think the merged solution(#660) does not fix it.

From what I understand, the objective of the grace period is to allow clients to send another refresh_token request using the same refresh token and get the newly generated access token back every time during such period.

The problem with #660 is that it only makes sure the new access token is returned, not the new refresh token generated by the token rotation.

This means that all refresh_token requests made during the grace period will return the **revoked refresh token** instead of the new one generated during the first refresh_token request. As such, if the client loses the new refresh token returned during the first call, it won't be able to renew tokens anymore.

To illustrate, assume we have a pair of access-refresh(A1, R1) tokens that we want to refresh.
This is the current behavior:
## First refresh
- R1 is sent to /o/token/
- R1 is revoked
- /o/token/ returns a pair (A2, R2) of new access and refresh tokens (per token rotation)

## Second refresh (during grace period)
- R1 is sent to /o/token/
- R1 is already revoked
- /o/token/ returns a pair (A2, R1) of the new access token and the **old, revoked refresh token**

Shouldn't the second refresh call return (A2, R2)? Is this the desired behavior? 
If not, I already have a fix and will create a pull request.


Discussion:
please create a PR and let's discuss there

Resolving patch:
diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py
--- a/oauth2_provider/oauth2_validators.py
+++ b/oauth2_provider/oauth2_validators.py
@@ -551,7 +551,9 @@ def save_bearer_token(self, token, request, *args, **kwargs):
                     # make sure that the token data we're returning matches
                     # the existing token
                     token["access_token"] = previous_access_token.token
-                    token["refresh_token"] = previous_access_token.source_refresh_token.token
+                    token["refresh_token"] = RefreshToken.objects.filter(
+                        access_token=previous_access_token
+                    ).first().token
                     token["scope"] = previous_access_token.scope
 
         # No refresh token should be created, just access token


Test patch:
diff --git a/tests/test_authorization_code.py b/tests/test_authorization_code.py
--- a/tests/test_authorization_code.py
+++ b/tests/test_authorization_code.py
@@ -672,13 +672,14 @@ def test_refresh_with_grace_period(self):
             "refresh_token": content["refresh_token"],
             "scope": content["scope"],
         }
-        refresh_token = content["refresh_token"]
+
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
         self.assertEqual(response.status_code, 200)
 
         content = json.loads(response.content.decode("utf-8"))
         self.assertTrue("access_token" in content)
         first_access_token = content["access_token"]
+        first_refresh_token = content["refresh_token"]
 
         # check access token returns same data if used twice, see #497
         response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers)
@@ -688,7 +689,7 @@ def test_refresh_with_grace_period(self):
         self.assertEqual(content["access_token"], first_access_token)
         # refresh token should be the same as well
         self.assertTrue("refresh_token" in content)
-        self.assertEqual(content["refresh_token"], refresh_token)
+        self.assertEqual(content["refresh_token"], first_refresh_token)
         oauth2_settings.REFRESH_TOKEN_GRACE_PERIOD_SECONDS = 0
 
     def test_refresh_invalidates_old_tokens(self):