diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py
index 6ae1384fb..90a5ade2a 100644
--- a/oauth2_provider/models.py
+++ b/oauth2_provider/models.py
@@ -221,6 +221,13 @@ def revoke(self):
"""
self.delete()
+ @property
+ def scopes(self):
+ """
+ Returns a dictionary of allowed scope names (as keys) with their descriptions (as values)
+ """
+ return {name: desc for name, desc in oauth2_settings.SCOPES.items() if name in self.scope.split()}
+
def __str__(self):
return self.token
diff --git a/oauth2_provider/templates/oauth2_provider/authorized-token-delete.html b/oauth2_provider/templates/oauth2_provider/authorized-token-delete.html
new file mode 100644
index 000000000..e08233a70
--- /dev/null
+++ b/oauth2_provider/templates/oauth2_provider/authorized-token-delete.html
@@ -0,0 +1,9 @@
+{% extends "oauth2_provider/base.html" %}
+
+{% load i18n %}
+{% block content %}
+
+{% endblock %}
diff --git a/oauth2_provider/templates/oauth2_provider/authorized-tokens.html b/oauth2_provider/templates/oauth2_provider/authorized-tokens.html
new file mode 100644
index 000000000..f25069e61
--- /dev/null
+++ b/oauth2_provider/templates/oauth2_provider/authorized-tokens.html
@@ -0,0 +1,24 @@
+{% extends "oauth2_provider/base.html" %}
+
+{% load i18n %}
+{% load url from compat %}
+{% block content %}
+
+
{% trans "Tokens" %}
+
+ {% for authorized_token in authorized_tokens %}
+ -
+ {{ authorized_token.application }}
+ (revoke)
+
+
+ {% for scope_name, scope_description in authorized_token.scopes.items %}
+ - {{ scope_name }}: {{ scope_description }}
+ {% endfor %}
+
+ {% empty %}
+ - {% trans "There are no authorized tokens yet." %}
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/oauth2_provider/tests/test_models.py b/oauth2_provider/tests/test_models.py
index d082ab8dc..d12ed30ef 100644
--- a/oauth2_provider/tests/test_models.py
+++ b/oauth2_provider/tests/test_models.py
@@ -81,6 +81,36 @@ def test_str(self):
app.name = "test_app"
self.assertEqual("%s" % app, "test_app")
+ def test_scopes_property(self):
+ self.client.login(username="test_user", password="123456")
+
+ app = Application.objects.create(
+ name="test_app",
+ redirect_uris="http://localhost http://example.com http://example.it",
+ user=self.user,
+ client_type=Application.CLIENT_CONFIDENTIAL,
+ authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
+ )
+
+ access_token = AccessToken(
+ user=self.user,
+ scope='read write',
+ expires=0,
+ token='',
+ application=app
+ )
+
+ access_token2 = AccessToken(
+ user=self.user,
+ scope='write',
+ expires=0,
+ token='',
+ application=app
+ )
+
+ self.assertEqual(access_token.scopes, {'read': 'Reading scope', 'write': 'Writing scope'})
+ self.assertEqual(access_token2.scopes, {'write': 'Writing scope'})
+
@skipIf(django.VERSION < (1, 5), "Behavior is broken on 1.4 and there is no solution")
@override_settings(OAUTH2_PROVIDER_APPLICATION_MODEL='tests.TestApplication')
diff --git a/oauth2_provider/tests/test_token_view.py b/oauth2_provider/tests/test_token_view.py
new file mode 100644
index 000000000..7e02a32b2
--- /dev/null
+++ b/oauth2_provider/tests/test_token_view.py
@@ -0,0 +1,178 @@
+from __future__ import unicode_literals
+
+import datetime
+
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+from django.utils import timezone
+
+from ..models import get_application_model, AccessToken
+from ..compat import get_user_model
+
+Application = get_application_model()
+UserModel = get_user_model()
+
+
+class TestAuthorizedTokenViews(TestCase):
+ """
+ TestCase superclass for Authorized Token Views' Test Cases
+ """
+ def setUp(self):
+ self.foo_user = UserModel.objects.create_user("foo_user", "test@user.com", "123456")
+ self.bar_user = UserModel.objects.create_user("bar_user", "dev@user.com", "123456")
+
+ self.application = Application(
+ name="Test Application",
+ redirect_uris="http://localhost http://example.com http://example.it",
+ user=self.bar_user,
+ client_type=Application.CLIENT_CONFIDENTIAL,
+ authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
+ )
+ self.application.save()
+
+ def tearDown(self):
+ self.foo_user.delete()
+ self.bar_user.delete()
+
+
+class TestAuthorizedTokenListView(TestAuthorizedTokenViews):
+ """
+ Tests for the Authorized Token ListView
+ """
+ def test_list_view_authorization_required(self):
+ """
+ Test that the view redirects to login page if user is not logged-in.
+ """
+ response = self.client.get(reverse('oauth2_provider:authorized-token-list'))
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue('/accounts/login/?next=' in response['Location'])
+
+ def test_empty_list_view(self):
+ """
+ Test that when you have no tokens, an appropriate message is shown
+ """
+ self.client.login(username="foo_user", password="123456")
+
+ response = self.client.get(reverse('oauth2_provider:authorized-token-list'))
+ self.assertEqual(response.status_code, 200)
+ self.assertIn(b'There are no authorized tokens yet.', response.content)
+
+ def test_list_view_one_token(self):
+ """
+ Test that the view shows your token
+ """
+ self.client.login(username="bar_user", password="123456")
+ AccessToken.objects.create(user=self.bar_user, token='1234567890',
+ application=self.application,
+ expires=timezone.now() + datetime.timedelta(days=1),
+ scope='read write')
+
+ response = self.client.get(reverse('oauth2_provider:authorized-token-list'))
+ self.assertEqual(response.status_code, 200)
+ self.assertIn(b'read', response.content)
+ self.assertIn(b'write', response.content)
+ self.assertNotIn(b'There are no authorized tokens yet.', response.content)
+
+ def test_list_view_two_tokens(self):
+ """
+ Test that the view shows your tokens
+ """
+ self.client.login(username="bar_user", password="123456")
+ AccessToken.objects.create(user=self.bar_user, token='1234567890',
+ application=self.application,
+ expires=timezone.now() + datetime.timedelta(days=1),
+ scope='read write')
+ AccessToken.objects.create(user=self.bar_user, token='0123456789',
+ application=self.application,
+ expires=timezone.now() + datetime.timedelta(days=1),
+ scope='read write')
+
+ response = self.client.get(reverse('oauth2_provider:authorized-token-list'))
+ self.assertEqual(response.status_code, 200)
+ self.assertNotIn(b'There are no authorized tokens yet.', response.content)
+
+ def test_list_view_shows_correct_user_token(self):
+ """
+ Test that only currently logged-in user's tokens are shown
+ """
+ self.client.login(username="bar_user", password="123456")
+ AccessToken.objects.create(user=self.foo_user, token='1234567890',
+ application=self.application,
+ expires=timezone.now() + datetime.timedelta(days=1),
+ scope='read write')
+
+ response = self.client.get(reverse('oauth2_provider:authorized-token-list'))
+ self.assertEqual(response.status_code, 200)
+ self.assertIn(b'There are no authorized tokens yet.', response.content)
+
+
+class TestAuthorizedTokenDeleteView(TestAuthorizedTokenViews):
+ """
+ Tests for the Authorized Token DeleteView
+ """
+ def test_delete_view_authorization_required(self):
+ """
+ Test that the view redirects to login page if user is not logged-in.
+ """
+ self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890',
+ application=self.application,
+ expires=timezone.now() + datetime.timedelta(days=1),
+ scope='read write')
+
+ response = self.client.get(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk}))
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue('/accounts/login/?next=' in response['Location'])
+
+ def test_delete_view_works(self):
+ """
+ Test that a GET on this view returns 200 if the token belongs to the logged-in user.
+ """
+ self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890',
+ application=self.application,
+ expires=timezone.now() + datetime.timedelta(days=1),
+ scope='read write')
+
+ self.client.login(username="foo_user", password="123456")
+ response = self.client.get(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk}))
+ self.assertEqual(response.status_code, 200)
+
+ def test_delete_view_token_belongs_to_user(self):
+ """
+ Test that a 404 is returned when trying to GET this view with someone else's tokens.
+ """
+ self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890',
+ application=self.application,
+ expires=timezone.now() + datetime.timedelta(days=1),
+ scope='read write')
+
+ self.client.login(username="bar_user", password="123456")
+ response = self.client.get(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk}))
+ self.assertEqual(response.status_code, 404)
+
+ def test_delete_view_post_actually_deletes(self):
+ """
+ Test that a POST on this view works if the token belongs to the logged-in user.
+ """
+ self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890',
+ application=self.application,
+ expires=timezone.now() + datetime.timedelta(days=1),
+ scope='read write')
+
+ self.client.login(username="foo_user", password="123456")
+ response = self.client.post(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk}))
+ self.assertFalse(AccessToken.objects.exists())
+ self.assertRedirects(response, reverse('oauth2_provider:authorized-token-list'))
+
+ def test_delete_view_only_deletes_user_own_token(self):
+ """
+ Test that a 404 is returned when trying to POST on this view with someone else's tokens.
+ """
+ self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890',
+ application=self.application,
+ expires=timezone.now() + datetime.timedelta(days=1),
+ scope='read write')
+
+ self.client.login(username="bar_user", password="123456")
+ response = self.client.post(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk}))
+ self.assertTrue(AccessToken.objects.exists())
+ self.assertEqual(response.status_code, 404)
diff --git a/oauth2_provider/urls.py b/oauth2_provider/urls.py
index e098e4148..ebcb9e0b6 100644
--- a/oauth2_provider/urls.py
+++ b/oauth2_provider/urls.py
@@ -17,3 +17,9 @@
url(r'^applications/(?P\d+)/delete/$', views.ApplicationDelete.as_view(), name="delete"),
url(r'^applications/(?P\d+)/update/$', views.ApplicationUpdate.as_view(), name="update"),
)
+
+urlpatterns += (
+ url(r'^authorized_tokens/$', views.AuthorizedTokensListView.as_view(), name="authorized-token-list"),
+ url(r'^authorized_tokens/(?P\d+)/delete/$', views.AuthorizedTokenDeleteView.as_view(),
+ name="authorized-token-delete"),
+)
diff --git a/oauth2_provider/views/__init__.py b/oauth2_provider/views/__init__.py
index e285d518b..257c86add 100644
--- a/oauth2_provider/views/__init__.py
+++ b/oauth2_provider/views/__init__.py
@@ -2,3 +2,4 @@
from .application import ApplicationRegistration, ApplicationDetail, ApplicationList, \
ApplicationDelete, ApplicationUpdate
from .generic import ProtectedResourceView, ScopedProtectedResourceView, ReadWriteScopedResourceView
+from .token import AuthorizedTokensListView, AuthorizedTokenDeleteView
diff --git a/oauth2_provider/views/token.py b/oauth2_provider/views/token.py
new file mode 100644
index 000000000..f7e4562e9
--- /dev/null
+++ b/oauth2_provider/views/token.py
@@ -0,0 +1,36 @@
+from __future__ import absolute_import, unicode_literals
+
+from django.core.urlresolvers import reverse_lazy
+from django.views.generic import ListView, DeleteView
+
+from braces.views import LoginRequiredMixin
+
+from ..models import AccessToken
+
+
+class AuthorizedTokensListView(LoginRequiredMixin, ListView):
+ """
+ Show a page where the current logged-in user can see his tokens so they can revoke them
+ """
+ context_object_name = 'authorized_tokens'
+ template_name = 'oauth2_provider/authorized-tokens.html'
+ model = AccessToken
+
+ def get_queryset(self):
+ """
+ Show only user's tokens
+ """
+ return super(AuthorizedTokensListView, self).get_queryset()\
+ .select_related('application').filter(user=self.request.user)
+
+
+class AuthorizedTokenDeleteView(LoginRequiredMixin, DeleteView):
+ """
+ View for revoking a specific token
+ """
+ template_name = 'oauth2_provider/authorized-token-delete.html'
+ success_url = reverse_lazy('oauth2_provider:authorized-token-list')
+ model = AccessToken
+
+ def get_queryset(self):
+ return super(AuthorizedTokenDeleteView, self).get_queryset().filter(user=self.request.user)