Repository: jazzband/django-oauth-toolkit

Issue:
Cannot revoke OAuth2 RefreshToken without related Token
##### SUMMARY
Django-OAuth-Toolkit supports deleting a token, but does not cascade on delete for it's RefreshToken anymore as of DOT 1.1.2.  This creates an issue when you try to revoke a RefreshToken that no longer has an associated AccessToken.  This will cause a traceback at [this line](https://github.com/jazzband/django-oauth-toolkit/blob/master/oauth2_provider/models.py#L380).  

> This could also potentially become an issue in reverse [here](https://github.com/jazzband/django-oauth-toolkit/blob/1.1.2/oauth2_provider/models.py#L365), but is unlikely to be hit unless a user is manually deleting RefreshTokens.  

Currently, users have no way to delete RefreshToken's as of DOT 1.1.2 given these circumstances, which is an issue because RefreshTokens can be used to issue a new AccessToken.  


##### ENVIRONMENT
* Django OAuth Toolkit version 1.1.2
* Python version 2.7.5

##### STEPS TO REPRODUCE

1. Create App
2. Create AccessToken
3. Revoke AccessToken using either method (`api/o/revoke_token`)
4. Observe that the RefreshToken remains and the AccessToken is deleted. 
^^ This all is as expected
5. Revoke RefreshToken (we currently can't do this)

```
awx.main.models.oauth.DoesNotExist: OAuth2AccessToken matching query does not exist.
```

Related Issue from AWX: https://github.com/ansible/awx/pull/2044



Discussion:

Resolving patch:
diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py
--- a/oauth2_provider/models.py
+++ b/oauth2_provider/models.py
@@ -377,7 +377,10 @@ def revoke(self):
             if not self:
                 return
 
-            access_token_model.objects.get(id=self.access_token_id).revoke()
+            try:
+                access_token_model.objects.get(id=self.access_token_id).revoke()
+            except access_token_model.DoesNotExist:
+                pass
             self.access_token = None
             self.revoked = timezone.now()
             self.save()


Test patch:
diff --git a/tests/test_token_revocation.py b/tests/test_token_revocation.py
--- a/tests/test_token_revocation.py
+++ b/tests/test_token_revocation.py
@@ -151,6 +151,31 @@ def test_revoke_refresh_token(self):
         self.assertIsNotNone(refresh_token.revoked)
         self.assertFalse(AccessToken.objects.filter(id=rtok.access_token.id).exists())
 
+    def test_revoke_refresh_token_with_revoked_access_token(self):
+        tok = AccessToken.objects.create(
+            user=self.test_user, token="1234567890",
+            application=self.application,
+            expires=timezone.now() + datetime.timedelta(days=1),
+            scope="read write"
+        )
+        rtok = RefreshToken.objects.create(
+            user=self.test_user, token="999999999",
+            application=self.application, access_token=tok
+        )
+        for token in (tok.token, rtok.token):
+            query_string = urlencode({
+                "client_id": self.application.client_id,
+                "client_secret": self.application.client_secret,
+                "token": token,
+            })
+            url = "{url}?{qs}".format(url=reverse("oauth2_provider:revoke-token"), qs=query_string)
+            response = self.client.post(url)
+            self.assertEqual(response.status_code, 200)
+
+        self.assertFalse(AccessToken.objects.filter(id=tok.id).exists())
+        refresh_token = RefreshToken.objects.filter(id=rtok.id).first()
+        self.assertIsNotNone(refresh_token.revoked)
+
     def test_revoke_token_with_wrong_hint(self):
         """
         From the revocation rfc, `Section 4.1.2`_ :