From ef517c8accc1a4a374ae0815c6a80e55966dd9a2 Mon Sep 17 00:00:00 2001 From: Nick Lang Date: Thu, 2 Apr 2015 03:16:21 -0600 Subject: [PATCH 1/5] Long running refresh tokens This allows for a client to request refresh tokens. These refresh tokens do not expire. They can be revoked (deleted). When a JWT has expired, it's possible to send a request with the refresh token in the header, and get back a new JWT. This allows for the client to not have to store username/passwords. So, if the client gets a responce about an expired token the client can automatically make a call (behind the scenes) to delegate a new JWT using the stored refresh token. Thus keeping the 'session' active. moving everything to it's own sub dir, so that the refresh token functionality can be optionally installed. --- rest_framework_jwt/authentication.py | 46 +++++- rest_framework_jwt/refreshtoken/__init__.py | 0 rest_framework_jwt/refreshtoken/models.py | 40 ++++++ .../refreshtoken/permissions.py | 21 +++ rest_framework_jwt/refreshtoken/routers.py | 11 ++ .../refreshtoken/serializers.py | 17 +++ rest_framework_jwt/refreshtoken/views.py | 65 +++++++++ rest_framework_jwt/views.py | 2 - runtests.py | 5 + tests/conftest.py | 3 + tests/test_views.py | 135 +++++++++++++++++- 11 files changed, 336 insertions(+), 9 deletions(-) create mode 100644 rest_framework_jwt/refreshtoken/__init__.py create mode 100644 rest_framework_jwt/refreshtoken/models.py create mode 100644 rest_framework_jwt/refreshtoken/permissions.py create mode 100644 rest_framework_jwt/refreshtoken/routers.py create mode 100644 rest_framework_jwt/refreshtoken/serializers.py create mode 100644 rest_framework_jwt/refreshtoken/views.py diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index 64d5f9a5..df2b29be 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -2,12 +2,13 @@ from django.utils.encoding import smart_text from django.utils.translation import ugettext as _ from rest_framework import exceptions -from rest_framework.authentication import (BaseAuthentication, - get_authorization_header) +from rest_framework.authentication import ( + BaseAuthentication, get_authorization_header, TokenAuthentication +) from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings - +from rest_framework_jwt.refreshtoken.models import RefreshToken jwt_decode_handler = api_settings.JWT_DECODE_HANDLER jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER @@ -97,3 +98,42 @@ def authenticate_header(self, request): authentication scheme should return `403 Permission Denied` responses. """ return 'JWT realm="{0}"'.format(self.www_authenticate_realm) + + +class RefreshTokenAuthentication(TokenAuthentication): + """ + Subclassed from rest_framework.authentication.TokenAuthentication + + Auth header: + Authorization: RefreshToken 401f7ac837da42b97f613d789819ff93537bee6a + """ + model = RefreshToken + + def authenticate(self, request): + auth = get_authorization_header(request).split() + + if not auth or auth[0].lower() != b'refreshtoken': + return None + + if len(auth) == 1: + msg = _('Invalid token header. No credentials provided.') + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = _('Invalid token header. Token string should not contain spaces.') + raise exceptions.AuthenticationFailed(msg) + + return self.authenticate_credentials(auth[1]) + + def authenticate_credentials(self, key): + try: + token = self.model.objects.select_related('user').get(key=key) + except self.model.DoesNotExist: + raise exceptions.AuthenticationFailed(_('Invalid token.')) + + if not token.user.is_active: + raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + + return (token.user, token) + + def authenticate_header(self, request): + return 'RefreshToken' diff --git a/rest_framework_jwt/refreshtoken/__init__.py b/rest_framework_jwt/refreshtoken/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rest_framework_jwt/refreshtoken/models.py b/rest_framework_jwt/refreshtoken/models.py new file mode 100644 index 00000000..3070554c --- /dev/null +++ b/rest_framework_jwt/refreshtoken/models.py @@ -0,0 +1,40 @@ +import binascii +import os + +from django.conf import settings +from django.db import models +from django.utils.encoding import python_2_unicode_compatible + + +# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist. +# Note that we don't perform this code in the compat module due to +# bug report #1297 +# See: https://github.com/tomchristie/django-rest-framework/issues/1297 +AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') + + +@python_2_unicode_compatible +class RefreshToken(models.Model): + """ + Copied from + https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/authtoken/models.py + Wanted to only change the user relation to be a "ForeignKey" instead of a OneToOneField + + The `ForeignKey` value allows us to create multiple RefreshTokens per user + + """ + key = models.CharField(max_length=40, primary_key=True) + user = models.ForeignKey(AUTH_USER_MODEL, related_name='refresh_tokens') + app = models.CharField(max_length=255, unique=True) + created = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs): + if not self.key: + self.key = self.generate_key() + return super(RefreshToken, self).save(*args, **kwargs) + + def generate_key(self): + return binascii.hexlify(os.urandom(20)).decode() + + def __str__(self): + return self.key diff --git a/rest_framework_jwt/refreshtoken/permissions.py b/rest_framework_jwt/refreshtoken/permissions.py new file mode 100644 index 00000000..1c08f80e --- /dev/null +++ b/rest_framework_jwt/refreshtoken/permissions.py @@ -0,0 +1,21 @@ +from rest_framework import permissions + + +class IsOwnerOrAdmin(permissions.BasePermission): + """ + Only admins or owners can have permission + """ + def has_permission(self, request, view): + return request.user and request.user.is_authenticated() + + def has_object_permission(self, request, view, obj): + """ + If user is staff or superuser or 'owner' of object return True + Else return false. + """ + if not request.user.is_authenticated(): + return False + elif request.user.is_staff or request.user.is_superuser: + return True + else: + return request.user == obj.user diff --git a/rest_framework_jwt/refreshtoken/routers.py b/rest_framework_jwt/refreshtoken/routers.py new file mode 100644 index 00000000..0a912ae9 --- /dev/null +++ b/rest_framework_jwt/refreshtoken/routers.py @@ -0,0 +1,11 @@ +from rest_framework import routers +from django.conf.urls import patterns, url + +from .views import RefreshTokenViewSet, DelagateJSONWebToken + +router = routers.SimpleRouter() +router.register(r'refresh-token', RefreshTokenViewSet) + +urlpatterns = router.urls + patterns('', # NOQA + url(r'delgate/$', DelagateJSONWebToken.as_view(), name='delgate-tokens'), +) diff --git a/rest_framework_jwt/refreshtoken/serializers.py b/rest_framework_jwt/refreshtoken/serializers.py new file mode 100644 index 00000000..d53be3a7 --- /dev/null +++ b/rest_framework_jwt/refreshtoken/serializers.py @@ -0,0 +1,17 @@ +from .models import RefreshToken +from rest_framework import serializers + + +class RefreshTokenSerializer(serializers.ModelSerializer): + """ + Serializer for refresh tokens (Not RefreshJWTToken) + """ + + class Meta: + model = RefreshToken + fields = ('key', 'user', 'created', 'app') + read_only_fields = ('key', 'user', 'created') + + def validate(self, attrs): + attrs['user'] = self.context['request'].user + return attrs diff --git a/rest_framework_jwt/refreshtoken/views.py b/rest_framework_jwt/refreshtoken/views.py new file mode 100644 index 00000000..54b21226 --- /dev/null +++ b/rest_framework_jwt/refreshtoken/views.py @@ -0,0 +1,65 @@ +from calendar import timegm +from datetime import datetime + +from rest_framework import mixins +from rest_framework import viewsets +from rest_framework.response import Response +from rest_framework import status +from rest_framework import parsers +from rest_framework import renderers + +from rest_framework_jwt.settings import api_settings +from rest_framework_jwt.views import JSONWebTokenAPIView +from rest_framework_jwt.authentication import RefreshTokenAuthentication + +from .permissions import IsOwnerOrAdmin +from .models import RefreshToken +from .serializers import RefreshTokenSerializer + +jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER +jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER + + +class DelagateJSONWebToken(JSONWebTokenAPIView): + """ + API View that checks the veracity of a refresh token, returning a JWT if it + is valid. + """ + authentication_classes = (RefreshTokenAuthentication, ) + + def post(self, request): + user = request.user + payload = jwt_payload_handler(user) + if api_settings.JWT_ALLOW_REFRESH: + payload['orig_iat'] = timegm(datetime.utcnow().utctimetuple()) + return Response( + {'token': jwt_encode_handler(payload)}, + status=status.HTTP_201_CREATED + ) + + +class RefreshTokenViewSet(mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + """ + API View that will Create/Delete/List `RefreshToken`. + + https://auth0.com/docs/refresh-token + """ + throttle_classes = () + authentication_classes = () + parser_classes = (parsers.FormParser, parsers.JSONParser,) + renderer_classes = (renderers.JSONRenderer,) + permission_classes = (IsOwnerOrAdmin, ) + serializer_class = RefreshTokenSerializer + queryset = RefreshToken.objects.all() + lookup_field = 'key' + + def get_queryset(self): + queryset = super(RefreshTokenViewSet, self).get_queryset() + if self.request.user.is_superuser or self.request.user.is_staff: + return queryset + else: + return queryset.filter(user=self.request.user) diff --git a/rest_framework_jwt/views.py b/rest_framework_jwt/views.py index 650ef1cd..595696a7 100644 --- a/rest_framework_jwt/views.py +++ b/rest_framework_jwt/views.py @@ -3,7 +3,6 @@ from rest_framework import parsers from rest_framework import renderers from rest_framework.response import Response - from rest_framework_jwt.settings import api_settings from .serializers import ( @@ -63,7 +62,6 @@ def post(self, request): user = serializer.object.get('user') or request.user token = serializer.object.get('token') response_data = jwt_response_payload_handler(token, user, request) - return Response(response_data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/runtests.py b/runtests.py index 45dcbf4e..627b1613 100755 --- a/runtests.py +++ b/runtests.py @@ -20,24 +20,29 @@ sys.path.append(os.path.dirname(__file__)) + def exit_on_failure(ret, message=None): if ret: sys.exit(ret) + def flake8_main(args): print('Running flake8 code linting') ret = subprocess.call(['flake8'] + args) print('flake8 failed' if ret else 'flake8 passed') return ret + def split_class_and_function(string): class_string, function_string = string.split('.', 1) return "%s and %s" % (class_string, function_string) + def is_function(string): # `True` if it looks like a test function is included in the string. return string.startswith('test_') or '.test_' in string + def is_class(string): # `True` if first character is uppercase - assume it's a class name. return string[0] == string[0].upper() diff --git a/tests/conftest.py b/tests/conftest.py index fe079bc7..eb7ee83d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ def pytest_configure(): 'NAME': ':memory:' } }, + SOUTH_TESTS_MIGRATE=False, SITE_ID=1, SECRET_KEY='not very secret in tests', USE_I18N=True, @@ -36,10 +37,12 @@ def pytest_configure(): 'django.contrib.staticfiles', 'tests', + 'rest_framework_jwt.refreshtoken', ), PASSWORD_HASHERS=( 'django.contrib.auth.hashers.MD5PasswordHasher', ), + SOUTH_DATABASE_ADAPTERS={'default': 'south.db.sqlite3'} ) try: diff --git a/tests/test_views.py b/tests/test_views.py index 820280bf..ecc199ad 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -3,6 +3,7 @@ from django import get_version from django.test import TestCase +from django.core.urlresolvers import reverse from django.test.utils import override_settings from django.utils import unittest from django.conf.urls import patterns @@ -11,10 +12,11 @@ from freezegun import freeze_time from rest_framework import status -from rest_framework.test import APIClient +from rest_framework.test import APIClient, APITestCase from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings, DEFAULTS +from rest_framework_jwt.refreshtoken.models import RefreshToken from . import utils as test_utils @@ -50,10 +52,9 @@ def setUp(self): class TestCustomResponsePayload(BaseTestCase): - def setUp(self): - api_settings.JWT_RESPONSE_PAYLOAD_HANDLER = test_utils\ - .jwt_response_payload_handler + api_settings.JWT_RESPONSE_PAYLOAD_HANDLER = test_utils.\ + jwt_response_payload_handler return super(TestCustomResponsePayload, self).setUp() def test_jwt_login_custom_response_json(self): @@ -361,3 +362,129 @@ def test_refresh_jwt_after_refresh_expiration(self): def tearDown(self): # Restore original settings api_settings.JWT_ALLOW_REFRESH = DEFAULTS['JWT_ALLOW_REFRESH'] + + +class RefreshTokenTestCase(APITestCase): + urls = 'rest_framework_jwt.refreshtoken.routers' + + def setUp(self): + self.email = 'jpueblo@example.com' + self.username = 'jpueblo' + self.password = 'password' + self.user = User.objects.create_user( + self.username, self.email, self.password) + self.token = RefreshToken.objects.create(user=self.user, app='test-app') + email1 = 'jonny@example.com' + username1 = 'jonnytestpants' + password1 = 'password' + self.user1 = User.objects.create_user(username1, email1, password1) + self.token1 = RefreshToken.objects.create(user=self.user1, app='another-app') + + self.list_url = reverse('refreshtoken-list') + self.detail_url = reverse( + 'refreshtoken-detail', + kwargs={'key': self.token.key} + ) + self.detail_url1 = reverse( + 'refreshtoken-detail', + kwargs={'key': self.token1.key} + ) + self.delgate_url = reverse('delgate-tokens') + + def test_requires_auth(self): + response = self.client.get(self.list_url) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN, + (response.status_code, response.content) + ) + + response = self.client.get(self.detail_url) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN, + (response.status_code, response.content) + ) + + response = self.client.delete(self.detail_url) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN, + (response.status_code, response.content) + ) + + response = self.client.post(self.list_url) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN, + (response.status_code, response.content) + ) + + def test_get_refresh_token_list(self): + self.client.force_authenticate(self.user) + response = self.client.get(self.list_url) + self.assertEqual(len(response.data), 1) + resp0 = response.data[0] + self.assertEqual(self.token.key, resp0['key']) + + self.client.force_authenticate(self.user1) + response = self.client.get(self.list_url) + self.assertEqual(len(response.data), 1) + resp0 = response.data[0] + self.assertEqual(self.token1.key, resp0['key']) + + self.assertEqual(RefreshToken.objects.count(), 2) + + def test_get_refresth_token_detail(self): + self.client.force_authenticate(self.user) + response = self.client.get(self.detail_url) + self.assertEqual( + response.status_code, + status.HTTP_200_OK, + (response.status_code, response.content) + ) + response = self.client.get(self.detail_url1) + self.assertEqual( + response.status_code, + status.HTTP_404_NOT_FOUND, + (response.status_code, response.content) + ) + + def test_delete_refresth_token(self): + self.client.force_authenticate(self.user) + response = self.client.delete(self.detail_url) + self.assertEqual( + response.status_code, + status.HTTP_204_NO_CONTENT, + (response.status_code, response.content) + ) + response = self.client.delete(self.detail_url1) + self.assertEqual( + response.status_code, + status.HTTP_404_NOT_FOUND, + (response.status_code, response.content) + ) + + def test_create_refresth_token(self): + self.client.force_authenticate(self.user) + data = { + 'app': 'gandolf' + } + response = self.client.post(self.list_url, data, format='json') + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED, + (response.status_code, response.content) + ) + self.assertEqual(response.data['user'], self.user.pk) + self.assertEqual(response.data['app'], data['app']) + + def test_delegate_jwt(self): + headers = {'HTTP_AUTHORIZATION': 'RefreshToken {}'.format(self.token1.key)} + response = self.client.post(self.delgate_url, format='json', **headers) + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED, + (response.status_code, response.content) + ) + self.assertIn('token', response.data) From ca84a6f0b80e162b96078e30c69f6371ec8afe4b Mon Sep 17 00:00:00 2001 From: Nick Lang Date: Thu, 2 Apr 2015 13:08:22 -0600 Subject: [PATCH 2/5] fixing typos, removing some viewset overrides --- rest_framework_jwt/refreshtoken/routers.py | 4 ++-- rest_framework_jwt/refreshtoken/views.py | 8 +------- tests/test_views.py | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/rest_framework_jwt/refreshtoken/routers.py b/rest_framework_jwt/refreshtoken/routers.py index 0a912ae9..2318904a 100644 --- a/rest_framework_jwt/refreshtoken/routers.py +++ b/rest_framework_jwt/refreshtoken/routers.py @@ -1,11 +1,11 @@ from rest_framework import routers from django.conf.urls import patterns, url -from .views import RefreshTokenViewSet, DelagateJSONWebToken +from .views import RefreshTokenViewSet, DelegateJSONWebToken router = routers.SimpleRouter() router.register(r'refresh-token', RefreshTokenViewSet) urlpatterns = router.urls + patterns('', # NOQA - url(r'delgate/$', DelagateJSONWebToken.as_view(), name='delgate-tokens'), + url(r'^delegate/$', DelegateJSONWebToken.as_view(), name='delegate-tokens'), ) diff --git a/rest_framework_jwt/refreshtoken/views.py b/rest_framework_jwt/refreshtoken/views.py index 54b21226..582730ce 100644 --- a/rest_framework_jwt/refreshtoken/views.py +++ b/rest_framework_jwt/refreshtoken/views.py @@ -5,8 +5,6 @@ from rest_framework import viewsets from rest_framework.response import Response from rest_framework import status -from rest_framework import parsers -from rest_framework import renderers from rest_framework_jwt.settings import api_settings from rest_framework_jwt.views import JSONWebTokenAPIView @@ -20,7 +18,7 @@ jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER -class DelagateJSONWebToken(JSONWebTokenAPIView): +class DelegateJSONWebToken(JSONWebTokenAPIView): """ API View that checks the veracity of a refresh token, returning a JWT if it is valid. @@ -48,10 +46,6 @@ class RefreshTokenViewSet(mixins.RetrieveModelMixin, https://auth0.com/docs/refresh-token """ - throttle_classes = () - authentication_classes = () - parser_classes = (parsers.FormParser, parsers.JSONParser,) - renderer_classes = (renderers.JSONRenderer,) permission_classes = (IsOwnerOrAdmin, ) serializer_class = RefreshTokenSerializer queryset = RefreshToken.objects.all() diff --git a/tests/test_views.py b/tests/test_views.py index ecc199ad..0f6b5c2c 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -389,7 +389,7 @@ def setUp(self): 'refreshtoken-detail', kwargs={'key': self.token1.key} ) - self.delgate_url = reverse('delgate-tokens') + self.delegate_url = reverse('delegate-tokens') def test_requires_auth(self): response = self.client.get(self.list_url) @@ -481,7 +481,7 @@ def test_create_refresth_token(self): def test_delegate_jwt(self): headers = {'HTTP_AUTHORIZATION': 'RefreshToken {}'.format(self.token1.key)} - response = self.client.post(self.delgate_url, format='json', **headers) + response = self.client.post(self.delegate_url, format='json', **headers) self.assertEqual( response.status_code, status.HTTP_201_CREATED, From b0b75702dff4b14c78ff63e6d1c1f4791139ad4f Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Fri, 8 May 2015 15:31:15 +0200 Subject: [PATCH 3/5] Make refreshtoken a separate app. --- docs/index.md | 53 +++++++++++++++++++ rest_framework_jwt/authentication.py | 46 ++-------------- .../refreshtoken/authentication.py | 46 ++++++++++++++++ .../refreshtoken/migrations/0001_initial.py | 24 +++++++++ .../refreshtoken/migrations/__init__.py | 0 .../refreshtoken/serializers.py | 52 +++++++++++++----- rest_framework_jwt/refreshtoken/views.py | 10 +++- 7 files changed, 173 insertions(+), 58 deletions(-) create mode 100644 rest_framework_jwt/refreshtoken/authentication.py create mode 100644 rest_framework_jwt/refreshtoken/migrations/0001_initial.py create mode 100644 rest_framework_jwt/refreshtoken/migrations/__init__.py diff --git a/docs/index.md b/docs/index.md index 323b430a..40f5f10d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -101,6 +101,59 @@ Refresh with tokens can be repeated (token1 -> token2 -> token3), but this chain A typical use case might be a web app where you'd like to keep the user "logged in" the site without having to re-enter their password, or get kicked out by surprise before their token expired. Imagine they had a 1-hour token and are just at the last minute while they're still doing something. With mobile you could perhaps store the username/password to get a new token, but this is not a great idea in a browser. Each time the user loads the page, you can check if there is an existing non-expired token and if it's close to being expired, refresh it to extend their session. In other words, if a user is actively using your site, they can keep their "session" alive. +## Long Running Refresh Token + +This allows for a client to request refresh tokens. These refresh tokens do not expire. +They can be revoked (deleted). When a JWT has expired, it's possible to send a request +with the refresh token in the header, and get back a new JWT. + +Declare the app +```python +INSTALLED_APPS = [ + ..., + 'rest_framework_jwt.refreshtoken', +] + +``` + +Run migrations + +```bash +$ python manage.py migrate refreshtoken +``` + +Configure your urls to add new endpoint + +```python +from rest_framework_jwt.refreshtoken.routers import urlpatterns as jwt_urlpatterns + +urlpatterns = [ + url(...), +] + jwt_urlpatterns + +``` + +You can include this refresh token in your JWT_RESPONSE_PAYLOAD_HANDLER + +```python + +def jwt_response_payload_handler(token, user=None, request=None): + return { + 'token': token, + 'user': UserSerializer(user).data, + 'refresh_token': user.refresh_tokens.first().key, + } + +``` + +Then your user can ask a new JWT token as long as the refresh_token exists. + +```bash +$ curl -X POST -H "Authorization: RefreshToken " http://localhost:8000/delegate/ +'{"token": "your_jwt_token_..."}' + +``` + ## Verify Token In some microservice architectures, authentication is handled by a single service. Other services delegate the responsibility of confirming that a user is logged in to this authentication service. This usually means that a service will pass a JWT received from the user to the authentication service, and wait for a confirmation that the JWT is valid before returning protected resources to the user. diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index df2b29be..64d5f9a5 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -2,13 +2,12 @@ from django.utils.encoding import smart_text from django.utils.translation import ugettext as _ from rest_framework import exceptions -from rest_framework.authentication import ( - BaseAuthentication, get_authorization_header, TokenAuthentication -) +from rest_framework.authentication import (BaseAuthentication, + get_authorization_header) from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings -from rest_framework_jwt.refreshtoken.models import RefreshToken + jwt_decode_handler = api_settings.JWT_DECODE_HANDLER jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER @@ -98,42 +97,3 @@ def authenticate_header(self, request): authentication scheme should return `403 Permission Denied` responses. """ return 'JWT realm="{0}"'.format(self.www_authenticate_realm) - - -class RefreshTokenAuthentication(TokenAuthentication): - """ - Subclassed from rest_framework.authentication.TokenAuthentication - - Auth header: - Authorization: RefreshToken 401f7ac837da42b97f613d789819ff93537bee6a - """ - model = RefreshToken - - def authenticate(self, request): - auth = get_authorization_header(request).split() - - if not auth or auth[0].lower() != b'refreshtoken': - return None - - if len(auth) == 1: - msg = _('Invalid token header. No credentials provided.') - raise exceptions.AuthenticationFailed(msg) - elif len(auth) > 2: - msg = _('Invalid token header. Token string should not contain spaces.') - raise exceptions.AuthenticationFailed(msg) - - return self.authenticate_credentials(auth[1]) - - def authenticate_credentials(self, key): - try: - token = self.model.objects.select_related('user').get(key=key) - except self.model.DoesNotExist: - raise exceptions.AuthenticationFailed(_('Invalid token.')) - - if not token.user.is_active: - raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) - - return (token.user, token) - - def authenticate_header(self, request): - return 'RefreshToken' diff --git a/rest_framework_jwt/refreshtoken/authentication.py b/rest_framework_jwt/refreshtoken/authentication.py new file mode 100644 index 00000000..bfe563fe --- /dev/null +++ b/rest_framework_jwt/refreshtoken/authentication.py @@ -0,0 +1,46 @@ +from django.utils.translation import ugettext as _ +from rest_framework.authentication import ( + TokenAuthentication, + get_authorization_header, +) +from rest_framework import exceptions +from rest_framework_jwt.refreshtoken.models import RefreshToken + + +class RefreshTokenAuthentication(TokenAuthentication): + """ + Subclassed from rest_framework.authentication.TokenAuthentication + + Auth header: + Authorization: RefreshToken 401f7ac837da42b97f613d789819ff93537bee6a + """ + model = RefreshToken + + def authenticate(self, request): + auth = get_authorization_header(request).split() + + if not auth or auth[0].lower() != b'refreshtoken': + return None + + if len(auth) == 1: + msg = _('Invalid token header. No credentials provided.') + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = _('Invalid token header. Token string should not contain spaces.') + raise exceptions.AuthenticationFailed(msg) + + return self.authenticate_credentials(auth[1]) + + def authenticate_credentials(self, key): + try: + token = self.model.objects.select_related('user').get(key=key) + except self.model.DoesNotExist: + raise exceptions.AuthenticationFailed(_('Invalid token.')) + + if not token.user.is_active: + raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + + return (token.user, token) + + def authenticate_header(self, request): + return 'RefreshToken' diff --git a/rest_framework_jwt/refreshtoken/migrations/0001_initial.py b/rest_framework_jwt/refreshtoken/migrations/0001_initial.py new file mode 100644 index 00000000..6f849b3e --- /dev/null +++ b/rest_framework_jwt/refreshtoken/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='RefreshToken', + fields=[ + ('key', models.CharField(max_length=40, primary_key=True, serialize=False)), + ('app', models.CharField(unique=True, max_length=255)), + ('created', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(related_name='refresh_tokens', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/rest_framework_jwt/refreshtoken/migrations/__init__.py b/rest_framework_jwt/refreshtoken/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rest_framework_jwt/refreshtoken/serializers.py b/rest_framework_jwt/refreshtoken/serializers.py index d53be3a7..99e5360c 100644 --- a/rest_framework_jwt/refreshtoken/serializers.py +++ b/rest_framework_jwt/refreshtoken/serializers.py @@ -1,17 +1,43 @@ -from .models import RefreshToken +from calendar import timegm +from datetime import datetime + from rest_framework import serializers +from rest_framework_jwt.settings import api_settings +from rest_framework_jwt.serializers import ( + jwt_encode_handler, + jwt_payload_handler, +) + +from .models import RefreshToken class RefreshTokenSerializer(serializers.ModelSerializer): - """ - Serializer for refresh tokens (Not RefreshJWTToken) - """ - - class Meta: - model = RefreshToken - fields = ('key', 'user', 'created', 'app') - read_only_fields = ('key', 'user', 'created') - - def validate(self, attrs): - attrs['user'] = self.context['request'].user - return attrs + """ + Serializer for refresh tokens (Not RefreshJWTToken) + """ + + class Meta: + model = RefreshToken + fields = ('key', 'user', 'created', 'app') + read_only_fields = ('key', 'user', 'created') + + def validate(self, attrs): + attrs['user'] = self.context['request'].user + return attrs + + +class DelegateJSONWebTokenSerializer(serializers.Serializer): + def validate(self, attrs): + user = self.context['request'].user + payload = jwt_payload_handler(user) + + # Include original issued at time for a brand new token, + # to allow token refresh + if api_settings.JWT_ALLOW_REFRESH: + payload['orig_iat'] = timegm( + datetime.utcnow().utctimetuple() + ) + + attrs['token'] = jwt_encode_handler(payload) + attrs['user'] = user + return attrs diff --git a/rest_framework_jwt/refreshtoken/views.py b/rest_framework_jwt/refreshtoken/views.py index 582730ce..af3ffc98 100644 --- a/rest_framework_jwt/refreshtoken/views.py +++ b/rest_framework_jwt/refreshtoken/views.py @@ -8,11 +8,16 @@ from rest_framework_jwt.settings import api_settings from rest_framework_jwt.views import JSONWebTokenAPIView -from rest_framework_jwt.authentication import RefreshTokenAuthentication +from rest_framework_jwt.refreshtoken.authentication import ( + RefreshTokenAuthentication, +) from .permissions import IsOwnerOrAdmin from .models import RefreshToken -from .serializers import RefreshTokenSerializer +from .serializers import ( + DelegateJSONWebTokenSerializer, + RefreshTokenSerializer, +) jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER @@ -24,6 +29,7 @@ class DelegateJSONWebToken(JSONWebTokenAPIView): is valid. """ authentication_classes = (RefreshTokenAuthentication, ) + serializer_class = DelegateJSONWebTokenSerializer def post(self, request): user = request.user From 80e5b7c8a01320b085b337bd19d599219999b650 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Fri, 15 May 2015 11:49:16 +0200 Subject: [PATCH 4/5] Remove unicity constraint on app field and choose constraint for user and app together --- .../migrations/0002_auto_20150515_0948.py | 23 +++++++++++++++++++ rest_framework_jwt/refreshtoken/models.py | 5 +++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 rest_framework_jwt/refreshtoken/migrations/0002_auto_20150515_0948.py diff --git a/rest_framework_jwt/refreshtoken/migrations/0002_auto_20150515_0948.py b/rest_framework_jwt/refreshtoken/migrations/0002_auto_20150515_0948.py new file mode 100644 index 00000000..6a39032c --- /dev/null +++ b/rest_framework_jwt/refreshtoken/migrations/0002_auto_20150515_0948.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('refreshtoken', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='refreshtoken', + name='app', + field=models.CharField(max_length=255), + ), + migrations.AlterUniqueTogether( + name='refreshtoken', + unique_together=set([('user', 'app')]), + ), + ] diff --git a/rest_framework_jwt/refreshtoken/models.py b/rest_framework_jwt/refreshtoken/models.py index 3070554c..e2303d82 100644 --- a/rest_framework_jwt/refreshtoken/models.py +++ b/rest_framework_jwt/refreshtoken/models.py @@ -25,9 +25,12 @@ class RefreshToken(models.Model): """ key = models.CharField(max_length=40, primary_key=True) user = models.ForeignKey(AUTH_USER_MODEL, related_name='refresh_tokens') - app = models.CharField(max_length=255, unique=True) + app = models.CharField(max_length=255) created = models.DateTimeField(auto_now_add=True) + class Meta: + unique_together = ('user', 'app') + def save(self, *args, **kwargs): if not self.key: self.key = self.generate_key() From d0cedd1e03e2e8f84928a74a8eb842c5bf8e6e73 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Tue, 19 May 2015 10:47:40 +0200 Subject: [PATCH 5/5] Be compliant with https://auth0.com/docs/refresh-token The delegation endpoint is a `POST`, with the following body. ```json { "client_id": "YOUR_CLIENT_ID", "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "refresh_token": "your_refresh_token", "api_type": "app" } ``` --- docs/index.md | 2 +- rest_framework_jwt/compat.py | 10 + .../refreshtoken/authentication.py | 46 ----- .../refreshtoken/serializers.py | 58 +++--- rest_framework_jwt/refreshtoken/views.py | 25 ++- tests/test_long_refresh_token_views.py | 186 ++++++++++++++++++ tests/test_views.py | 130 +----------- 7 files changed, 250 insertions(+), 207 deletions(-) delete mode 100644 rest_framework_jwt/refreshtoken/authentication.py create mode 100644 tests/test_long_refresh_token_views.py diff --git a/docs/index.md b/docs/index.md index 40f5f10d..09f45220 100644 --- a/docs/index.md +++ b/docs/index.md @@ -149,7 +149,7 @@ def jwt_response_payload_handler(token, user=None, request=None): Then your user can ask a new JWT token as long as the refresh_token exists. ```bash -$ curl -X POST -H "Authorization: RefreshToken " http://localhost:8000/delegate/ +$ curl -X POST -d '{"client_id": "app", "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "refresh_token": , "api_type": "app"}' http://localhost:8000/delegate/ '{"token": "your_jwt_token_..."}' ``` diff --git a/rest_framework_jwt/compat.py b/rest_framework_jwt/compat.py index 01313aae..4f9aad39 100644 --- a/rest_framework_jwt/compat.py +++ b/rest_framework_jwt/compat.py @@ -1,4 +1,5 @@ import rest_framework +import rest_framework.exceptions from distutils.version import StrictVersion @@ -9,3 +10,12 @@ class Serializer(rest_framework.serializers.Serializer): @property def object(self): return self.validated_data + +try: + from rest_framework.serializers import CurrentUserDefault +except ImportError: + # DRF 2.4 + class CurrentUserDefault(object): + + def __call__(self): + pass diff --git a/rest_framework_jwt/refreshtoken/authentication.py b/rest_framework_jwt/refreshtoken/authentication.py deleted file mode 100644 index bfe563fe..00000000 --- a/rest_framework_jwt/refreshtoken/authentication.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.utils.translation import ugettext as _ -from rest_framework.authentication import ( - TokenAuthentication, - get_authorization_header, -) -from rest_framework import exceptions -from rest_framework_jwt.refreshtoken.models import RefreshToken - - -class RefreshTokenAuthentication(TokenAuthentication): - """ - Subclassed from rest_framework.authentication.TokenAuthentication - - Auth header: - Authorization: RefreshToken 401f7ac837da42b97f613d789819ff93537bee6a - """ - model = RefreshToken - - def authenticate(self, request): - auth = get_authorization_header(request).split() - - if not auth or auth[0].lower() != b'refreshtoken': - return None - - if len(auth) == 1: - msg = _('Invalid token header. No credentials provided.') - raise exceptions.AuthenticationFailed(msg) - elif len(auth) > 2: - msg = _('Invalid token header. Token string should not contain spaces.') - raise exceptions.AuthenticationFailed(msg) - - return self.authenticate_credentials(auth[1]) - - def authenticate_credentials(self, key): - try: - token = self.model.objects.select_related('user').get(key=key) - except self.model.DoesNotExist: - raise exceptions.AuthenticationFailed(_('Invalid token.')) - - if not token.user.is_active: - raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) - - return (token.user, token) - - def authenticate_header(self, request): - return 'RefreshToken' diff --git a/rest_framework_jwt/refreshtoken/serializers.py b/rest_framework_jwt/refreshtoken/serializers.py index 99e5360c..2c1c6cd7 100644 --- a/rest_framework_jwt/refreshtoken/serializers.py +++ b/rest_framework_jwt/refreshtoken/serializers.py @@ -1,12 +1,7 @@ -from calendar import timegm -from datetime import datetime - +from django.utils.translation import ugettext as _ +from rest_framework import exceptions from rest_framework import serializers -from rest_framework_jwt.settings import api_settings -from rest_framework_jwt.serializers import ( - jwt_encode_handler, - jwt_payload_handler, -) +from rest_framework_jwt.compat import CurrentUserDefault, Serializer from .models import RefreshToken @@ -16,28 +11,45 @@ class RefreshTokenSerializer(serializers.ModelSerializer): Serializer for refresh tokens (Not RefreshJWTToken) """ + user = serializers.PrimaryKeyRelatedField( + required=False, + read_only=True, + default=CurrentUserDefault()) + class Meta: model = RefreshToken fields = ('key', 'user', 'created', 'app') - read_only_fields = ('key', 'user', 'created') + read_only_fields = ('key', 'created') def validate(self, attrs): - attrs['user'] = self.context['request'].user + """ + only for DRF < 3.0 support. + Otherwise CurrentUserDefault() is doing the job of obtaining user + from current request. + """ + if 'user' not in attrs: + attrs['user'] = self.context['request'].user return attrs -class DelegateJSONWebTokenSerializer(serializers.Serializer): +class DelegateJSONWebTokenSerializer(Serializer): + client_id = serializers.CharField() + grant_type = serializers.CharField( + default='urn:ietf:params:oauth:grant-type:jwt-bearer', + required=False, + ) + refresh_token = serializers.CharField() + api_type = serializers.CharField( + default='app', + required=False, + ) + def validate(self, attrs): - user = self.context['request'].user - payload = jwt_payload_handler(user) - - # Include original issued at time for a brand new token, - # to allow token refresh - if api_settings.JWT_ALLOW_REFRESH: - payload['orig_iat'] = timegm( - datetime.utcnow().utctimetuple() - ) - - attrs['token'] = jwt_encode_handler(payload) - attrs['user'] = user + refresh_token = attrs['refresh_token'] + try: + token = RefreshToken.objects.select_related('user').get( + key=refresh_token) + except RefreshToken.DoesNotExist: + raise exceptions.AuthenticationFailed(_('Invalid token.')) + attrs['user'] = token.user return attrs diff --git a/rest_framework_jwt/refreshtoken/views.py b/rest_framework_jwt/refreshtoken/views.py index af3ffc98..d60f5b63 100644 --- a/rest_framework_jwt/refreshtoken/views.py +++ b/rest_framework_jwt/refreshtoken/views.py @@ -1,16 +1,15 @@ from calendar import timegm from datetime import datetime +from django.utils.translation import ugettext as _ +from rest_framework import exceptions +from rest_framework import generics from rest_framework import mixins from rest_framework import viewsets from rest_framework.response import Response from rest_framework import status from rest_framework_jwt.settings import api_settings -from rest_framework_jwt.views import JSONWebTokenAPIView -from rest_framework_jwt.refreshtoken.authentication import ( - RefreshTokenAuthentication, -) from .permissions import IsOwnerOrAdmin from .models import RefreshToken @@ -23,16 +22,26 @@ jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER -class DelegateJSONWebToken(JSONWebTokenAPIView): +class DelegateJSONWebToken(generics.CreateAPIView): """ API View that checks the veracity of a refresh token, returning a JWT if it is valid. """ - authentication_classes = (RefreshTokenAuthentication, ) serializer_class = DelegateJSONWebTokenSerializer - def post(self, request): - user = request.user + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.DATA) + # pass raise_exception=True argument once we drop support + # of DRF < 3.0 + serializer.is_valid() + if serializer.errors: + return Response(serializer.errors, + status=status.HTTP_400_BAD_REQUEST) + user = serializer.object['user'] + if not user.is_active: + raise exceptions.AuthenticationFailed( + _('User inactive or deleted.')) + payload = jwt_payload_handler(user) if api_settings.JWT_ALLOW_REFRESH: payload['orig_iat'] = timegm(datetime.utcnow().utctimetuple()) diff --git a/tests/test_long_refresh_token_views.py b/tests/test_long_refresh_token_views.py new file mode 100644 index 00000000..cffdc0b8 --- /dev/null +++ b/tests/test_long_refresh_token_views.py @@ -0,0 +1,186 @@ +from django.conf.urls import patterns, url +from django.contrib.auth import get_user_model +from django.core.urlresolvers import reverse +from rest_framework import routers +from rest_framework import status +from rest_framework.test import APITestCase + +from rest_framework_jwt.authentication import JSONWebTokenAuthentication +from rest_framework_jwt import utils +from rest_framework_jwt.refreshtoken.models import RefreshToken +from rest_framework_jwt.refreshtoken.views import ( + RefreshTokenViewSet, + DelegateJSONWebToken, +) + + +User = get_user_model() + +RefreshTokenViewSet.authentication_classes = [JSONWebTokenAuthentication] + +router = routers.SimpleRouter() +router.register(r'refresh-token', RefreshTokenViewSet) +urlpatterns = router.urls + patterns( + '', + url(r'^delegate/$', + DelegateJSONWebToken.as_view( + authentication_classes=[JSONWebTokenAuthentication]), + name='delegate-tokens'), +) + + +class RefreshTokenTestCase(APITestCase): + urls = __name__ + + def setUp(self): + self.email = 'jpueblo@example.com' + self.username = 'jpueblo' + self.password = 'password' + self.user = User.objects.create_user( + self.username, self.email, self.password) + self.token = RefreshToken.objects.create(user=self.user, app='test-app') + email1 = 'jonny@example.com' + username1 = 'jonnytestpants' + password1 = 'password' + self.user1 = User.objects.create_user(username1, email1, password1) + self.token1 = RefreshToken.objects.create(user=self.user1, + app='another-app') + + self.list_url = reverse('refreshtoken-list') + self.detail_url = reverse( + 'refreshtoken-detail', + kwargs={'key': self.token.key} + ) + self.detail_url1 = reverse( + 'refreshtoken-detail', + kwargs={'key': self.token1.key} + ) + self.delegate_url = reverse('delegate-tokens') + + def test_requires_auth(self): + response = self.client.get(self.list_url) + self.assertEqual( + response.status_code, + status.HTTP_401_UNAUTHORIZED, + (response.status_code, response.content) + ) + + response = self.client.get(self.detail_url) + self.assertEqual( + response.status_code, + status.HTTP_401_UNAUTHORIZED, + (response.status_code, response.content) + ) + + response = self.client.delete(self.detail_url) + self.assertEqual( + response.status_code, + status.HTTP_401_UNAUTHORIZED, + (response.status_code, response.content) + ) + + response = self.client.post(self.list_url) + self.assertEqual( + response.status_code, + status.HTTP_401_UNAUTHORIZED, + (response.status_code, response.content) + ) + + def test_get_refresh_token_list(self): + self.client.credentials( + HTTP_AUTHORIZATION='JWT ' + utils.jwt_encode_handler( + utils.jwt_payload_handler(self.user))) + response = self.client.get(self.list_url) + self.assertEqual(len(response.data), 1) + resp0 = response.data[0] + self.assertEqual(self.token.key, resp0['key']) + + self.client.force_authenticate(self.user1) + response = self.client.get(self.list_url) + self.assertEqual(len(response.data), 1) + resp0 = response.data[0] + self.assertEqual(self.token1.key, resp0['key']) + + self.assertEqual(RefreshToken.objects.count(), 2) + + def test_get_refresth_token_detail(self): + self.client.credentials( + HTTP_AUTHORIZATION='JWT ' + utils.jwt_encode_handler( + utils.jwt_payload_handler(self.user))) + response = self.client.get(self.detail_url) + self.assertEqual( + response.status_code, + status.HTTP_200_OK, + (response.status_code, response.content) + ) + response = self.client.get(self.detail_url1) + self.assertEqual( + response.status_code, + status.HTTP_404_NOT_FOUND, + (response.status_code, response.content) + ) + + def test_delete_refresth_token(self): + self.client.credentials( + HTTP_AUTHORIZATION='JWT ' + utils.jwt_encode_handler( + utils.jwt_payload_handler(self.user))) + response = self.client.delete(self.detail_url) + self.assertEqual( + response.status_code, + status.HTTP_204_NO_CONTENT, + (response.status_code, response.content) + ) + response = self.client.delete(self.detail_url1) + self.assertEqual( + response.status_code, + status.HTTP_404_NOT_FOUND, + (response.status_code, response.content) + ) + + def test_create_refresth_token(self): + data = { + 'app': 'gandolf' + } + self.client.credentials( + HTTP_AUTHORIZATION='JWT ' + utils.jwt_encode_handler( + utils.jwt_payload_handler(self.user))) + response = self.client.post(self.list_url, data, format='json') + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED, + (response.status_code, response.content) + ) + self.assertEqual(response.data['user'], self.user.pk) + self.assertEqual(response.data['app'], data['app']) + + def test_delegate_jwt(self): + data = { + 'client_id': 'gandolf', + 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'refresh_token': self.token1.key, + 'api_type': 'app', + } + response = self.client.post(self.delegate_url, + data=data, + format='json') + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED, + (response.status_code, response.content) + ) + self.assertIn('token', response.data) + + def test_invalid_body_delegate_jwt(self): + # client_id is missing + data = { + 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'refresh_token': self.token1.key, + 'api_type': 'app', + } + response = self.client.post(self.delegate_url, data=data, + format='json') + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + (response.status_code, response.content) + ) diff --git a/tests/test_views.py b/tests/test_views.py index 0f6b5c2c..f36c3d80 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -3,7 +3,6 @@ from django import get_version from django.test import TestCase -from django.core.urlresolvers import reverse from django.test.utils import override_settings from django.utils import unittest from django.conf.urls import patterns @@ -12,11 +11,10 @@ from freezegun import freeze_time from rest_framework import status -from rest_framework.test import APIClient, APITestCase +from rest_framework.test import APIClient from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings, DEFAULTS -from rest_framework_jwt.refreshtoken.models import RefreshToken from . import utils as test_utils @@ -362,129 +360,3 @@ def test_refresh_jwt_after_refresh_expiration(self): def tearDown(self): # Restore original settings api_settings.JWT_ALLOW_REFRESH = DEFAULTS['JWT_ALLOW_REFRESH'] - - -class RefreshTokenTestCase(APITestCase): - urls = 'rest_framework_jwt.refreshtoken.routers' - - def setUp(self): - self.email = 'jpueblo@example.com' - self.username = 'jpueblo' - self.password = 'password' - self.user = User.objects.create_user( - self.username, self.email, self.password) - self.token = RefreshToken.objects.create(user=self.user, app='test-app') - email1 = 'jonny@example.com' - username1 = 'jonnytestpants' - password1 = 'password' - self.user1 = User.objects.create_user(username1, email1, password1) - self.token1 = RefreshToken.objects.create(user=self.user1, app='another-app') - - self.list_url = reverse('refreshtoken-list') - self.detail_url = reverse( - 'refreshtoken-detail', - kwargs={'key': self.token.key} - ) - self.detail_url1 = reverse( - 'refreshtoken-detail', - kwargs={'key': self.token1.key} - ) - self.delegate_url = reverse('delegate-tokens') - - def test_requires_auth(self): - response = self.client.get(self.list_url) - self.assertEqual( - response.status_code, - status.HTTP_403_FORBIDDEN, - (response.status_code, response.content) - ) - - response = self.client.get(self.detail_url) - self.assertEqual( - response.status_code, - status.HTTP_403_FORBIDDEN, - (response.status_code, response.content) - ) - - response = self.client.delete(self.detail_url) - self.assertEqual( - response.status_code, - status.HTTP_403_FORBIDDEN, - (response.status_code, response.content) - ) - - response = self.client.post(self.list_url) - self.assertEqual( - response.status_code, - status.HTTP_403_FORBIDDEN, - (response.status_code, response.content) - ) - - def test_get_refresh_token_list(self): - self.client.force_authenticate(self.user) - response = self.client.get(self.list_url) - self.assertEqual(len(response.data), 1) - resp0 = response.data[0] - self.assertEqual(self.token.key, resp0['key']) - - self.client.force_authenticate(self.user1) - response = self.client.get(self.list_url) - self.assertEqual(len(response.data), 1) - resp0 = response.data[0] - self.assertEqual(self.token1.key, resp0['key']) - - self.assertEqual(RefreshToken.objects.count(), 2) - - def test_get_refresth_token_detail(self): - self.client.force_authenticate(self.user) - response = self.client.get(self.detail_url) - self.assertEqual( - response.status_code, - status.HTTP_200_OK, - (response.status_code, response.content) - ) - response = self.client.get(self.detail_url1) - self.assertEqual( - response.status_code, - status.HTTP_404_NOT_FOUND, - (response.status_code, response.content) - ) - - def test_delete_refresth_token(self): - self.client.force_authenticate(self.user) - response = self.client.delete(self.detail_url) - self.assertEqual( - response.status_code, - status.HTTP_204_NO_CONTENT, - (response.status_code, response.content) - ) - response = self.client.delete(self.detail_url1) - self.assertEqual( - response.status_code, - status.HTTP_404_NOT_FOUND, - (response.status_code, response.content) - ) - - def test_create_refresth_token(self): - self.client.force_authenticate(self.user) - data = { - 'app': 'gandolf' - } - response = self.client.post(self.list_url, data, format='json') - self.assertEqual( - response.status_code, - status.HTTP_201_CREATED, - (response.status_code, response.content) - ) - self.assertEqual(response.data['user'], self.user.pk) - self.assertEqual(response.data['app'], data['app']) - - def test_delegate_jwt(self): - headers = {'HTTP_AUTHORIZATION': 'RefreshToken {}'.format(self.token1.key)} - response = self.client.post(self.delegate_url, format='json', **headers) - self.assertEqual( - response.status_code, - status.HTTP_201_CREATED, - (response.status_code, response.content) - ) - self.assertIn('token', response.data)