Skip to content

Commit 5c60bb3

Browse files
committed
integrate token revocation flow
1 parent 324cd9c commit 5c60bb3

File tree

7 files changed

+187
-1
lines changed

7 files changed

+187
-1
lines changed

oauth2_provider/oauth2_backends.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ def create_token_response(self, request):
108108

109109
return uri, headers, body, status
110110

111+
def create_revocation_response(self, request):
112+
"""
113+
A wrapper method that calls create_revocation_response on a
114+
`server_class` instance.
115+
116+
:param request: The current django.http.HttpRequest object
117+
"""
118+
uri, http_method, body, headers = self._extract_params(request)
119+
120+
headers, body, status = self.server.create_revocation_response(
121+
uri, http_method, body, headers)
122+
uri = headers.get("Location", None)
123+
124+
return uri, headers, body, status
125+
111126
def verify_request(self, request, scopes):
112127
"""
113128
A wrapper method that calls verify_request on `server_class` instance.

oauth2_provider/oauth2_validators.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,31 @@ def save_bearer_token(self, token, request, *args, **kwargs):
292292
# TODO check out a more reliable way to communicate expire time to oauthlib
293293
token['expires_in'] = oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS
294294

295+
def revoke_token(self, token, token_type_hint, request, *args, **kwargs):
296+
"""
297+
Revoke an access or refresh token.
298+
299+
:param token: The token string.
300+
:param token_type_hint: access_token or refresh_token.
301+
:param request: The HTTP Request (oauthlib.common.Request)
302+
"""
303+
if token_type_hint not in [None, 'access_token', 'refresh_token']:
304+
token_type_hint = None
305+
306+
if token_type_hint in [None, 'access_token']:
307+
try:
308+
AccessToken.objects.get(token=token).delete()
309+
return
310+
except AccessToken.DoesNotExist:
311+
pass
312+
313+
if token_type_hint in [None, 'refresh_token']:
314+
try:
315+
RefreshToken.objects.get(token=token).delete()
316+
return
317+
except RefreshToken.DoesNotExist:
318+
pass
319+
295320
def validate_user(self, username, password, client, request, *args, **kwargs):
296321
"""
297322
Check username and password correspond to a valid and active User
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from __future__ import unicode_literals
2+
3+
import datetime
4+
5+
from django.test import TestCase, RequestFactory
6+
from django.core.urlresolvers import reverse
7+
from django.utils import timezone
8+
9+
from ..compat import urlencode, get_user_model
10+
from ..models import get_application_model, AccessToken, RefreshToken
11+
from ..settings import oauth2_settings
12+
13+
from .test_utils import TestCaseUtils
14+
15+
16+
Application = get_application_model()
17+
UserModel = get_user_model()
18+
19+
20+
class BaseTest(TestCaseUtils, TestCase):
21+
def setUp(self):
22+
self.factory = RequestFactory()
23+
self.test_user = UserModel.objects.create_user("test_user", "[email protected]", "123456")
24+
self.dev_user = UserModel.objects.create_user("dev_user", "[email protected]", "123456")
25+
26+
self.application = Application(
27+
name="Test Application",
28+
redirect_uris="http://localhost http://example.com http://example.it",
29+
user=self.dev_user,
30+
client_type=Application.CLIENT_CONFIDENTIAL,
31+
authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
32+
)
33+
self.application.save()
34+
35+
oauth2_settings._SCOPES = ['read', 'write']
36+
37+
def tearDown(self):
38+
self.application.delete()
39+
self.test_user.delete()
40+
self.dev_user.delete()
41+
42+
43+
class TestRevocationView(BaseTest):
44+
def test_revoke_access_token(self):
45+
"""
46+
47+
"""
48+
tok = AccessToken.objects.create(user=self.test_user, token='1234567890',
49+
application=self.application,
50+
expires=timezone.now()+datetime.timedelta(days=1),
51+
scope='read write')
52+
query_string = urlencode({
53+
'client_id': self.application.client_id,
54+
'client_secret': self.application.client_secret,
55+
'token': tok.token,
56+
})
57+
url = "{url}?{qs}".format(url=reverse('oauth2_provider:revoke-token'), qs=query_string)
58+
response = self.client.post(url)
59+
self.assertEqual(response.status_code, 200)
60+
self.assertFalse(AccessToken.objects.filter(id=tok.id).exists())
61+
62+
def test_revoke_access_token_with_hint(self):
63+
"""
64+
65+
"""
66+
tok = AccessToken.objects.create(user=self.test_user, token='1234567890',
67+
application=self.application,
68+
expires=timezone.now()+datetime.timedelta(days=1),
69+
scope='read write')
70+
query_string = urlencode({
71+
'client_id': self.application.client_id,
72+
'client_secret': self.application.client_secret,
73+
'token': tok.token,
74+
'token_type_hint': 'access_token'
75+
})
76+
url = "{url}?{qs}".format(url=reverse('oauth2_provider:revoke-token'), qs=query_string)
77+
response = self.client.post(url)
78+
self.assertEqual(response.status_code, 200)
79+
self.assertFalse(AccessToken.objects.filter(id=tok.id).exists())
80+
81+
def test_revoke_access_token_with_invalid_hint(self):
82+
"""
83+
84+
"""
85+
tok = AccessToken.objects.create(user=self.test_user, token='1234567890',
86+
application=self.application,
87+
expires=timezone.now()+datetime.timedelta(days=1),
88+
scope='read write')
89+
# invalid hint should have no effect
90+
query_string = urlencode({
91+
'client_id': self.application.client_id,
92+
'client_secret': self.application.client_secret,
93+
'token': tok.token,
94+
'token_type_hint': 'bad_hint'
95+
})
96+
url = "{url}?{qs}".format(url=reverse('oauth2_provider:revoke-token'), qs=query_string)
97+
response = self.client.post(url)
98+
self.assertEqual(response.status_code, 200)
99+
self.assertFalse(AccessToken.objects.filter(id=tok.id).exists())
100+
101+
def test_revoke_refresh_token(self):
102+
"""
103+
104+
"""
105+
tok = AccessToken.objects.create(user=self.test_user, token='1234567890',
106+
application=self.application,
107+
expires=timezone.now()+datetime.timedelta(days=1),
108+
scope='read write')
109+
rtok = RefreshToken.objects.create(user=self.test_user, token='999999999',
110+
application=self.application, access_token=tok)
111+
query_string = urlencode({
112+
'client_id': self.application.client_id,
113+
'client_secret': self.application.client_secret,
114+
'token': rtok.token,
115+
})
116+
url = "{url}?{qs}".format(url=reverse('oauth2_provider:revoke-token'), qs=query_string)
117+
response = self.client.post(url)
118+
self.assertEqual(response.status_code, 200)
119+
self.assertFalse(RefreshToken.objects.filter(id=rtok.id).exists())

oauth2_provider/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
'',
88
url(r'^authorize/$', views.AuthorizationView.as_view(), name="authorize"),
99
url(r'^token/$', views.TokenView.as_view(), name="token"),
10+
url(r'^revoke_token/$', views.RevokeTokenView.as_view(), name="revoke-token"),
1011
)
1112

1213
# Application management views

oauth2_provider/views/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .base import AuthorizationView, TokenView
1+
from .base import AuthorizationView, TokenView, RevokeTokenView
22
from .application import ApplicationRegistration, ApplicationDetail, ApplicationList, \
33
ApplicationDelete, ApplicationUpdate
44
from .generic import ProtectedResourceView, ScopedProtectedResourceView, ReadWriteScopedResourceView

oauth2_provider/views/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,19 @@ def post(self, request, *args, **kwargs):
157157
for k, v in headers.items():
158158
response[k] = v
159159
return response
160+
161+
162+
class RevokeTokenView(CsrfExemptMixin, OAuthLibMixin, View):
163+
"""
164+
Implements an endpoint to revoke access or refresh tokens
165+
"""
166+
server_class = Server
167+
validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS
168+
169+
def post(self, request, *args, **kwargs):
170+
url, headers, body, status = self.create_revocation_response(request)
171+
response = HttpResponse(content=body, status=status)
172+
173+
for k, v in headers.items():
174+
response[k] = v
175+
return response

oauth2_provider/views/mixins.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ def create_token_response(self, request):
123123
core = self.get_oauthlib_core()
124124
return core.create_token_response(request)
125125

126+
def create_revocation_response(self, request):
127+
"""
128+
A wrapper method that calls create_revocation_response on the
129+
`server_class` instance.
130+
131+
:param request: The current django.http.HttpRequest object
132+
"""
133+
core = self.get_oauthlib_core()
134+
return core.create_revocation_response(request)
135+
126136
def verify_request(self, request):
127137
"""
128138
A wrapper method that calls verify_request on `server_class` instance.

0 commit comments

Comments
 (0)