Skip to content
This repository was archived by the owner on Mar 3, 2020. It is now read-only.

Commit 656aa92

Browse files
committed
Added access token detail view
This view is useful for APIs not hosted on the authentication server to determine if a given access token is valid.
1 parent 6a784e8 commit 656aa92

File tree

5 files changed

+124
-29
lines changed

5 files changed

+124
-29
lines changed

provider/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.7-fork-edx-3"
1+
__version__ = "0.2.7-fork-edx-4"

provider/oauth2/tests.py

Lines changed: 78 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import json
22
import urlparse
33
import datetime
4+
45
from django.http import QueryDict
56
from django.conf import settings
67
from django.core.urlresolvers import reverse
78
from django.utils.html import escape
89
from django.test import TestCase
910
from django.contrib.auth.models import User
11+
import mock
12+
1013
from .. import constants, scope
1114
from ..compat import skipIfCustomUser
15+
from provider.constants import CONFIDENTIAL, READ
1216
from ..templatetags.scope import scopes
1317
from ..utils import now as date_now
1418
from .forms import ClientForm
1519
from .models import Client, Grant, AccessToken, RefreshToken
16-
from .backends import BasicClientBackend, RequestParamsClientBackend
17-
from .backends import AccessTokenBackend
20+
from .backends import AccessTokenBackend, BasicClientBackend, RequestParamsClientBackend
1821

1922

2023
@skipIfCustomUser
@@ -109,7 +112,8 @@ def test_authorization_requires_response_type(self):
109112

110113
def test_authorization_requires_supported_response_type(self):
111114
self.login()
112-
response = self.client.get(self.auth_url() + '?client_id=%s&response_type=unsupported' % self.get_client().client_id)
115+
response = self.client.get(
116+
self.auth_url() + '?client_id=%s&response_type=unsupported' % self.get_client().client_id)
113117
response = self.client.get(self.auth_url2())
114118

115119
self.assertEqual(400, response.status_code)
@@ -144,7 +148,8 @@ def test_authorization_requires_a_valid_redirect_uri(self):
144148
def test_authorization_requires_a_valid_scope(self):
145149
self.login()
146150

147-
response = self.client.get(self.auth_url() + '?client_id=%s&response_type=code&scope=invalid+invalid2' % self.get_client().client_id)
151+
response = self.client.get(
152+
self.auth_url() + '?client_id=%s&response_type=code&scope=invalid+invalid2' % self.get_client().client_id)
148153
response = self.client.get(self.auth_url2())
149154

150155
self.assertEqual(400, response.status_code)
@@ -211,7 +216,8 @@ def test_access_token_get_expire_delta_value(self):
211216
now = date_now()
212217
default_expiration_timedelta = constants.EXPIRE_DELTA
213218
current_expiration_timedelta = datetime.timedelta(seconds=token.get_expire_delta(reference=now))
214-
self.assertTrue(abs(current_expiration_timedelta - default_expiration_timedelta) <= datetime.timedelta(seconds=1))
219+
self.assertLessEqual(abs(current_expiration_timedelta - default_expiration_timedelta),
220+
datetime.timedelta(seconds=1))
215221

216222
def test_fetching_access_token_with_invalid_client(self):
217223
self.login()
@@ -259,8 +265,7 @@ def _login_authorize_get_token(self):
259265
token = json.loads(response.content)
260266

261267
for prop in required_props:
262-
self.assertIn(prop, token, "Access token response missing "
263-
"required property: %s" % prop)
268+
self.assertIn(prop, token, "Access token response missing required property: %s" % prop)
264269

265270
return token
266271

@@ -283,8 +288,7 @@ def test_fetching_access_token_with_invalid_grant_type(self):
283288
})
284289

285290
self.assertEqual(400, response.status_code)
286-
self.assertEqual('unsupported_grant_type', json.loads(response.content)['error'],
287-
response.content)
291+
self.assertEqual('unsupported_grant_type', json.loads(response.content)['error'], response.content)
288292

289293
def test_fetching_single_access_token(self):
290294
constants.SINGLE_ACCESS_TOKEN = True
@@ -361,12 +365,11 @@ def test_refreshing_an_access_token(self):
361365
})
362366

363367
self.assertEqual(400, response.status_code)
364-
self.assertEqual('invalid_grant', json.loads(response.content)['error'],
365-
response.content)
368+
self.assertEqual('invalid_grant', json.loads(response.content)['error'], response.content)
366369

367370
def test_password_grant_public(self):
368371
c = self.get_client()
369-
c.client_type = 1 # public
372+
c.client_type = 1 # public
370373
c.save()
371374

372375
response = self.client.post(self.access_token_url(), {
@@ -385,7 +388,7 @@ def test_password_grant_public(self):
385388

386389
def test_password_grant_confidential(self):
387390
c = self.get_client()
388-
c.client_type = 0 # confidential
391+
c.client_type = 0 # confidential
389392
c.save()
390393

391394
response = self.client.post(self.access_token_url(), {
@@ -401,7 +404,7 @@ def test_password_grant_confidential(self):
401404

402405
def test_password_grant_confidential_no_secret(self):
403406
c = self.get_client()
404-
c.client_type = 0 # confidential
407+
c.client_type = 0 # confidential
405408
c.save()
406409

407410
response = self.client.post(self.access_token_url(), {
@@ -415,7 +418,7 @@ def test_password_grant_confidential_no_secret(self):
415418

416419
def test_password_grant_invalid_password_public(self):
417420
c = self.get_client()
418-
c.client_type = 1 # public
421+
c.client_type = 1 # public
419422
c.save()
420423

421424
response = self.client.post(self.access_token_url(), {
@@ -430,7 +433,7 @@ def test_password_grant_invalid_password_public(self):
430433

431434
def test_password_grant_invalid_password_confidential(self):
432435
c = self.get_client()
433-
c.client_type = 0 # confidential
436+
c.client_type = 0 # confidential
434437
c.save()
435438

436439
response = self.client.post(self.access_token_url(), {
@@ -475,8 +478,7 @@ def test_access_token_backend(self):
475478
client = self.get_client()
476479
backend = AccessTokenBackend()
477480
token = AccessToken.objects.create(user=user, client=client)
478-
authenticated = backend.authenticate(access_token=token.token,
479-
client=client)
481+
authenticated = backend.authenticate(access_token=token.token, client=client)
480482

481483
self.assertIsNotNone(authenticated)
482484

@@ -508,7 +510,7 @@ def test_access_token_enforces_SSL(self):
508510
class ClientFormTest(TestCase):
509511
def test_client_form(self):
510512
form = ClientForm({'name': 'TestName', 'url': 'http://127.0.0.1:8000',
511-
'redirect_uri': 'http://localhost:8000/'})
513+
'redirect_uri': 'http://localhost:8000/'})
512514

513515
self.assertFalse(form.is_valid())
514516

@@ -597,10 +599,8 @@ def test_clear_expired(self):
597599
# make sure the grant is gone
598600
self.assertFalse(Grant.objects.filter(code=code).exists())
599601
# and verify that the AccessToken and RefreshToken exist
600-
self.assertTrue(AccessToken.objects.filter(token=access_token)
601-
.exists())
602-
self.assertTrue(RefreshToken.objects.filter(token=refresh_token)
603-
.exists())
602+
self.assertTrue(AccessToken.objects.filter(token=access_token).exists())
603+
self.assertTrue(RefreshToken.objects.filter(token=refresh_token).exists())
604604

605605
# refresh the token
606606
response = self.client.post(self.access_token_url(), {
@@ -617,7 +617,58 @@ def test_clear_expired(self):
617617
self.assertNotEquals(refresh_token, token['refresh_token'])
618618

619619
# make sure the orig AccessToken and RefreshToken are gone
620-
self.assertFalse(AccessToken.objects.filter(token=access_token)
621-
.exists())
622-
self.assertFalse(RefreshToken.objects.filter(token=refresh_token)
623-
.exists())
620+
self.assertFalse(AccessToken.objects.filter(token=access_token).exists())
621+
self.assertFalse(RefreshToken.objects.filter(token=refresh_token).exists())
622+
623+
624+
class AccessTokenDetailViewTests(TestCase):
625+
JSON_CONTENT_TYPE = 'application/json'
626+
627+
def setUp(self):
628+
super(AccessTokenDetailViewTests, self)
629+
self.user = User.objects.create_user('TEST-USER', '[email protected]')
630+
self.oauth_client = Client.objects.create(client_type=CONFIDENTIAL)
631+
632+
def assert_invalid_token_response(self, token):
633+
""" Verifies that the view returns an invalid token response for the specified token. """
634+
url = reverse('oauth2:access_token_detail', kwargs={'token': token})
635+
response = self.client.get(url)
636+
self.assertEqual(response.status_code, 400)
637+
638+
self.assertEqual(response['Content-Type'], self.JSON_CONTENT_TYPE)
639+
self.assertEqual(response.content, json.dumps({'error': 'invalid_token'}))
640+
641+
def test_invalid_token(self):
642+
"""
643+
If the requested token is invalid for any reason (expired, doesn't exist, etc.) the view should return HTTP 400.
644+
"""
645+
# Non-existent token
646+
self.assert_invalid_token_response('abc')
647+
648+
# Expired token
649+
access_token = AccessToken.objects.create(user=self.user, client=self.oauth_client,
650+
expires=datetime.datetime.min)
651+
self.assert_invalid_token_response(access_token.token)
652+
653+
def test_valid_token(self):
654+
""" If the token is valid, details about the token should be returned. """
655+
656+
expires = datetime.datetime(2016, 1, 1, 0, 0, 0)
657+
access_token = AccessToken.objects.create(user=self.user, client=self.oauth_client, scope=READ, expires=expires)
658+
659+
url = reverse('oauth2:access_token_detail', kwargs={'token': access_token.token})
660+
661+
# Mock datetime.datetime.now() so that we can validate the expiration date
662+
now = datetime.datetime(2015, 1, 1, 0, 0, 0)
663+
with mock.patch('provider.oauth2.models.now', return_value=now):
664+
response = self.client.get(url)
665+
666+
self.assertEqual(response.status_code, 200)
667+
self.assertEqual(response['Content-Type'], self.JSON_CONTENT_TYPE)
668+
669+
expected = {
670+
'username': self.user.username,
671+
'scope': 'read',
672+
'expires_in': int((expires - now).total_seconds())
673+
}
674+
self.assertEqual(response.content, json.dumps(expected))

provider/oauth2/urls.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from django.contrib.auth.decorators import login_required
3737
from django.views.decorators.csrf import csrf_exempt
3838
from ..compat.urls import *
39-
from .views import Authorize, Redirect, Capture, AccessTokenView
39+
from .views import Authorize, Redirect, Capture, AccessTokenView, AccessTokenDetailView
4040

4141

4242
urlpatterns = patterns('',
@@ -52,4 +52,7 @@
5252
url('^access_token/?$',
5353
csrf_exempt(AccessTokenView.as_view()),
5454
name='access_token'),
55+
url('^access_token/(?P<token>[\w]+)/$',
56+
csrf_exempt(AccessTokenDetailView.as_view()),
57+
name='access_token_detail'),
5558
)

provider/oauth2/views.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from datetime import timedelta
2+
import json
3+
4+
from django.core.exceptions import ObjectDoesNotExist
25
from django.core.urlresolvers import reverse
6+
from django.http import HttpResponseBadRequest, HttpResponse
7+
from django.views.generic import View
8+
39
from .. import constants
410
from ..views import Capture, Authorize, Redirect
511
from ..views import AccessToken as AccessTokenView, OAuthError
@@ -15,6 +21,7 @@ class Capture(Capture):
1521
"""
1622
Implementation of :class:`provider.views.Capture`.
1723
"""
24+
1825
def get_redirect_url(self, request):
1926
return reverse('oauth2:authorize')
2027

@@ -23,6 +30,7 @@ class Authorize(Authorize):
2330
"""
2431
Implementation of :class:`provider.views.Authorize`.
2532
"""
33+
2634
def get_request_form(self, client, data):
2735
return AuthorizationRequestForm(data, client=client)
2836

@@ -137,3 +145,35 @@ def invalidate_access_token(self, at):
137145
else:
138146
at.expires = now() - timedelta(days=1)
139147
at.save()
148+
149+
150+
class AccessTokenDetailView(View):
151+
"""
152+
This view returns info about a given access token. If the token does not exist or is expired, HTTP 400 is returned.
153+
154+
A successful response has HTTP status 200 and includes a JSON object containing the username, scope, and expiry
155+
time (in seconds) for the access token.
156+
157+
Example
158+
GET /access_token/abc123/
159+
160+
{
161+
username: "some-user",
162+
scope: "read",
163+
expires_in: 60
164+
}
165+
"""
166+
167+
def get(self, request, *args, **kwargs):
168+
JSON_CONTENT_TYPE = 'application/json'
169+
170+
try:
171+
access_token = AccessToken.objects.get_token(kwargs['token'])
172+
content = {
173+
'username': access_token.user.username,
174+
'scope': access_token.get_scope_display(),
175+
'expires_in': access_token.get_expire_delta()
176+
}
177+
return HttpResponse(json.dumps(content), content_type=JSON_CONTENT_TYPE)
178+
except ObjectDoesNotExist:
179+
return HttpResponseBadRequest(json.dumps({'error': 'invalid_token'}), content_type=JSON_CONTENT_TYPE)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
Django==1.4
2+
mock==1.0.1
23
shortuuid==0.3

0 commit comments

Comments
 (0)