From b175f39e99318bf6a50d459ea89170c1749893b5 Mon Sep 17 00:00:00 2001 From: AM Date: Sun, 22 Mar 2015 19:56:56 +0100 Subject: [PATCH 01/93] Added support for jti claim --- requirements.txt | 1 + rest_framework_jwt/serializers.py | 31 +++++++++++++++++++++++++++ rest_framework_jwt/settings.py | 2 ++ rest_framework_jwt/utils.py | 35 ++++++++++++++++++++++++++++--- rest_framework_jwt/views.py | 10 ++++++++- tests/test_utils.py | 16 ++++++++++++++ tests/test_views.py | 29 ++++++++++++++++++++++--- 7 files changed, 117 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 12c1a878..7f1cb6d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ flake8==2.2.2 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 +pymongo==2.8 diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index f79079f7..0d882015 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -103,6 +103,10 @@ def _check_payload(self, token): msg = _('Error decoding signature.') raise serializers.ValidationError(msg) + if api_settings.JWT_ENABLE_BLACKLIST and utils.jwt_is_blacklisted(payload): + msg = _("Token is blacklisted") + raise serializers.ValidationError(msg) + return payload def _check_user(self, payload): @@ -152,6 +156,7 @@ def validate(self, attrs): payload = self._check_payload(token=token) user = self._check_user(payload=payload) + # Get and check 'orig_iat' orig_iat = payload.get('orig_iat') @@ -180,3 +185,29 @@ def validate(self, attrs): 'token': jwt_encode_handler(new_payload), 'user': user } + + +class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): + """ + Blacklist an access token. + """ + + def validate(self, attrs): + if not api_settings.JWT_ENABLE_BLACKLIST: + msg = _('JWT_ENABLE_BLACKLIST is set to False.') + raise serializers.ValidationError(msg) + + token = attrs['token'] + + payload = self._check_payload(token=token) + user = self._check_user(payload=payload) + # Get and check 'jti' + jti = payload.get('jti') + + if jti: + utils.jwt_blacklist(payload) + + return { + 'token': None, + 'user': user + } diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index 178b4f21..90fd8104 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -35,6 +35,8 @@ 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), 'JWT_AUTH_HEADER_PREFIX': 'JWT', + + 'JWT_ENABLE_BLACKLIST': False, } # List of settings that may be in string import notation. diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 4e9ee042..09a393bc 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,9 +1,16 @@ -import jwt - +import random +import string from datetime import datetime +import jwt +from django.utils.translation import gettext_lazy as _ + from rest_framework_jwt.settings import api_settings +if api_settings.JWT_ENABLE_BLACKLIST: + import pymongo + jti_collection = pymongo.MongoClient().jwt_db.jti_collection + def get_user_model(): try: @@ -22,13 +29,18 @@ def jwt_payload_handler(user): except AttributeError: username = user.username - return { + payload = { 'user_id': user.pk, 'email': user.email, 'username': username, 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA } + if 'jti' not in payload: + payload['jti'] = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(20)) + + return payload + def jwt_get_user_id_from_payload_handler(payload): """ @@ -74,6 +86,23 @@ def jwt_response_payload_handler(token, user=None): } """ + return { 'token': token } + + +def jwt_is_blacklisted(payload): + if 'jti' not in payload or api_settings.JWT_ENABLE_BLACKLIST is False: + return False + + return jti_collection.find_one({'jti': payload['jti']}) is not None + + +def jwt_blacklist(payload): + if 'jti' not in payload: + raise ValueError(_("Can't blacklist payloads that don't have a jti claim")) + + if not jwt_is_blacklisted(payload): + jti_collection.insert({'jti': payload['jti'], + 'payload': payload}) diff --git a/rest_framework_jwt/views.py b/rest_framework_jwt/views.py index e951dd51..33ee6d7e 100644 --- a/rest_framework_jwt/views.py +++ b/rest_framework_jwt/views.py @@ -8,7 +8,7 @@ from .serializers import ( JSONWebTokenSerializer, RefreshJSONWebTokenSerializer, - VerifyJSONWebTokenSerializer + VerifyJSONWebTokenSerializer, BlacklistJSONWebTokenSerializer ) jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER @@ -65,6 +65,14 @@ class RefreshJSONWebToken(JSONWebTokenAPIView): serializer_class = RefreshJSONWebTokenSerializer +class BlacklistJSONWebToken(JSONWebTokenAPIView): + """ + API View that blacklists a token + """ + serializer_class = BlacklistJSONWebTokenSerializer + + obtain_jwt_token = ObtainJSONWebToken.as_view() refresh_jwt_token = RefreshJSONWebToken.as_view() verify_jwt_token = VerifyJSONWebToken.as_view() +blacklist_jwt_token = BlacklistJSONWebToken.as_view() diff --git a/tests/test_utils.py b/tests/test_utils.py index 421876fe..0232ceed 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -57,6 +57,22 @@ def test_jwt_response_payload(self): self.assertEqual(response_data, dict(token=token)) + def test_jti_blacklist(self): + api_settings.JWT_ENABLE_BLACKLIST = True + reload(utils) # it will now retrieve the mongo collection for the jti blacklist + payload = utils.jwt_payload_handler(self.user) + utils.jwt_blacklist(payload) + + self.assertEqual(utils.jwt_is_blacklisted(payload), True) + + def test_fail_blacklist_without_jti(self): + api_settings.JWT_ENABLE_BLACKLIST = True + payload = utils.jwt_payload_handler(self.user) + payload.pop('jti') + + with self.assertRaisesMessage(ValueError, "Can't blacklist payloads that don't have a jti claim"): + utils.jwt_blacklist(payload) + class TestAudience(TestCase): def setUp(self): diff --git a/tests/test_views.py b/tests/test_views.py index 1799dcd4..c9a4c129 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -26,6 +26,8 @@ (r'^auth-token/$', 'rest_framework_jwt.views.obtain_jwt_token'), (r'^auth-token-refresh/$', 'rest_framework_jwt.views.refresh_jwt_token'), (r'^auth-token-verify/$', 'rest_framework_jwt.views.verify_jwt_token'), + (r'^auth-token-blacklist/$', 'rest_framework_jwt.views.blacklist_jwt_token'), + ) @@ -233,7 +235,7 @@ class VerifyJSONWebTokenTests(TokenTestCase): def test_verify_jwt(self): """ Test that a valid, non-expired token will return a 200 response - and itself when passed to the validation endpoint. + when passed to the validation endpoint. """ client = APIClient(enforce_csrf_checks=True) @@ -244,8 +246,6 @@ def test_verify_jwt(self): format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['token'], orig_token) - def test_verify_jwt_fails_with_expired_token(self): """ Test that an expired token will fail with the correct error. @@ -298,6 +298,29 @@ def test_verify_jwt_fails_with_missing_user(self): self.assertRegexpMatches(response.data['non_field_errors'][0], "User doesn't exist") + def test_verify_jwt_fails_with_blacklisted_token(self): + """ + Test that a blacklisted token will fail. + """ + api_settings.JWT_ENABLE_BLACKLIST = True + client = APIClient(enforce_csrf_checks=True) + + user = User.objects.create_user( + email='jsmith@example.com', username='jsmith', password='password') + + token = self.create_token(user) + + response = client.post('/auth-token-blacklist/', {'token': token}, + format='json') + + self.assertIsNone(response.data['token']) + + response = client.post('/auth-token-verify/', {'token': token}, + format='json') + + self.assertRegexpMatches(response.data['non_field_errors'][0], + "Token is blacklisted") + class RefreshJSONWebTokenTests(TokenTestCase): From 003e80548b058a21a50d041a2c95d0f98a0fc54f Mon Sep 17 00:00:00 2001 From: AM Date: Mon, 23 Mar 2015 11:22:47 +0100 Subject: [PATCH 02/93] reducing the probability of repeated keys by using the user_id:jti pair as the key --- rest_framework_jwt/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 09a393bc..ef5c35b3 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -96,7 +96,7 @@ def jwt_is_blacklisted(payload): if 'jti' not in payload or api_settings.JWT_ENABLE_BLACKLIST is False: return False - return jti_collection.find_one({'jti': payload['jti']}) is not None + return jti_collection.find_one({str(payload['user_id']): payload['jti']}) is not None def jwt_blacklist(payload): @@ -104,5 +104,5 @@ def jwt_blacklist(payload): raise ValueError(_("Can't blacklist payloads that don't have a jti claim")) if not jwt_is_blacklisted(payload): - jti_collection.insert({'jti': payload['jti'], + jti_collection.insert({str(payload['user_id']): payload['jti'], 'payload': payload}) From 0c0f946d8ff72c2ac4b1c0425649b14a29f493b6 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:01:09 -0700 Subject: [PATCH 03/93] Adds JWTBlackListToken model and default implementation for blacklisting jwt tokens --- requirements.txt | 2 +- rest_framework_jwt/models.py | 15 +++++++++- rest_framework_jwt/serializers.py | 25 +++++++++------- rest_framework_jwt/settings.py | 8 ++++++ rest_framework_jwt/utils.py | 47 ++++++++++++++++--------------- rest_framework_jwt/views.py | 13 ++++----- 6 files changed, 68 insertions(+), 42 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7f1cb6d4..f0053f53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pytest-cov==1.6 flake8==2.2.2 # Optional packages +django-uuidfield>=0.5.0 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 -pymongo==2.8 diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 5b53a526..ebb30349 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -1 +1,14 @@ -# Just to keep things like ./manage.py test happy +from django.db import models + +# Django 1.8 includes UUIDField +if hasattr(models, 'UUIDField'): + import uuid + jti_field = models.UUIDField(editable=False, unique=True) +else: + from uuidfield import UUIDField + jti_field = UUIDField(auto=False, unique=True) + + +class JWTBlackListToken(models.Model): + jti = jti_field + timestamp = models.DateTimeField(auto_now_add=True) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 0d882015..c1460260 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -16,6 +16,8 @@ jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER jwt_decode_handler = api_settings.JWT_DECODE_HANDLER jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER +jwt_blacklist_get_handler = api_settings.JWT_BLACKLIST_GET_HANDLER +jwt_blacklist_set_handler = api_settings.JWT_BLACKLIST_SET_HANDLER class JSONWebTokenSerializer(Serializer): @@ -103,9 +105,12 @@ def _check_payload(self, token): msg = _('Error decoding signature.') raise serializers.ValidationError(msg) - if api_settings.JWT_ENABLE_BLACKLIST and utils.jwt_is_blacklisted(payload): - msg = _("Token is blacklisted") - raise serializers.ValidationError(msg) + # Check if the token has been blacklisted. + if api_settings.JWT_ENABLE_BLACKLIST: + blacklisted = jwt_blacklist_get_handler(payload) + + if blacklisted: + raise serializers.ValidationError(_('Token is blacklisted.')) return payload @@ -193,19 +198,19 @@ class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): """ def validate(self, attrs): + + token = attrs['token'] + if not api_settings.JWT_ENABLE_BLACKLIST: msg = _('JWT_ENABLE_BLACKLIST is set to False.') raise serializers.ValidationError(msg) - token = attrs['token'] - payload = self._check_payload(token=token) - user = self._check_user(payload=payload) - # Get and check 'jti' - jti = payload.get('jti') + + # Handle blacklisting a token. + jwt_blacklist_set_handler(payload) - if jti: - utils.jwt_blacklist(payload) + user = self._check_user(payload=payload) return { 'token': None, diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index 90fd8104..70da4d35 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -22,6 +22,12 @@ 'JWT_RESPONSE_PAYLOAD_HANDLER': 'rest_framework_jwt.utils.jwt_response_payload_handler', + 'JWT_BLACKLIST_GET_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_get_handler', + + 'JWT_BLACKLIST_DET_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_set_handler', + 'JWT_SECRET_KEY': settings.SECRET_KEY, 'JWT_ALGORITHM': 'HS256', 'JWT_VERIFY': True, @@ -46,6 +52,8 @@ 'JWT_PAYLOAD_HANDLER', 'JWT_PAYLOAD_GET_USER_ID_HANDLER', 'JWT_RESPONSE_PAYLOAD_HANDLER', + 'JWT_BLACKLIST_GET_HANDLER', + 'JWT_BLACKLIST_SET_HANDLER', ) api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index ec06d6bd..a48c772f 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,15 +1,15 @@ +import jwt +import uuid import random import string + from datetime import datetime -import jwt from django.utils.translation import gettext_lazy as _ from rest_framework_jwt.settings import api_settings -if api_settings.JWT_ENABLE_BLACKLIST: - import pymongo - jti_collection = pymongo.MongoClient().jwt_db.jti_collection +from . import models def get_user_model(): @@ -29,18 +29,14 @@ def jwt_payload_handler(user): except AttributeError: username = user.username - payload = { + return { 'user_id': user.pk, 'email': user.email, 'username': username, - 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA + 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA, + 'jti': uuid.uuid4() } - if 'jti' not in payload: - payload['jti'] = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(20)) - - return payload - def jwt_get_user_id_from_payload_handler(payload): """ @@ -92,17 +88,24 @@ def jwt_response_payload_handler(token, user=None, request=None): } -def jwt_is_blacklisted(payload): - if 'jti' not in payload or api_settings.JWT_ENABLE_BLACKLIST is False: - return False - - return jti_collection.find_one({str(payload['user_id']): payload['jti']}) is not None +def jwt_blacklist_get_handler(payload): + """ + Default implementation to check if a blacklisted jwt token exists. + """ + jti = payload.get('jti') + try: + token = models.JWTBlackListToken.objects.get(jti=jti) + except models.JWTBlackListToken.DoesNotExist: + return False + else: + return True -def jwt_blacklist(payload): - if 'jti' not in payload: - raise ValueError(_("Can't blacklist payloads that don't have a jti claim")) - if not jwt_is_blacklisted(payload): - jti_collection.insert({str(payload['user_id']): payload['jti'], - 'payload': payload}) +def jwt_blacklist_set_handler(payload): + """ + Default implementation that blacklists a jwt token. + """ + jti = payload.get('jti') + + return models.JWTBlackListToken.objects.create(jti=jti) diff --git a/rest_framework_jwt/views.py b/rest_framework_jwt/views.py index 33ee6d7e..6834bd4e 100644 --- a/rest_framework_jwt/views.py +++ b/rest_framework_jwt/views.py @@ -6,10 +6,7 @@ from rest_framework_jwt.settings import api_settings -from .serializers import ( - JSONWebTokenSerializer, RefreshJSONWebTokenSerializer, - VerifyJSONWebTokenSerializer, BlacklistJSONWebTokenSerializer -) +from . import serializers jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER @@ -43,7 +40,7 @@ class ObtainJSONWebToken(JSONWebTokenAPIView): Returns a JSON Web Token that can be used for authenticated requests. """ - serializer_class = JSONWebTokenSerializer + serializer_class = serializers.JSONWebTokenSerializer class VerifyJSONWebToken(JSONWebTokenAPIView): @@ -51,7 +48,7 @@ class VerifyJSONWebToken(JSONWebTokenAPIView): API View that checks the veracity of a token, returning the token if it is valid. """ - serializer_class = VerifyJSONWebTokenSerializer + serializer_class = serializers.VerifyJSONWebTokenSerializer class RefreshJSONWebToken(JSONWebTokenAPIView): @@ -62,14 +59,14 @@ class RefreshJSONWebToken(JSONWebTokenAPIView): If 'orig_iat' field (original issued-at-time) is found, will first check if it's within expiration window, then copy it to the new token """ - serializer_class = RefreshJSONWebTokenSerializer + serializer_class = serializers.RefreshJSONWebTokenSerializer class BlacklistJSONWebToken(JSONWebTokenAPIView): """ API View that blacklists a token """ - serializer_class = BlacklistJSONWebTokenSerializer + serializer_class = serializers.BlacklistJSONWebTokenSerializer obtain_jwt_token = ObtainJSONWebToken.as_view() From caaa6eb9d11f161b4fbb3552a2d5b0e9e58aeb45 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:05:19 -0700 Subject: [PATCH 04/93] Fixed typo --- rest_framework_jwt/settings.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index 70da4d35..d99f8b15 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -25,7 +25,7 @@ 'JWT_BLACKLIST_GET_HANDLER': 'rest_framework_jwt.utils.jwt_blacklist_get_handler', - 'JWT_BLACKLIST_DET_HANDLER': + 'JWT_BLACKLIST_SET_HANDLER': 'rest_framework_jwt.utils.jwt_blacklist_set_handler', 'JWT_SECRET_KEY': settings.SECRET_KEY, @@ -36,12 +36,9 @@ 'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300), 'JWT_AUDIENCE': None, 'JWT_ISSUER': None, - 'JWT_ALLOW_REFRESH': False, 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), - 'JWT_AUTH_HEADER_PREFIX': 'JWT', - 'JWT_ENABLE_BLACKLIST': False, } From e08ad9318c5b401b0884706524cd3001cb5d36d2 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:13:46 -0700 Subject: [PATCH 05/93] Disable blacklist tests temporarily --- tests/test_utils.py | 14 ++------------ tests/test_views.py | 19 +------------------ 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index d034235d..9f530b15 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -58,20 +58,10 @@ def test_jwt_response_payload(self): self.assertEqual(response_data, dict(token=token)) def test_jti_blacklist(self): - api_settings.JWT_ENABLE_BLACKLIST = True - reload(utils) # it will now retrieve the mongo collection for the jti blacklist - payload = utils.jwt_payload_handler(self.user) - utils.jwt_blacklist(payload) - - self.assertEqual(utils.jwt_is_blacklisted(payload), True) + pass def test_fail_blacklist_without_jti(self): - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) - payload.pop('jti') - - with self.assertRaisesMessage(ValueError, "Can't blacklist payloads that don't have a jti claim"): - utils.jwt_blacklist(payload) + pass class TestAudience(TestCase): diff --git a/tests/test_views.py b/tests/test_views.py index c9a4c129..6825f0bc 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -302,24 +302,7 @@ def test_verify_jwt_fails_with_blacklisted_token(self): """ Test that a blacklisted token will fail. """ - api_settings.JWT_ENABLE_BLACKLIST = True - client = APIClient(enforce_csrf_checks=True) - - user = User.objects.create_user( - email='jsmith@example.com', username='jsmith', password='password') - - token = self.create_token(user) - - response = client.post('/auth-token-blacklist/', {'token': token}, - format='json') - - self.assertIsNone(response.data['token']) - - response = client.post('/auth-token-verify/', {'token': token}, - format='json') - - self.assertRegexpMatches(response.data['non_field_errors'][0], - "Token is blacklisted") + pass class RefreshJSONWebTokenTests(TokenTestCase): From 09d5b8ac1ba6f51113c1f937e6de466b3ca1c9dd Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:19:07 -0700 Subject: [PATCH 06/93] Added fallback for import error --- rest_framework_jwt/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index ebb30349..f1733966 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -5,8 +5,11 @@ import uuid jti_field = models.UUIDField(editable=False, unique=True) else: - from uuidfield import UUIDField - jti_field = UUIDField(auto=False, unique=True) + try: + from uuidfield import UUIDField + jti_field = UUIDField(auto=False, unique=True) + except ImportError: + jti_field = CharField(max_length=64, editable= False, unique=True) class JWTBlackListToken(models.Model): From 4bc7e9648cb560217e857d30011aeebf8ff8a692 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:20:43 -0700 Subject: [PATCH 07/93] Fixed typo --- rest_framework_jwt/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index f1733966..4111f920 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -9,7 +9,7 @@ from uuidfield import UUIDField jti_field = UUIDField(auto=False, unique=True) except ImportError: - jti_field = CharField(max_length=64, editable= False, unique=True) + jti_field = models.CharField(max_length=64, editable= False, unique=True) class JWTBlackListToken(models.Model): From 1eaf24fa6cf8f69ff2d02ee99bb4b22e8cb123f2 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:27:00 -0700 Subject: [PATCH 08/93] flake8 --- rest_framework_jwt/models.py | 3 ++- rest_framework_jwt/serializers.py | 4 ++-- rest_framework_jwt/utils.py | 2 +- tests/test_views.py | 12 +++++++----- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 4111f920..8eb501f3 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -9,7 +9,8 @@ from uuidfield import UUIDField jti_field = UUIDField(auto=False, unique=True) except ImportError: - jti_field = models.CharField(max_length=64, editable= False, unique=True) + jti_field = models.CharField(max_length=64, + editable=False, unique=True) class JWTBlackListToken(models.Model): diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index c1460260..14884f49 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -198,7 +198,7 @@ class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): """ def validate(self, attrs): - + token = attrs['token'] if not api_settings.JWT_ENABLE_BLACKLIST: @@ -206,7 +206,7 @@ def validate(self, attrs): raise serializers.ValidationError(msg) payload = self._check_payload(token=token) - + # Handle blacklisting a token. jwt_blacklist_set_handler(payload) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index a48c772f..5780744a 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -107,5 +107,5 @@ def jwt_blacklist_set_handler(payload): Default implementation that blacklists a jwt token. """ jti = payload.get('jti') - + return models.JWTBlackListToken.objects.create(jti=jti) diff --git a/tests/test_views.py b/tests/test_views.py index 6825f0bc..9f48850b 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -26,9 +26,8 @@ (r'^auth-token/$', 'rest_framework_jwt.views.obtain_jwt_token'), (r'^auth-token-refresh/$', 'rest_framework_jwt.views.refresh_jwt_token'), (r'^auth-token-verify/$', 'rest_framework_jwt.views.verify_jwt_token'), - (r'^auth-token-blacklist/$', 'rest_framework_jwt.views.blacklist_jwt_token'), - - + (r'^auth-token-blacklist/$', + 'rest_framework_jwt.views.blacklist_jwt_token'), ) orig_datetime = datetime @@ -347,8 +346,11 @@ def test_refresh_jwt_after_refresh_expiration(self): """ client = APIClient(enforce_csrf_checks=True) - orig_iat = (datetime.utcnow() - api_settings.JWT_REFRESH_EXPIRATION_DELTA - - timedelta(seconds=5)) + orig_iat = ( + datetime.utcnow() + - api_settings.JWT_REFRESH_EXPIRATION_DELTA + - timedelta(seconds=5) + ) token = self.create_token( self.user, exp=datetime.utcnow() + timedelta(hours=1), From dda83039d21e932294b0ffdfa3a2c1b9d8cd90e9 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 14:06:57 -0700 Subject: [PATCH 09/93] Cleanup --- rest_framework_jwt/models.py | 1 - rest_framework_jwt/utils.py | 6 +----- tests/test_views.py | 6 +++--- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 8eb501f3..a3a7baa6 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -2,7 +2,6 @@ # Django 1.8 includes UUIDField if hasattr(models, 'UUIDField'): - import uuid jti_field = models.UUIDField(editable=False, unique=True) else: try: diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 5780744a..17abc6f5 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,12 +1,8 @@ import jwt import uuid -import random -import string from datetime import datetime -from django.utils.translation import gettext_lazy as _ - from rest_framework_jwt.settings import api_settings from . import models @@ -99,7 +95,7 @@ def jwt_blacklist_get_handler(payload): except models.JWTBlackListToken.DoesNotExist: return False else: - return True + return bool(token) def jwt_blacklist_set_handler(payload): diff --git a/tests/test_views.py b/tests/test_views.py index 9f48850b..91b4e126 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -347,9 +347,9 @@ def test_refresh_jwt_after_refresh_expiration(self): client = APIClient(enforce_csrf_checks=True) orig_iat = ( - datetime.utcnow() - - api_settings.JWT_REFRESH_EXPIRATION_DELTA - - timedelta(seconds=5) + datetime.utcnow() - + api_settings.JWT_REFRESH_EXPIRATION_DELTA - + timedelta(seconds=5) ) token = self.create_token( self.user, From c592c25c87bf7798e474f42a3ce81adebdb2364a Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 14:19:23 -0700 Subject: [PATCH 10/93] Use uuid.hex --- rest_framework_jwt/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 17abc6f5..5e02a2f3 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -30,7 +30,7 @@ def jwt_payload_handler(user): 'email': user.email, 'username': username, 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA, - 'jti': uuid.uuid4() + 'jti': uuid.uuid4().hex } From 8a5264716a86882839f9f91079e5cbaa730f522e Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 16:24:09 -0700 Subject: [PATCH 11/93] Moved uuid import to compat --- rest_framework_jwt/compat.py | 21 +++++++++++++++++++++ rest_framework_jwt/models.py | 15 ++++----------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/rest_framework_jwt/compat.py b/rest_framework_jwt/compat.py index 01313aae..764899ea 100644 --- a/rest_framework_jwt/compat.py +++ b/rest_framework_jwt/compat.py @@ -1,5 +1,9 @@ import rest_framework + +from django.db import models + from distutils.version import StrictVersion +from functools import partial if StrictVersion(rest_framework.VERSION) < StrictVersion('3.0.0'): @@ -9,3 +13,20 @@ class Serializer(rest_framework.serializers.Serializer): @property def object(self): return self.validated_data + + +def get_uuid_field(object): + """ + Returns a partial object that when called instantiates a UUIDField + either from Django 1.8's native implementation, from django-uuidfield, + or as a CharField as the final fallback. + """ + if hasattr(models, 'UUIDField'): + return partial(models.UUIDField, editable=False, unique=True) + else: + try: + from uuidfield import UUIDField + return partial(UUIDField, auto=False, unique=True) + except ImportError: + return partial(models.CharField, max_length=64, + editable=False, unique=True) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index a3a7baa6..a8d6a2d9 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -1,17 +1,10 @@ from django.db import models -# Django 1.8 includes UUIDField -if hasattr(models, 'UUIDField'): - jti_field = models.UUIDField(editable=False, unique=True) -else: - try: - from uuidfield import UUIDField - jti_field = UUIDField(auto=False, unique=True) - except ImportError: - jti_field = models.CharField(max_length=64, - editable=False, unique=True) +from .compat import get_uuid_field + +UUIDField = get_uuid_field() class JWTBlackListToken(models.Model): - jti = jti_field + jti = UUIDField() timestamp = models.DateTimeField(auto_now_add=True) From 9fc775063556454de2b6d79850c7078f4fe0936b Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 20:14:59 -0700 Subject: [PATCH 12/93] Cleanup --- rest_framework_jwt/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_jwt/compat.py b/rest_framework_jwt/compat.py index 764899ea..4a53eeeb 100644 --- a/rest_framework_jwt/compat.py +++ b/rest_framework_jwt/compat.py @@ -15,7 +15,7 @@ def object(self): return self.validated_data -def get_uuid_field(object): +def get_uuid_field(): """ Returns a partial object that when called instantiates a UUIDField either from Django 1.8's native implementation, from django-uuidfield, From 2b80040a41fcbc50e1162139c8fc7958cdfbc293 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 21:26:42 -0700 Subject: [PATCH 13/93] Added blacklisted token check to authentiate method to verify incoming requests arent using black listed token --- rest_framework_jwt/authentication.py | 8 ++++++++ rest_framework_jwt/utils.py | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index 513be8db..4641f3e0 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -11,6 +11,7 @@ jwt_decode_handler = api_settings.JWT_DECODE_HANDLER jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER +jwt_blacklist_get_handler = api_settings.JWT_BLACKLIST_GET_HANDLER class BaseJSONWebTokenAuthentication(BaseAuthentication): @@ -36,6 +37,13 @@ def authenticate(self, request): msg = _('Error decoding signature.') raise exceptions.AuthenticationFailed(msg) + # Check if the token has been blacklisted. + if api_settings.JWT_ENABLE_BLACKLIST: + blacklisted = jwt_blacklist_get_handler(payload) + + if blacklisted: + raise serializers.ValidationError(_('Token is blacklisted.')) + user = self.authenticate_credentials(payload) return (user, jwt_value) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 5e02a2f3..33328676 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -38,8 +38,7 @@ def jwt_get_user_id_from_payload_handler(payload): """ Override this function if user_id is formatted differently in payload """ - user_id = payload.get('user_id') - return user_id + return payload.get('user_id') def jwt_encode_handler(payload): From d63cb708055624d50669ca4ad888ad7313ea549e Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 21:28:08 -0700 Subject: [PATCH 14/93] Fixed import --- rest_framework_jwt/authentication.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index 4641f3e0..5b08b962 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -1,7 +1,10 @@ import jwt + from django.utils.encoding import smart_text from django.utils.translation import ugettext as _ + from rest_framework import exceptions +from rest_framework import serializers from rest_framework.authentication import (BaseAuthentication, get_authorization_header) From 303dd59c1917ae24cce7b38dd3c937d2cc34a7e6 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 21:56:14 -0700 Subject: [PATCH 15/93] Changed ValidationError to Authorization exception --- rest_framework_jwt/authentication.py | 5 +++-- rest_framework_jwt/serializers.py | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index 5b08b962..889e71cd 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -4,7 +4,6 @@ from django.utils.translation import ugettext as _ from rest_framework import exceptions -from rest_framework import serializers from rest_framework.authentication import (BaseAuthentication, get_authorization_header) @@ -45,7 +44,9 @@ def authenticate(self, request): blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: - raise serializers.ValidationError(_('Token is blacklisted.')) + raise exceptions.AuthenticationFailed( + _('Token is blacklisted.') + ) user = self.authenticate_credentials(payload) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 14884f49..3be4dda5 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -5,12 +5,13 @@ from django.contrib.auth import authenticate from django.utils.translation import ugettext as _ -from rest_framework import serializers -from .compat import Serializer +from rest_framework import exceptions +from rest_framework import serializers from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings +from .compat import Serializer jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER @@ -110,7 +111,9 @@ def _check_payload(self, token): blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: - raise serializers.ValidationError(_('Token is blacklisted.')) + raise exceptions.AuthenticationFailed( + _('Token is blacklisted.') + ) return payload From fda33b36abbfa72d2189187dfa493a20afea713a Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 23:43:33 -0700 Subject: [PATCH 16/93] Cleanup --- rest_framework_jwt/authentication.py | 5 ++--- rest_framework_jwt/serializers.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index 889e71cd..530a18ed 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -44,9 +44,8 @@ def authenticate(self, request): blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: - raise exceptions.AuthenticationFailed( - _('Token is blacklisted.') - ) + msg = _('Token is blacklisted.') + raise exceptions.AuthenticationFailed(msg) user = self.authenticate_credentials(payload) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 3be4dda5..47dced4f 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -111,9 +111,8 @@ def _check_payload(self, token): blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: - raise exceptions.AuthenticationFailed( - _('Token is blacklisted.') - ) + msg = _('Token is blacklisted.') + raise exceptions.AuthenticationFailed(msg) return payload From 0452dbefb562002bd0dae6379011db01c959ec1e Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 08:03:11 -0700 Subject: [PATCH 17/93] Added expires_at field to JWTBlackListToken model --- rest_framework_jwt/models.py | 1 + rest_framework_jwt/utils.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index a8d6a2d9..e34c19ea 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -7,4 +7,5 @@ class JWTBlackListToken(models.Model): jti = UUIDField() + expires_at = models.DateTimeField() timestamp = models.DateTimeField(auto_now_add=True) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 33328676..7b0f3b19 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -86,15 +86,17 @@ def jwt_response_payload_handler(token, user=None, request=None): def jwt_blacklist_get_handler(payload): """ Default implementation to check if a blacklisted jwt token exists. + + Should return a black listed token or None. """ jti = payload.get('jti') try: token = models.JWTBlackListToken.objects.get(jti=jti) except models.JWTBlackListToken.DoesNotExist: - return False + return None else: - return bool(token) + return token def jwt_blacklist_set_handler(payload): @@ -102,5 +104,6 @@ def jwt_blacklist_set_handler(payload): Default implementation that blacklists a jwt token. """ jti = payload.get('jti') + exp = datetime.fromtimestamp(payload.get('exp')) - return models.JWTBlackListToken.objects.create(jti=jti) + return models.JWTBlackListToken.objects.create(jti=jti, expires_at=exp) From e857b8011aa281586d1e6a68ddeca869061f7e2c Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 09:48:30 -0700 Subject: [PATCH 18/93] Code tweaks --- rest_framework_jwt/models.py | 6 ++++++ rest_framework_jwt/serializers.py | 4 ++-- rest_framework_jwt/utils.py | 20 ++++++++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index e34c19ea..242c7f03 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -1,3 +1,5 @@ +import datetime + from django.db import models from .compat import get_uuid_field @@ -9,3 +11,7 @@ class JWTBlackListToken(models.Model): jti = UUIDField() expires_at = models.DateTimeField() timestamp = models.DateTimeField(auto_now_add=True) + + def is_expired(self): + now = datetime.datetime.now() + return expires_at < now diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 47dced4f..ef4bd690 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -210,11 +210,11 @@ def validate(self, attrs): payload = self._check_payload(token=token) # Handle blacklisting a token. - jwt_blacklist_set_handler(payload) + token = jwt_blacklist_set_handler(payload) user = self._check_user(payload=payload) return { - 'token': None, + 'token': token, 'user': user } diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 7b0f3b19..de55da76 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -3,6 +3,9 @@ from datetime import datetime +from django.utils.translations import ugettext_lazy as _ + +from rest_framework import exceptions from rest_framework_jwt.settings import api_settings from . import models @@ -102,8 +105,17 @@ def jwt_blacklist_get_handler(payload): def jwt_blacklist_set_handler(payload): """ Default implementation that blacklists a jwt token. - """ - jti = payload.get('jti') - exp = datetime.fromtimestamp(payload.get('exp')) - return models.JWTBlackListToken.objects.create(jti=jti, expires_at=exp) + Should return a black listed token. + """ + data = { + 'jti': payload.get('jti') + } + try: + data.update({ + 'exp': datetime.fromtimestamp(payload.get('exp')) + }) + except TypeError: + return None + else: + return models.JWTBlackListToken.objects.create(**data) From 35f7017ccc520c3f09a2d60204680d47e5eb3b39 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 09:53:15 -0700 Subject: [PATCH 19/93] cleanup --- rest_framework_jwt/models.py | 2 +- rest_framework_jwt/utils.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 242c7f03..c966b285 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -14,4 +14,4 @@ class JWTBlackListToken(models.Model): def is_expired(self): now = datetime.datetime.now() - return expires_at < now + return self.expires_at < now diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index de55da76..bd022dc8 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -3,9 +3,6 @@ from datetime import datetime -from django.utils.translations import ugettext_lazy as _ - -from rest_framework import exceptions from rest_framework_jwt.settings import api_settings from . import models @@ -106,7 +103,7 @@ def jwt_blacklist_set_handler(payload): """ Default implementation that blacklists a jwt token. - Should return a black listed token. + Should return a black listed token or None. """ data = { 'jti': payload.get('jti') From 75e4f4d9efebb20be335f1ff5245697d0ad31634 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 09:57:04 -0700 Subject: [PATCH 20/93] Fix expires_at --- rest_framework_jwt/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index bd022dc8..27924334 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -110,7 +110,7 @@ def jwt_blacklist_set_handler(payload): } try: data.update({ - 'exp': datetime.fromtimestamp(payload.get('exp')) + 'expires_at': datetime.fromtimestamp(payload.get('exp')) }) except TypeError: return None From 849f87916a229ca045905d0f74ffa990cca658ca Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 10:02:15 -0700 Subject: [PATCH 21/93] Changed back to return None --- rest_framework_jwt/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index ef4bd690..47dced4f 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -210,11 +210,11 @@ def validate(self, attrs): payload = self._check_payload(token=token) # Handle blacklisting a token. - token = jwt_blacklist_set_handler(payload) + jwt_blacklist_set_handler(payload) user = self._check_user(payload=payload) return { - 'token': token, + 'token': None, 'user': user } From 84b4baa629bbfc4d7ecd71e0b27c567a80e8b415 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 13:10:12 -0700 Subject: [PATCH 22/93] Added admin for default implementation --- rest_framework_jwt/admin.py | 15 +++++++++++++++ rest_framework_jwt/models.py | 14 ++++++++++---- rest_framework_jwt/utils.py | 4 +++- 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 rest_framework_jwt/admin.py diff --git a/rest_framework_jwt/admin.py b/rest_framework_jwt/admin.py new file mode 100644 index 00000000..ecab732a --- /dev/null +++ b/rest_framework_jwt/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin + +from . import models + + +class JWTBlackListTokenAdmin(admin.ModelAdmin): + list_display = ('jti', 'expires', 'created', 'is_expired') + fields = ('jti', 'expires', 'created', 'is_expired') + readonly_fields = ('jti', 'expires', 'created', 'is_expired') + + def is_expired(self, obj): + return obj.is_expired() + is_expired.boolean = True + +admin.site.register(models.JWTBlackListToken, JWTBlackListTokenAdmin) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index c966b285..d3ce810a 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -1,6 +1,8 @@ +import pytz import datetime from django.db import models +from django.utils.translation import ugettext_lazy as _ from .compat import get_uuid_field @@ -9,9 +11,13 @@ class JWTBlackListToken(models.Model): jti = UUIDField() - expires_at = models.DateTimeField() - timestamp = models.DateTimeField(auto_now_add=True) + expires = models.DateTimeField() + created = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = _('JWT Blacklist Token') + verbose_name_plural = _('JWT Blacklist Tokens') def is_expired(self): - now = datetime.datetime.now() - return self.expires_at < now + now = datetime.datetime.now(pytz.utc) + return self.expires < now diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 27924334..aac12889 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,4 +1,5 @@ import jwt +import pytz import uuid from datetime import datetime @@ -110,7 +111,8 @@ def jwt_blacklist_set_handler(payload): } try: data.update({ - 'expires_at': datetime.fromtimestamp(payload.get('exp')) + 'expires': datetime.fromtimestamp(payload.get('exp')), + 'created': datetime.now(pytz.utc) }) except TypeError: return None From 8e29a5c38db67e646725fe17f384e3b2fb28e734 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 13:13:16 -0700 Subject: [PATCH 23/93] Added pytz to req --- requirements.txt | 1 + rest_framework_jwt/utils.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index f0053f53..6ee592f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ pytest-django==2.6 pytest==2.5.2 pytest-cov==1.6 flake8==2.2.2 +pytz # Optional packages django-uuidfield>=0.5.0 diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index aac12889..0338ff68 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -107,12 +107,12 @@ def jwt_blacklist_set_handler(payload): Should return a black listed token or None. """ data = { - 'jti': payload.get('jti') + 'jti': payload.get('jti'), + 'created': datetime.now(pytz.utc) } try: data.update({ - 'expires': datetime.fromtimestamp(payload.get('exp')), - 'created': datetime.now(pytz.utc) + 'expires': datetime.fromtimestamp(payload.get('exp')) }) except TypeError: return None From 48b26e654eab8bb784737bedb8990290295a9051 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 13:49:29 -0700 Subject: [PATCH 24/93] Make admin dependent on settings --- requirements.txt | 2 +- rest_framework_jwt/admin.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6ee592f6..ae01f7ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ pytest-django==2.6 pytest==2.5.2 pytest-cov==1.6 flake8==2.2.2 -pytz +pytz==2014.10 # Optional packages django-uuidfield>=0.5.0 diff --git a/rest_framework_jwt/admin.py b/rest_framework_jwt/admin.py index ecab732a..6aaa9653 100644 --- a/rest_framework_jwt/admin.py +++ b/rest_framework_jwt/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin +from rest_framework_jwt.settings import api_settings + from . import models @@ -12,4 +14,5 @@ def is_expired(self, obj): return obj.is_expired() is_expired.boolean = True -admin.site.register(models.JWTBlackListToken, JWTBlackListTokenAdmin) +if api_settings.JWT_ENABLE_BLACKLIST: + admin.site.register(models.JWTBlackListToken, JWTBlackListTokenAdmin) From 5a8496d27511adccc2b8f509063678a163a9e377 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 18:20:52 -0700 Subject: [PATCH 25/93] Updated tests and requirements --- requirements.txt | 1 + rest_framework_jwt/compat.py | 7 +++++-- tests/test_authentication.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index ae01f7ed..bc31bf54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ django-uuidfield>=0.5.0 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 +djangorestframework-oauth>=1.0.1 diff --git a/rest_framework_jwt/compat.py b/rest_framework_jwt/compat.py index 4a53eeeb..17981bb2 100644 --- a/rest_framework_jwt/compat.py +++ b/rest_framework_jwt/compat.py @@ -5,11 +5,13 @@ from distutils.version import StrictVersion from functools import partial +from rest_framework import serializers + if StrictVersion(rest_framework.VERSION) < StrictVersion('3.0.0'): from rest_framework.serializers import Serializer else: - class Serializer(rest_framework.serializers.Serializer): + class Serializer(serializers.Serializer): @property def object(self): return self.validated_data @@ -26,7 +28,8 @@ def get_uuid_field(): else: try: from uuidfield import UUIDField - return partial(UUIDField, auto=False, unique=True) + return partial(UUIDField, editable=False, + auto=False, unique=True) except ImportError: return partial(models.CharField, max_length=64, editable=False, unique=True) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index d58ad51d..33f6d0c7 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -5,14 +5,14 @@ from django.contrib.auth import get_user_model from rest_framework import permissions, status -from rest_framework.authentication import OAuth2Authentication -from rest_framework.compat import oauth2_provider from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings, DEFAULTS from rest_framework_jwt.authentication import JSONWebTokenAuthentication +from rest_framework_oauth.authentication import oauth2_provider +from rest_framework_oauth.authentication import OAuth2Authentication User = get_user_model() From b5e5f49365546aafa903942af8ac5adcee038fb9 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 22:57:40 -0700 Subject: [PATCH 26/93] Refactored view to allow custom payload handler, added default black list token serializer, fixed import issue with tests --- rest_framework_jwt/serializers.py | 17 +++++++++++++++-- rest_framework_jwt/settings.py | 4 ++++ rest_framework_jwt/utils.py | 15 +++++++++++++++ rest_framework_jwt/views.py | 5 ++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 47dced4f..b9c20f03 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -13,6 +13,8 @@ from .compat import Serializer +from . import models + jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER jwt_decode_handler = api_settings.JWT_DECODE_HANDLER @@ -210,11 +212,22 @@ def validate(self, attrs): payload = self._check_payload(token=token) # Handle blacklisting a token. - jwt_blacklist_set_handler(payload) + token = jwt_blacklist_set_handler(payload) user = self._check_user(payload=payload) return { - 'token': None, + 'token': token, 'user': user } + + +class JWTBlackListTokenSerializer(serializers.ModelSerializer): + jti = serializers.SerializerMethodField('get_jti_value') + + class Meta: + model = models.JWTBlackListToken + + def get_jti_value(self, obj): + """Returns obj.jti manually due to py3 bug in django-uuidfield""" + return obj.jti diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index d99f8b15..c9ec568c 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -28,6 +28,9 @@ 'JWT_BLACKLIST_SET_HANDLER': 'rest_framework_jwt.utils.jwt_blacklist_set_handler', + 'JWT_BLACKLIST_RESPONSE_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_response_handler', + 'JWT_SECRET_KEY': settings.SECRET_KEY, 'JWT_ALGORITHM': 'HS256', 'JWT_VERIFY': True, @@ -51,6 +54,7 @@ 'JWT_RESPONSE_PAYLOAD_HANDLER', 'JWT_BLACKLIST_GET_HANDLER', 'JWT_BLACKLIST_SET_HANDLER', + 'JWT_BLACKLIST_RESPONSE_HANDLER', ) api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 0338ff68..98a8c931 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -2,6 +2,8 @@ import pytz import uuid +from django.utils.translation import ugettext_lazy as _ + from datetime import datetime from rest_framework_jwt.settings import api_settings @@ -118,3 +120,16 @@ def jwt_blacklist_set_handler(payload): return None else: return models.JWTBlackListToken.objects.create(**data) + + +def jwt_blacklist_response_handler(token, user=None, request=None): + """ + Default blacklist token response data. Override to provide a + custom response. + """ + from . import serializers + + return { + 'token': serializers.JWTBlackListTokenSerializer(token).data, + 'message': _('Token successfully blacklisted.') + } diff --git a/rest_framework_jwt/views.py b/rest_framework_jwt/views.py index 6834bd4e..a067091e 100644 --- a/rest_framework_jwt/views.py +++ b/rest_framework_jwt/views.py @@ -9,6 +9,7 @@ from . import serializers jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER +jwt_blacklist_response_handler = api_settings.JWT_BLACKLIST_RESPONSE_HANDLER class JSONWebTokenAPIView(APIView): @@ -20,6 +21,7 @@ class JSONWebTokenAPIView(APIView): authentication_classes = () parser_classes = (parsers.FormParser, parsers.JSONParser,) renderer_classes = (renderers.JSONRenderer,) + response_payload_handler = staticmethod(jwt_response_payload_handler) def post(self, request): serializer = self.serializer_class(data=request.DATA) @@ -27,7 +29,7 @@ def post(self, request): if serializer.is_valid(): user = serializer.object.get('user') or request.user token = serializer.object.get('token') - response_data = jwt_response_payload_handler(token, user, request) + response_data = self.response_payload_handler(token, user, request) return Response(response_data) @@ -67,6 +69,7 @@ class BlacklistJSONWebToken(JSONWebTokenAPIView): API View that blacklists a token """ serializer_class = serializers.BlacklistJSONWebTokenSerializer + response_payload_handler = staticmethod(jwt_blacklist_response_handler) obtain_jwt_token = ObtainJSONWebToken.as_view() From 20799c39ff0043993e0acb4dd2732da850107f48 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 29 Mar 2015 00:03:03 -0700 Subject: [PATCH 27/93] Updated tox --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 5b24031c..2123cfd7 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ setenv = deps = django1.6: Django==1.6.8 django1.7: Django==1.7.1 + djangorestframework-oauth==1.0.1 drf2.4.3: djangorestframework==2.4.3 drf2.4.4: djangorestframework==2.4.4 drf3.0.0: djangorestframework==3.0.0 @@ -17,6 +18,7 @@ deps = py27-django1.6-drf{2.4.3,2.4.4,3.0.0}: django-oauth-plus==2.2.1 py27-django1.6-drf{2.4.3,2.4.4,3.0.0}: django-oauth2-provider==0.2.4 pytest-django==2.6.1 + pytz==2014.10 [testenv:py27-flake8] commands = ./runtests.py --lintonly From 568efb915047aad97fa03e7d29ca12efba681334 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 29 Mar 2015 01:03:00 -0700 Subject: [PATCH 28/93] Add check for successful blacklist token --- rest_framework_jwt/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index b9c20f03..4a84fdf4 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -214,6 +214,10 @@ def validate(self, attrs): # Handle blacklisting a token. token = jwt_blacklist_set_handler(payload) + if not token: + msg = _('Could not blacklist token.') + raise serializers.ValidationError(msg) + user = self._check_user(payload=payload) return { From 16f16df0fb52cff5ac3f4b528e31e516ebd14162 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 29 Mar 2015 10:17:05 -0700 Subject: [PATCH 29/93] Fix oauth import in tox --- tests/test_authentication.py | 12 ++++++++++-- tox.ini | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 33f6d0c7..253f2ae8 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -11,8 +11,16 @@ from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings, DEFAULTS from rest_framework_jwt.authentication import JSONWebTokenAuthentication -from rest_framework_oauth.authentication import oauth2_provider -from rest_framework_oauth.authentication import OAuth2Authentication + +try: + from rest_framework.authentication import oauth2_provider +except ImportError: + from rest_framework_oauth.authentication import oauth2_provider + +try: + from rest_framework.authentication import OAuth2Authentication +except ImportError: + from rest_framework_oauth.authentication import OAuth2Authentication User = get_user_model() diff --git a/tox.ini b/tox.ini index 2123cfd7..359f7c93 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ setenv = deps = django1.6: Django==1.6.8 django1.7: Django==1.7.1 - djangorestframework-oauth==1.0.1 + py27: djangorestframework-oauth==1.0.1 drf2.4.3: djangorestframework==2.4.3 drf2.4.4: djangorestframework==2.4.4 drf3.0.0: djangorestframework==3.0.0 From e664e1ce19ea9c1ce5d42fdacd84e3164af56f75 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Mon, 30 Mar 2015 10:47:27 -0700 Subject: [PATCH 30/93] Removed pytz and django-uuidfields --- requirements.txt | 1 - rest_framework_jwt/compat.py | 12 +++--------- rest_framework_jwt/models.py | 7 ++----- rest_framework_jwt/utils.py | 4 ++-- tox.ini | 1 - 5 files changed, 7 insertions(+), 18 deletions(-) diff --git a/requirements.txt b/requirements.txt index bc31bf54..adeae17b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,6 @@ flake8==2.2.2 pytz==2014.10 # Optional packages -django-uuidfield>=0.5.0 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 diff --git a/rest_framework_jwt/compat.py b/rest_framework_jwt/compat.py index 17981bb2..02f9f06c 100644 --- a/rest_framework_jwt/compat.py +++ b/rest_framework_jwt/compat.py @@ -20,16 +20,10 @@ def object(self): def get_uuid_field(): """ Returns a partial object that when called instantiates a UUIDField - either from Django 1.8's native implementation, from django-uuidfield, - or as a CharField as the final fallback. + either from Django 1.8's native implementation, or as a CharField. """ if hasattr(models, 'UUIDField'): return partial(models.UUIDField, editable=False, unique=True) else: - try: - from uuidfield import UUIDField - return partial(UUIDField, editable=False, - auto=False, unique=True) - except ImportError: - return partial(models.CharField, max_length=64, - editable=False, unique=True) + return partial(models.CharField, max_length=64, + editable=False, unique=True) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index d3ce810a..3e4de5f3 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -1,7 +1,5 @@ -import pytz -import datetime - from django.db import models +from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from .compat import get_uuid_field @@ -19,5 +17,4 @@ class Meta: verbose_name_plural = _('JWT Blacklist Tokens') def is_expired(self): - now = datetime.datetime.now(pytz.utc) - return self.expires < now + return self.expires < now() diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 98a8c931..f5ba665a 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,7 +1,7 @@ import jwt -import pytz import uuid +from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from datetime import datetime @@ -110,7 +110,7 @@ def jwt_blacklist_set_handler(payload): """ data = { 'jti': payload.get('jti'), - 'created': datetime.now(pytz.utc) + 'created': now() } try: data.update({ diff --git a/tox.ini b/tox.ini index 359f7c93..2fbe7e64 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,6 @@ deps = py27-django1.6-drf{2.4.3,2.4.4,3.0.0}: django-oauth-plus==2.2.1 py27-django1.6-drf{2.4.3,2.4.4,3.0.0}: django-oauth2-provider==0.2.4 pytest-django==2.6.1 - pytz==2014.10 [testenv:py27-flake8] commands = ./runtests.py --lintonly From 8b42c647580603cb8b1af55a52bde780e6182797 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Mon, 30 Mar 2015 11:34:39 -0700 Subject: [PATCH 31/93] Removed one last pytz import --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index adeae17b..5e74e6a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,6 @@ pytest-django==2.6 pytest==2.5.2 pytest-cov==1.6 flake8==2.2.2 -pytz==2014.10 # Optional packages django-oauth-plus>=2.2.1 From b553d93d3e2a52177010c39a4f5b6a18aac09e46 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 31 Mar 2015 10:34:51 -0700 Subject: [PATCH 32/93] Added blacklist auth test --- tests/conftest.py | 1 + tests/test_authentication.py | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 5909129b..82e41514 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,6 +35,7 @@ def pytest_configure(): 'django.contrib.staticfiles', 'tests', + 'rest_framework_jwt', ), PASSWORD_HASHERS=( 'django.contrib.auth.hashers.MD5PasswordHasher', diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 253f2ae8..a9bc5e26 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -3,12 +3,14 @@ from django.utils import unittest from django.conf.urls import patterns from django.contrib.auth import get_user_model +from django.utils.timezone import now from rest_framework import permissions, status from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView from rest_framework_jwt import utils +from rest_framework_jwt.models import JWTBlackListToken from rest_framework_jwt.settings import api_settings, DEFAULTS from rest_framework_jwt.authentication import JSONWebTokenAuthentication @@ -53,6 +55,42 @@ def post(self, request): OAuth2Authentication, JSONWebTokenAuthentication])), ) +class BlacklistTokenAuthenticationTest(TestCase): + urls = 'tests.test_authentication' + + def setUp(self): + self.csrf_client = APIClient(enforce_csrf_checks=True) + self.username = 'jpueblo' + self.email = 'jpueblo@example.com' + self.user = User.objects.create_user(self.username, self.email) + + api_settings.JWT_ENABLE_BLACKLIST = True + + def test_post_blacklisted_token_failing_jwt_auth(self): + """ + Ensure POSTing over JWT auth with blacklisted token fails + """ + payload = utils.jwt_payload_handler(self.user) + token = utils.jwt_encode_handler(payload) + + # Create blacklist token which effectively blacklists the token. + JWTBlackListToken.objects.create(jti=payload.get('jti'), + created=now(), expires=now()) + + auth = 'JWT {0}'.format(token) + response = self.csrf_client.post( + '/jwt/', {'example': 'example'}, + HTTP_AUTHORIZATION=auth, format='json') + + msg = 'Token is blacklisted.' + + self.assertEqual(response.data['detail'], msg) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(response['WWW-Authenticate'], 'JWT realm="api"') + + def tearDown(self): + api_settings.JWT_ENABLE_BLACKLIST = False + class JSONWebTokenAuthenticationTests(TestCase): """JSON Web Token Authentication""" From aba07a0e6faadaa9d0b89a190380042250492146 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 31 Mar 2015 11:20:57 -0700 Subject: [PATCH 33/93] Added another auth test --- tests/test_authentication.py | 37 +++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index a9bc5e26..e403739f 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -55,7 +55,9 @@ def post(self, request): OAuth2Authentication, JSONWebTokenAuthentication])), ) -class BlacklistTokenAuthenticationTest(TestCase): + +class JSONWebTokenAuthenticationTests(TestCase): + """JSON Web Token Authentication""" urls = 'tests.test_authentication' def setUp(self): @@ -63,19 +65,36 @@ def setUp(self): self.username = 'jpueblo' self.email = 'jpueblo@example.com' self.user = User.objects.create_user(self.username, self.email) - + + def test_post_json_passing_jwt_auth_blacklist_enabled(self): + """ + Ensure POSTing JSON over JWT auth with correct credentials + passes and does not require CSRF + """ api_settings.JWT_ENABLE_BLACKLIST = True + payload = utils.jwt_payload_handler(self.user) + token = utils.jwt_encode_handler(payload) + + auth = 'JWT {0}'.format(token) + response = self.csrf_client.post( + '/jwt/', {'example': 'example'}, + HTTP_AUTHORIZATION=auth, format='json') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + api_settings.JWT_ENABLE_BLACKLIST = False def test_post_blacklisted_token_failing_jwt_auth(self): """ Ensure POSTing over JWT auth with blacklisted token fails """ + api_settings.JWT_ENABLE_BLACKLIST = True payload = utils.jwt_payload_handler(self.user) token = utils.jwt_encode_handler(payload) # Create blacklist token which effectively blacklists the token. JWTBlackListToken.objects.create(jti=payload.get('jti'), - created=now(), expires=now()) + created=now(), expires=now()) auth = 'JWT {0}'.format(token) response = self.csrf_client.post( @@ -88,20 +107,8 @@ def test_post_blacklisted_token_failing_jwt_auth(self): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response['WWW-Authenticate'], 'JWT realm="api"') - def tearDown(self): api_settings.JWT_ENABLE_BLACKLIST = False - -class JSONWebTokenAuthenticationTests(TestCase): - """JSON Web Token Authentication""" - urls = 'tests.test_authentication' - - def setUp(self): - self.csrf_client = APIClient(enforce_csrf_checks=True) - self.username = 'jpueblo' - self.email = 'jpueblo@example.com' - self.user = User.objects.create_user(self.username, self.email) - def test_post_form_passing_jwt_auth(self): """ Ensure POSTing form over JWT auth with correct credentials From 4272c04fa56c85aa40972b294bb3b0026978bf8a Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 31 Mar 2015 18:00:17 -0700 Subject: [PATCH 34/93] Added other blacklist unit tests --- rest_framework_jwt/utils.py | 16 ++++----- tests/test_authentication.py | 1 + tests/test_serializers.py | 45 +++++++++++++++++++++++++ tests/test_utils.py | 59 +++++++++++++++++++++++++++++--- tests/test_views.py | 65 ++++++++++++++++++++++++++++++++++-- 5 files changed, 170 insertions(+), 16 deletions(-) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index f5ba665a..b8982e01 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,6 +1,7 @@ import jwt import uuid +from django.db import IntegrityError from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ @@ -108,18 +109,15 @@ def jwt_blacklist_set_handler(payload): Should return a black listed token or None. """ - data = { - 'jti': payload.get('jti'), - 'created': now() - } try: - data.update({ + data = { + 'jti': payload.get('jti'), + 'created': now(), 'expires': datetime.fromtimestamp(payload.get('exp')) - }) - except TypeError: - return None - else: + } return models.JWTBlackListToken.objects.create(**data) + except (TypeError, IntegrityError, Exception): + return None def jwt_blacklist_response_handler(token, user=None, request=None): diff --git a/tests/test_authentication.py b/tests/test_authentication.py index e403739f..6d1e8053 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -89,6 +89,7 @@ def test_post_blacklisted_token_failing_jwt_auth(self): Ensure POSTing over JWT auth with blacklisted token fails """ api_settings.JWT_ENABLE_BLACKLIST = True + payload = utils.jwt_payload_handler(self.user) token = utils.jwt_encode_handler(payload) diff --git a/tests/test_serializers.py b/tests/test_serializers.py index a7b4fd3b..8df52c08 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -5,7 +5,9 @@ from django.utils import unittest from django.contrib.auth import get_user_model +from rest_framework_jwt.settings import api_settings from rest_framework_jwt.serializers import JSONWebTokenSerializer +from rest_framework_jwt.serializers import BlacklistJSONWebTokenSerializer from rest_framework_jwt import utils User = get_user_model() @@ -93,3 +95,46 @@ def test_required_fields(self): self.assertFalse(is_valid) self.assertEqual(serializer.errors, expected_error) + + +class BlacklistJSONWebTokenSerializerTests(TestCase): + + 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.payload = utils.jwt_payload_handler(self.user) + self.data = { + 'token': utils.jwt_encode_handler(self.payload) + } + + def test_token_blacklisted(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + serializer = BlacklistJSONWebTokenSerializer(data=self.data) + is_valid = serializer.is_valid() + + token = serializer.object['token'] + + self.assertTrue(is_valid) + self.assertEqual(self.payload.get('jti'), token.jti) + + def test_token_blacklist_fail_missing_jti(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + self.payload['jti'] = None + self.data = { + 'token': utils.jwt_encode_handler(self.payload) + } + + serializer = BlacklistJSONWebTokenSerializer(data=self.data) + is_valid = serializer.is_valid() + + self.assertFalse(is_valid) + + msg = 'Could not blacklist token.' + + self.assertEqual(serializer.errors['non_field_errors'][0], msg) diff --git a/tests/test_utils.py b/tests/test_utils.py index 9f530b15..c19d91cf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,10 +1,14 @@ import json +import time import base64 +import jwt.exceptions from django.contrib.auth import get_user_model +from django.utils.timezone import now from django.test import TestCase -import jwt.exceptions + from rest_framework_jwt import utils +from rest_framework_jwt.models import JWTBlackListToken from rest_framework_jwt.settings import api_settings, DEFAULTS User = get_user_model() @@ -57,11 +61,56 @@ def test_jwt_response_payload(self): self.assertEqual(response_data, dict(token=token)) - def test_jti_blacklist(self): - pass + def test_jwt_blacklist_get_success(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + payload = utils.jwt_payload_handler(self.user) + + # Create blacklisted token. + token_created = JWTBlackListToken.objects.create( + jti=payload.get('jti'), + expires=now(), + created=now() + ) + + token_fetched = utils.jwt_blacklist_get_handler(payload) + + self.assertEqual(token_created.jti, token_fetched.jti) + + def test_jwt_blacklist_get_fail(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + payload = utils.jwt_payload_handler(self.user) + + # Test that incoming empty jti fails. + payload['jti'] = None + + token_fetched = utils.jwt_blacklist_get_handler(payload) + + self.assertIsNone(token_fetched) + + def test_jwt_blacklist_set_success(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + payload = utils.jwt_payload_handler(self.user) + + # exp field comes in as seconds since epoch + payload['exp'] = int(time.time()) + + # Create blacklisted token. + token = utils.jwt_blacklist_set_handler(payload) + + self.assertEqual(token.jti, payload.get('jti')) + + def test_jwt_blacklist_set_fail(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + payload = utils.jwt_payload_handler(self.user) + + # Create blacklisted token. + token = utils.jwt_blacklist_set_handler(payload) - def test_fail_blacklist_without_jti(self): - pass + self.assertIsNone(token) class TestAudience(TestCase): diff --git a/tests/test_views.py b/tests/test_views.py index 91b4e126..06940859 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -27,7 +27,7 @@ (r'^auth-token-refresh/$', 'rest_framework_jwt.views.refresh_jwt_token'), (r'^auth-token-verify/$', 'rest_framework_jwt.views.verify_jwt_token'), (r'^auth-token-blacklist/$', - 'rest_framework_jwt.views.blacklist_jwt_token'), + 'rest_framework_jwt.views.blacklist_jwt_token'), ) orig_datetime = datetime @@ -301,7 +301,29 @@ def test_verify_jwt_fails_with_blacklisted_token(self): """ Test that a blacklisted token will fail. """ - pass + api_settings.JWT_ENABLE_BLACKLIST = True + + client = APIClient(enforce_csrf_checks=True) + + user = User.objects.create_user( + email='jsmith@example.com', username='jsmith', password='password') + + token = self.create_token(user) + + # Handle blacklisting the token. + response = client.post('/auth-token-blacklist/', {'token': token}, + format='json') + + msg = 'Token successfully blacklisted.' + + self.assertEqual(response.data['message'], msg) + + response = client.post('/auth-token-verify/', {'token': token}, + format='json') + + msg = 'Token is blacklisted.' + + self.assertEqual(response.data['detail'], msg) class RefreshJSONWebTokenTests(TokenTestCase): @@ -366,3 +388,42 @@ def test_refresh_jwt_after_refresh_expiration(self): def tearDown(self): # Restore original settings api_settings.JWT_ALLOW_REFRESH = DEFAULTS['JWT_ALLOW_REFRESH'] + + +class BlacklistJSONWebTokenTests(TokenTestCase): + + def test_blacklist_jwt_successful_blacklist_enabled(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + client = APIClient(enforce_csrf_checks=True) + + user = User.objects.create_user( + email='jsmith@example.com', username='jsmith', password='password') + + token = self.create_token(user) + + # Handle blacklisting the token. + response = client.post('/auth-token-blacklist/', {'token': token}, + format='json') + + msg = 'Token successfully blacklisted.' + + self.assertEqual(response.data['message'], msg) + + def test_blacklist_jwt_fails_blacklist_disabled(self): + api_settings.JWT_ENABLE_BLACKLIST = False + + client = APIClient(enforce_csrf_checks=True) + + user = User.objects.create_user( + email='jsmith@example.com', username='jsmith', password='password') + + token = self.create_token(user) + + # Handle blacklisting the token. + response = client.post('/auth-token-blacklist/', {'token': token}, + format='json') + + msg = 'JWT_ENABLE_BLACKLIST is set to False.' + + self.assertEqual(response.data['non_field_errors'][0], msg) From 7df1149ceee1d599c03da4735b7f91d958f72859 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 31 Mar 2015 23:38:57 -0700 Subject: [PATCH 35/93] Updated documentation and did another pass through cleanup --- docs/index.md | 81 +++++++++++++++++++++++++++++++ rest_framework_jwt/admin.py | 4 +- rest_framework_jwt/models.py | 2 +- rest_framework_jwt/serializers.py | 4 +- rest_framework_jwt/utils.py | 8 +-- tests/test_authentication.py | 4 +- tests/test_utils.py | 4 +- tests/test_views.py | 1 + 8 files changed, 95 insertions(+), 13 deletions(-) diff --git a/docs/index.md b/docs/index.md index b5300ec4..8b0911d5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -101,6 +101,7 @@ 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. + ## 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. @@ -116,6 +117,28 @@ Passing a token to the verification endpoint will return a 200 response and the $ curl -X POST -H "Content-Type: application/json" -d '{"token":""}' http://localhost:8000/api-token-verify/ ``` +## Blacklist Token +If `JWT_ENABLE_BLACKLIST` is True, tokens can be made invalid (prior to expiration) by blacklisting them. More information on the concept of JTI (JWT ID) and blacklisting tokens can be read [here](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#jtiDef) and [here](https://auth0.com/blog/2015/03/10/blacklist-json-web-token-api-keys/). + +This package comes with a default implementation that stores the blacklisted tokens in the configured django database and includes an admin integration. + +To use this feature, add a URL like so: + +```python + url(r'^api-token-blacklist/', 'rest_framework_jwt.views.blacklist_jwt_token'), +``` + +Now to blacklist a token, send a POST request with a non-expired token to the blacklist endpoint: + +```bash +$ curl -X POST -H "Content-Type: application/json" -d '{"token":""}' http://localhost:8000/api-token-blacklist/ +``` + +If the blacklisting was successful, the response will contain the default implementation response data which is the token and a success message. + +The typical use case for this feature is forcefully logging a user out due to inactivity. Many applications, especially ones with sensitive information, may implement an activity-based countdown timer and wish to instantly inactivate the user's auth token. The default implementation stores a record in the database with the unique JTI (JWT ID). However there are configurable handlers for getting and setting the blacklisted token which leaves it up to the user to decide how or where they are stored. The only requirement are that both `JWT_BLACKLIST_GET_HANDLER` and `JWT_BLACKLIST_SET_HANDLER` return a valid blacklisted token or None. + + ## Additional Settings There are some additional settings that you can override similar to how you'd do it with Django REST framework itself. Here are all the available defaults. @@ -136,6 +159,15 @@ JWT_AUTH = { 'JWT_RESPONSE_PAYLOAD_HANDLER': 'rest_framework_jwt.utils.jwt_response_payload_handler', + 'JWT_BLACKLIST_GET_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_get_handler', + + 'JWT_BLACKLIST_SET_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_set_handler', + + 'JWT_BLACKLIST_RESPONSE_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_response_handler', + 'JWT_SECRET_KEY': settings.SECRET_KEY, 'JWT_ALGORITHM': 'HS256', 'JWT_VERIFY': True, @@ -149,6 +181,8 @@ JWT_AUTH = { 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), 'JWT_AUTH_HEADER_PREFIX': 'JWT', + + 'JWT_ENABLE_BLACKLIST': False, } ``` This packages uses the JSON Web Token Python implementation, [PyJWT](https://github.com/jpadilla/pyjwt) and allows to modify some of it's available options. @@ -227,6 +261,53 @@ def jwt_response_payload_handler(token, user=None, request=None): Default is `{'token': token}` +### JWT_BLACKLIST_GET_HANDLER +Responsible for fetching a blacklisted JWT token. This function should return either a valid blacklisted token or None. + +The default included implementation is as follows: +``` +def jwt_blacklist_get_handler(payload): + jti = payload.get('jti') + + try: + token = models.JWTBlackListToken.objects.get(jti=jti) + except models.JWTBlackListToken.DoesNotExist: + return None + else: + return token +``` + +### JWT_BLACKLIST_SET_HANDLER +Responsible for setting a blacklisted JWT token. This function should return either a valid blacklisted token or None. + +The default included implementation is as follows: +``` +def jwt_blacklist_set_handler(payload): + try: + data = { + 'jti': payload.get('jti'), + 'created': now(), + 'expires': datetime.fromtimestamp(payload.get('exp')) + } + return models.JWTBlackListToken.objects.create(\**data) + except (TypeError, IntegrityError, Exception): + return None +``` + +### JWT_BLACKLIST_RESPONSE_HANDLER +Controls what the response data for a request to the JWT blacklist endpoint returns. + +The default implementation is as follows: +``` +def jwt_blacklist_response_handler(token, user=None, request=None): + from . import serializers + + return { + 'token': serializers.JWTBlackListTokenSerializer(token).data, + 'message': _('Token successfully blacklisted.') + } +``` + ### JWT_AUTH_HEADER_PREFIX You can modify the Authorization header value prefix that is required to be sent together with the token. The default value is `JWT`. This decision was introduced in PR [#4](https://github.com/GetBlimp/django-rest-framework-jwt/pull/4) to allow using both this package and OAuth2 in DRF. diff --git a/rest_framework_jwt/admin.py b/rest_framework_jwt/admin.py index 6aaa9653..cf41026b 100644 --- a/rest_framework_jwt/admin.py +++ b/rest_framework_jwt/admin.py @@ -5,7 +5,7 @@ from . import models -class JWTBlackListTokenAdmin(admin.ModelAdmin): +class JWTBlacklistTokenAdmin(admin.ModelAdmin): list_display = ('jti', 'expires', 'created', 'is_expired') fields = ('jti', 'expires', 'created', 'is_expired') readonly_fields = ('jti', 'expires', 'created', 'is_expired') @@ -15,4 +15,4 @@ def is_expired(self, obj): is_expired.boolean = True if api_settings.JWT_ENABLE_BLACKLIST: - admin.site.register(models.JWTBlackListToken, JWTBlackListTokenAdmin) + admin.site.register(models.JWTBlacklistToken, JWTBlacklistTokenAdmin) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 3e4de5f3..105161ce 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -7,7 +7,7 @@ UUIDField = get_uuid_field() -class JWTBlackListToken(models.Model): +class JWTBlacklistToken(models.Model): jti = UUIDField() expires = models.DateTimeField() created = models.DateTimeField(auto_now_add=True) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 4a84fdf4..aeec2871 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -226,11 +226,11 @@ def validate(self, attrs): } -class JWTBlackListTokenSerializer(serializers.ModelSerializer): +class JWTBlacklistTokenSerializer(serializers.ModelSerializer): jti = serializers.SerializerMethodField('get_jti_value') class Meta: - model = models.JWTBlackListToken + model = models.JWTBlacklistToken def get_jti_value(self, obj): """Returns obj.jti manually due to py3 bug in django-uuidfield""" diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index b8982e01..772b79c1 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -96,8 +96,8 @@ def jwt_blacklist_get_handler(payload): jti = payload.get('jti') try: - token = models.JWTBlackListToken.objects.get(jti=jti) - except models.JWTBlackListToken.DoesNotExist: + token = models.JWTBlacklistToken.objects.get(jti=jti) + except models.JWTBlacklistToken.DoesNotExist: return None else: return token @@ -115,7 +115,7 @@ def jwt_blacklist_set_handler(payload): 'created': now(), 'expires': datetime.fromtimestamp(payload.get('exp')) } - return models.JWTBlackListToken.objects.create(**data) + return models.JWTBlacklistToken.objects.create(**data) except (TypeError, IntegrityError, Exception): return None @@ -128,6 +128,6 @@ def jwt_blacklist_response_handler(token, user=None, request=None): from . import serializers return { - 'token': serializers.JWTBlackListTokenSerializer(token).data, + 'token': serializers.JWTBlacklistTokenSerializer(token).data, 'message': _('Token successfully blacklisted.') } diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 6d1e8053..051ffc4a 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -10,7 +10,7 @@ from rest_framework.views import APIView from rest_framework_jwt import utils -from rest_framework_jwt.models import JWTBlackListToken +from rest_framework_jwt.models import JWTBlacklistToken from rest_framework_jwt.settings import api_settings, DEFAULTS from rest_framework_jwt.authentication import JSONWebTokenAuthentication @@ -94,7 +94,7 @@ def test_post_blacklisted_token_failing_jwt_auth(self): token = utils.jwt_encode_handler(payload) # Create blacklist token which effectively blacklists the token. - JWTBlackListToken.objects.create(jti=payload.get('jti'), + JWTBlacklistToken.objects.create(jti=payload.get('jti'), created=now(), expires=now()) auth = 'JWT {0}'.format(token) diff --git a/tests/test_utils.py b/tests/test_utils.py index c19d91cf..33990f3c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,7 +8,7 @@ from django.test import TestCase from rest_framework_jwt import utils -from rest_framework_jwt.models import JWTBlackListToken +from rest_framework_jwt.models import JWTBlacklistToken from rest_framework_jwt.settings import api_settings, DEFAULTS User = get_user_model() @@ -67,7 +67,7 @@ def test_jwt_blacklist_get_success(self): payload = utils.jwt_payload_handler(self.user) # Create blacklisted token. - token_created = JWTBlackListToken.objects.create( + token_created = JWTBlacklistToken.objects.create( jti=payload.get('jti'), expires=now(), created=now() diff --git a/tests/test_views.py b/tests/test_views.py index 06940859..15d6c59e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,4 +1,5 @@ import time + from calendar import timegm from datetime import datetime, timedelta From d0d1626cb4055bd862960912c6c3996ec4fb2df5 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 31 Mar 2015 23:50:52 -0700 Subject: [PATCH 36/93] Proofread docs --- docs/index.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 8b0911d5..0af92b5d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -134,9 +134,9 @@ Now to blacklist a token, send a POST request with a non-expired token to the bl $ curl -X POST -H "Content-Type: application/json" -d '{"token":""}' http://localhost:8000/api-token-blacklist/ ``` -If the blacklisting was successful, the response will contain the default implementation response data which is the token and a success message. +If the blacklisting was successful, the response will contain the default implementation response data which is the token and a success message. Any future requests using that token will be denied. -The typical use case for this feature is forcefully logging a user out due to inactivity. Many applications, especially ones with sensitive information, may implement an activity-based countdown timer and wish to instantly inactivate the user's auth token. The default implementation stores a record in the database with the unique JTI (JWT ID). However there are configurable handlers for getting and setting the blacklisted token which leaves it up to the user to decide how or where they are stored. The only requirement are that both `JWT_BLACKLIST_GET_HANDLER` and `JWT_BLACKLIST_SET_HANDLER` return a valid blacklisted token or None. +The typical use case for this feature is forcefully logging a user out due to inactivity. Many applications, especially ones with sensitive information, may implement an activity-based countdown timer and wish to instantly inactivate the user's auth token. The default implementation stores a record in the database with the unique JTI (JWT ID). However there are configurable handlers for getting and setting the blacklisted token which leaves it up to the user to decide how or where they are stored. The only requirements are that both `JWT_BLACKLIST_GET_HANDLER` and `JWT_BLACKLIST_SET_HANDLER` return a valid blacklisted token or None. ## Additional Settings @@ -242,6 +242,9 @@ Default is `datetime.timedelta(days=7)` (7 days). ### JWT_PAYLOAD_HANDLER Specify a custom function to generate the token payload +**Note** +If you have `JWT_ENABLE_BLACKLIST` set to True, *AND* you are using the default blacklist implementation, any custom token payload must include both a `jti` attribute which is a unique UUID hex, and a `exp` attribute which is a timestamp from a POSIX time (e.g. seconds since epoch). + ### JWT_PAYLOAD_GET_USER_ID_HANDLER If you store `user_id` differently than the default payload handler does, implement this function to fetch `user_id` from the payload. From 6ecdf7f742b4ec7aefc90d03778670cdf3b2a9af Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Wed, 1 Apr 2015 10:09:20 -0700 Subject: [PATCH 37/93] Update index.md --- docs/index.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index 0af92b5d..aba3d303 100644 --- a/docs/index.md +++ b/docs/index.md @@ -271,7 +271,6 @@ The default included implementation is as follows: ``` def jwt_blacklist_get_handler(payload): jti = payload.get('jti') - try: token = models.JWTBlackListToken.objects.get(jti=jti) except models.JWTBlackListToken.DoesNotExist: @@ -292,7 +291,7 @@ def jwt_blacklist_set_handler(payload): 'created': now(), 'expires': datetime.fromtimestamp(payload.get('exp')) } - return models.JWTBlackListToken.objects.create(\**data) + return models.JWTBlackListToken.objects.create(**data) except (TypeError, IntegrityError, Exception): return None ``` @@ -303,11 +302,9 @@ Controls what the response data for a request to the JWT blacklist endpoint retu The default implementation is as follows: ``` def jwt_blacklist_response_handler(token, user=None, request=None): - from . import serializers - return { - 'token': serializers.JWTBlackListTokenSerializer(token).data, - 'message': _('Token successfully blacklisted.') + 'token': JWTBlackListTokenSerializer(token).data, + 'message': 'Token successfully blacklisted.' } ``` From e7c3d22a07ae8e0003703eee3b26a23a7437e772 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Wed, 1 Apr 2015 10:10:56 -0700 Subject: [PATCH 38/93] Update index.md --- docs/index.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/index.md b/docs/index.md index aba3d303..7a6aa526 100644 --- a/docs/index.md +++ b/docs/index.md @@ -308,6 +308,11 @@ def jwt_blacklist_response_handler(token, user=None, request=None): } ``` +### JWT_ENABLE_BLACKLIST +Designates whether JWT token blacklisting is turned on or off. + +Defaults to False. + ### JWT_AUTH_HEADER_PREFIX You can modify the Authorization header value prefix that is required to be sent together with the token. The default value is `JWT`. This decision was introduced in PR [#4](https://github.com/GetBlimp/django-rest-framework-jwt/pull/4) to allow using both this package and OAuth2 in DRF. From 10ab7e05709e314b53a051b2b9b3ae71f2677f3b Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Wed, 1 Apr 2015 10:23:20 -0700 Subject: [PATCH 39/93] Changed model method to is_active --- rest_framework_jwt/admin.py | 12 ++++++------ rest_framework_jwt/models.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rest_framework_jwt/admin.py b/rest_framework_jwt/admin.py index cf41026b..9a41e408 100644 --- a/rest_framework_jwt/admin.py +++ b/rest_framework_jwt/admin.py @@ -6,13 +6,13 @@ class JWTBlacklistTokenAdmin(admin.ModelAdmin): - list_display = ('jti', 'expires', 'created', 'is_expired') - fields = ('jti', 'expires', 'created', 'is_expired') - readonly_fields = ('jti', 'expires', 'created', 'is_expired') + list_display = ('jti', 'expires', 'created', 'is_active') + fields = ('jti', 'expires', 'created', 'is_active') + readonly_fields = ('jti', 'expires', 'created', 'is_active') - def is_expired(self, obj): - return obj.is_expired() - is_expired.boolean = True + def is_active(self, obj): + return obj.is_active() + is_active.boolean = True if api_settings.JWT_ENABLE_BLACKLIST: admin.site.register(models.JWTBlacklistToken, JWTBlacklistTokenAdmin) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 105161ce..9e6e0223 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -16,5 +16,5 @@ class Meta: verbose_name = _('JWT Blacklist Token') verbose_name_plural = _('JWT Blacklist Tokens') - def is_expired(self): - return self.expires < now() + def is_active(self): + return self.expires > now() From 00dbd12fc24544d77cc96c469f65ca2b7ffe5b25 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Wed, 1 Apr 2015 10:23:59 -0700 Subject: [PATCH 40/93] Changed model method to is_active --- rest_framework_jwt/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework_jwt/admin.py b/rest_framework_jwt/admin.py index 9a41e408..968519d0 100644 --- a/rest_framework_jwt/admin.py +++ b/rest_framework_jwt/admin.py @@ -13,6 +13,7 @@ class JWTBlacklistTokenAdmin(admin.ModelAdmin): def is_active(self, obj): return obj.is_active() is_active.boolean = True + is_active.short_description = 'Active' if api_settings.JWT_ENABLE_BLACKLIST: admin.site.register(models.JWTBlacklistToken, JWTBlacklistTokenAdmin) From ef176c0819989f3fb00658d515723e68d1d43ef5 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 3 Apr 2015 14:33:38 -0700 Subject: [PATCH 41/93] Separated blacklist token feature into subapp --- docs/index.md | 4 +++- rest_framework_jwt/blacklist/__init__.py | 0 rest_framework_jwt/{ => blacklist}/admin.py | 0 rest_framework_jwt/{ => blacklist}/models.py | 2 +- rest_framework_jwt/blacklist/serializers.py | 14 ++++++++++++++ rest_framework_jwt/serializers.py | 13 ------------- rest_framework_jwt/utils.py | 6 ++---- tests/conftest.py | 1 + tests/test_authentication.py | 2 +- tests/test_utils.py | 2 +- 10 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 rest_framework_jwt/blacklist/__init__.py rename rest_framework_jwt/{ => blacklist}/admin.py (100%) rename rest_framework_jwt/{ => blacklist}/models.py (90%) create mode 100644 rest_framework_jwt/blacklist/serializers.py diff --git a/docs/index.md b/docs/index.md index 7a6aa526..5cdafd01 100644 --- a/docs/index.md +++ b/docs/index.md @@ -118,7 +118,7 @@ $ curl -X POST -H "Content-Type: application/json" -d '{"token":" Date: Fri, 3 Apr 2015 17:05:03 -0700 Subject: [PATCH 42/93] Separated blacklist view and utils into the subapp, updated docs --- docs/index.md | 2 +- rest_framework_jwt/blacklist/serializers.py | 43 ++++++++++++++--- rest_framework_jwt/blacklist/utils.py | 52 +++++++++++++++++++++ rest_framework_jwt/blacklist/views.py | 19 ++++++++ rest_framework_jwt/serializers.py | 31 ------------ rest_framework_jwt/settings.py | 6 +-- rest_framework_jwt/utils.py | 50 -------------------- rest_framework_jwt/views.py | 10 ---- tests/test_serializers.py | 2 +- tests/test_utils.py | 9 ++-- tests/test_views.py | 2 +- 11 files changed, 119 insertions(+), 107 deletions(-) create mode 100644 rest_framework_jwt/blacklist/utils.py create mode 100644 rest_framework_jwt/blacklist/views.py diff --git a/docs/index.md b/docs/index.md index 5cdafd01..6ab93d99 100644 --- a/docs/index.md +++ b/docs/index.md @@ -139,7 +139,7 @@ If the blacklisting was successful, the response will contain the default implem The typical use case for this feature is forcefully logging a user out due to inactivity. Many applications, especially ones with sensitive information, may implement an activity-based countdown timer and wish to instantly inactivate the user's auth token. The default implementation stores a record in the database with the unique JTI (JWT ID). However there are configurable handlers for getting and setting the blacklisted token which leaves it up to the user to decide how or where they are stored. The only requirements are that both `JWT_BLACKLIST_GET_HANDLER` and `JWT_BLACKLIST_SET_HANDLER` return a valid blacklisted token or None. **Note** -Applications that are built with a Serivce-Oriented-Architecture (SOA) may not be able to use the blacklist token feature. +Applications that are built with a Serivce-Oriented-Architecture (SOA) may not be able to use the blacklist token feature due to having to query the auth service on every request to check if the JWT is blacklisted. ## Additional Settings There are some additional settings that you can override similar to how you'd do it with Django REST framework itself. Here are all the available defaults. diff --git a/rest_framework_jwt/blacklist/serializers.py b/rest_framework_jwt/blacklist/serializers.py index 78892886..34a3666f 100644 --- a/rest_framework_jwt/blacklist/serializers.py +++ b/rest_framework_jwt/blacklist/serializers.py @@ -1,14 +1,45 @@ +from django.utils.translation import ugettext as _ + from rest_framework import serializers +from rest_framework_jwt.settings import api_settings +from rest_framework_jwt.serializers import VerificationBaseSerializer + from . import models +jwt_blacklist_set_handler = api_settings.JWT_BLACKLIST_SET_HANDLER -class JWTBlacklistTokenSerializer(serializers.ModelSerializer): - jti = serializers.SerializerMethodField('get_jti_value') +class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): + """ + Blacklist an access token. + """ + + def validate(self, attrs): + + token = attrs['token'] + + if not api_settings.JWT_ENABLE_BLACKLIST: + msg = _('JWT_ENABLE_BLACKLIST is set to False.') + raise serializers.ValidationError(msg) + + payload = self._check_payload(token=token) + + # Handle blacklisting a token. + token = jwt_blacklist_set_handler(payload) + + if not token: + msg = _('Could not blacklist token.') + raise serializers.ValidationError(msg) + + user = self._check_user(payload=payload) + + return { + 'token': token, + 'user': user + } + + +class JWTBlacklistTokenSerializer(serializers.ModelSerializer): class Meta: model = models.JWTBlacklistToken - - def get_jti_value(self, obj): - """Returns obj.jti manually due to py3 bug in django-uuidfield""" - return obj.jti diff --git a/rest_framework_jwt/blacklist/utils.py b/rest_framework_jwt/blacklist/utils.py new file mode 100644 index 00000000..bbe7c387 --- /dev/null +++ b/rest_framework_jwt/blacklist/utils.py @@ -0,0 +1,52 @@ +from datetime import datetime + +from django.db import IntegrityError +from django.utils.timezone import now +from django.utils.translation import ugettext_lazy as _ + +from . import models + + +def jwt_blacklist_get_handler(payload): + """ + Default implementation to check if a blacklisted jwt token exists. + + Should return a black listed token or None. + """ + jti = payload.get('jti') + + try: + token = models.JWTBlacklistToken.objects.get(jti=jti) + except models.JWTBlacklistToken.DoesNotExist: + return None + else: + return token + + +def jwt_blacklist_set_handler(payload): + """ + Default implementation that blacklists a jwt token. + + Should return a black listed token or None. + """ + try: + data = { + 'jti': payload.get('jti'), + 'created': now(), + 'expires': datetime.fromtimestamp(payload.get('exp')) + } + return models.JWTBlacklistToken.objects.create(**data) + except (TypeError, IntegrityError, Exception): + return None + + +def jwt_blacklist_response_handler(token, user=None, request=None): + """ + Default blacklist token response data. Override to provide a + custom response. + """ + from . import serializers + return { + 'token': serializers.JWTBlacklistTokenSerializer(token).data, + 'message': _('Token successfully blacklisted.') + } diff --git a/rest_framework_jwt/blacklist/views.py b/rest_framework_jwt/blacklist/views.py new file mode 100644 index 00000000..d5c75f81 --- /dev/null +++ b/rest_framework_jwt/blacklist/views.py @@ -0,0 +1,19 @@ +from rest_framework_jwt.settings import api_settings +from rest_framework_jwt.views import JSONWebTokenAPIView + +from . import serializers + +jwt_blacklist_response_handler = api_settings.JWT_BLACKLIST_RESPONSE_HANDLER + + +class BlacklistJSONWebToken(JSONWebTokenAPIView): + """ + API View that blacklists a token + """ + serializer_class = serializers.BlacklistJSONWebTokenSerializer + response_payload_handler = staticmethod( + jwt_blacklist_response_handler + ) + + +blacklist_jwt_token = BlacklistJSONWebToken.as_view() diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 79890579..ecb33b2b 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -18,7 +18,6 @@ jwt_decode_handler = api_settings.JWT_DECODE_HANDLER jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER jwt_blacklist_get_handler = api_settings.JWT_BLACKLIST_GET_HANDLER -jwt_blacklist_set_handler = api_settings.JWT_BLACKLIST_SET_HANDLER class JSONWebTokenSerializer(Serializer): @@ -192,33 +191,3 @@ def validate(self, attrs): 'token': jwt_encode_handler(new_payload), 'user': user } - - -class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): - """ - Blacklist an access token. - """ - - def validate(self, attrs): - - token = attrs['token'] - - if not api_settings.JWT_ENABLE_BLACKLIST: - msg = _('JWT_ENABLE_BLACKLIST is set to False.') - raise serializers.ValidationError(msg) - - payload = self._check_payload(token=token) - - # Handle blacklisting a token. - token = jwt_blacklist_set_handler(payload) - - if not token: - msg = _('Could not blacklist token.') - raise serializers.ValidationError(msg) - - user = self._check_user(payload=payload) - - return { - 'token': token, - 'user': user - } diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index c9ec568c..67729bb6 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -23,13 +23,13 @@ 'rest_framework_jwt.utils.jwt_response_payload_handler', 'JWT_BLACKLIST_GET_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_get_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_get_handler', 'JWT_BLACKLIST_SET_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_set_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_set_handler', 'JWT_BLACKLIST_RESPONSE_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_response_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_response_handler', 'JWT_SECRET_KEY': settings.SECRET_KEY, 'JWT_ALGORITHM': 'HS256', diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index c7ae8748..04894f81 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,15 +1,9 @@ import jwt import uuid -from django.db import IntegrityError -from django.utils.timezone import now -from django.utils.translation import ugettext_lazy as _ - from datetime import datetime from rest_framework_jwt.settings import api_settings -from rest_framework_jwt.blacklist import models -from rest_framework_jwt.blacklist import serializers def get_user_model(): @@ -85,47 +79,3 @@ def jwt_response_payload_handler(token, user=None, request=None): return { 'token': token } - - -def jwt_blacklist_get_handler(payload): - """ - Default implementation to check if a blacklisted jwt token exists. - - Should return a black listed token or None. - """ - jti = payload.get('jti') - - try: - token = models.JWTBlacklistToken.objects.get(jti=jti) - except models.JWTBlacklistToken.DoesNotExist: - return None - else: - return token - - -def jwt_blacklist_set_handler(payload): - """ - Default implementation that blacklists a jwt token. - - Should return a black listed token or None. - """ - try: - data = { - 'jti': payload.get('jti'), - 'created': now(), - 'expires': datetime.fromtimestamp(payload.get('exp')) - } - return models.JWTBlacklistToken.objects.create(**data) - except (TypeError, IntegrityError, Exception): - return None - - -def jwt_blacklist_response_handler(token, user=None, request=None): - """ - Default blacklist token response data. Override to provide a - custom response. - """ - return { - 'token': serializers.JWTBlacklistTokenSerializer(token).data, - 'message': _('Token successfully blacklisted.') - } diff --git a/rest_framework_jwt/views.py b/rest_framework_jwt/views.py index a067091e..ed8c09ee 100644 --- a/rest_framework_jwt/views.py +++ b/rest_framework_jwt/views.py @@ -9,7 +9,6 @@ from . import serializers jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER -jwt_blacklist_response_handler = api_settings.JWT_BLACKLIST_RESPONSE_HANDLER class JSONWebTokenAPIView(APIView): @@ -64,15 +63,6 @@ class RefreshJSONWebToken(JSONWebTokenAPIView): serializer_class = serializers.RefreshJSONWebTokenSerializer -class BlacklistJSONWebToken(JSONWebTokenAPIView): - """ - API View that blacklists a token - """ - serializer_class = serializers.BlacklistJSONWebTokenSerializer - response_payload_handler = staticmethod(jwt_blacklist_response_handler) - - obtain_jwt_token = ObtainJSONWebToken.as_view() refresh_jwt_token = RefreshJSONWebToken.as_view() verify_jwt_token = VerifyJSONWebToken.as_view() -blacklist_jwt_token = BlacklistJSONWebToken.as_view() diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 8df52c08..07b386b4 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -7,7 +7,7 @@ from rest_framework_jwt.settings import api_settings from rest_framework_jwt.serializers import JSONWebTokenSerializer -from rest_framework_jwt.serializers import BlacklistJSONWebTokenSerializer +from rest_framework_jwt.blacklist.serializers import BlacklistJSONWebTokenSerializer from rest_framework_jwt import utils User = get_user_model() diff --git a/tests/test_utils.py b/tests/test_utils.py index 31377608..4a9324db 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -9,6 +9,7 @@ from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings, DEFAULTS +from rest_framework_jwt.blacklist import utils as blacklist_utils from rest_framework_jwt.blacklist.models import JWTBlacklistToken User = get_user_model() @@ -73,7 +74,7 @@ def test_jwt_blacklist_get_success(self): created=now() ) - token_fetched = utils.jwt_blacklist_get_handler(payload) + token_fetched = blacklist_utils.jwt_blacklist_get_handler(payload) self.assertEqual(token_created.jti, token_fetched.jti) @@ -85,7 +86,7 @@ def test_jwt_blacklist_get_fail(self): # Test that incoming empty jti fails. payload['jti'] = None - token_fetched = utils.jwt_blacklist_get_handler(payload) + token_fetched = blacklist_utils.jwt_blacklist_get_handler(payload) self.assertIsNone(token_fetched) @@ -98,7 +99,7 @@ def test_jwt_blacklist_set_success(self): payload['exp'] = int(time.time()) # Create blacklisted token. - token = utils.jwt_blacklist_set_handler(payload) + token = blacklist_utils.jwt_blacklist_set_handler(payload) self.assertEqual(token.jti, payload.get('jti')) @@ -108,7 +109,7 @@ def test_jwt_blacklist_set_fail(self): payload = utils.jwt_payload_handler(self.user) # Create blacklisted token. - token = utils.jwt_blacklist_set_handler(payload) + token = blacklist_utils.jwt_blacklist_set_handler(payload) self.assertIsNone(token) diff --git a/tests/test_views.py b/tests/test_views.py index 15d6c59e..83b4fd70 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -28,7 +28,7 @@ (r'^auth-token-refresh/$', 'rest_framework_jwt.views.refresh_jwt_token'), (r'^auth-token-verify/$', 'rest_framework_jwt.views.verify_jwt_token'), (r'^auth-token-blacklist/$', - 'rest_framework_jwt.views.blacklist_jwt_token'), + 'rest_framework_jwt.blacklist.views.blacklist_jwt_token'), ) orig_datetime = datetime From 4bff8e32f698e8cd76a5183615baf46562c19003 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 4 Apr 2015 10:22:01 -0700 Subject: [PATCH 43/93] Removed JWT_ENABLE_BLACKLIST - tests broken --- rest_framework_jwt/authentication.py | 3 ++- rest_framework_jwt/blacklist/serializers.py | 5 +++-- rest_framework_jwt/serializers.py | 3 ++- rest_framework_jwt/settings.py | 1 - tests/test_authentication.py | 7 ------- tests/test_serializers.py | 4 ---- tests/test_utils.py | 8 -------- tests/test_views.py | 7 ++----- 8 files changed, 9 insertions(+), 29 deletions(-) diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index 530a18ed..4d4b9adf 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -1,5 +1,6 @@ import jwt +from django.conf import settings from django.utils.encoding import smart_text from django.utils.translation import ugettext as _ @@ -40,7 +41,7 @@ def authenticate(self, request): raise exceptions.AuthenticationFailed(msg) # Check if the token has been blacklisted. - if api_settings.JWT_ENABLE_BLACKLIST: + if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS: blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: diff --git a/rest_framework_jwt/blacklist/serializers.py b/rest_framework_jwt/blacklist/serializers.py index 34a3666f..a58968b9 100644 --- a/rest_framework_jwt/blacklist/serializers.py +++ b/rest_framework_jwt/blacklist/serializers.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.utils.translation import ugettext as _ from rest_framework import serializers @@ -19,8 +20,8 @@ def validate(self, attrs): token = attrs['token'] - if not api_settings.JWT_ENABLE_BLACKLIST: - msg = _('JWT_ENABLE_BLACKLIST is set to False.') + if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS: + msg = _('The blacklist app is not installed.') raise serializers.ValidationError(msg) payload = self._check_payload(token=token) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index ecb33b2b..ca3467eb 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -3,6 +3,7 @@ from calendar import timegm from datetime import datetime, timedelta +from django.conf import settings from django.contrib.auth import authenticate from django.utils.translation import ugettext as _ @@ -106,7 +107,7 @@ def _check_payload(self, token): raise serializers.ValidationError(msg) # Check if the token has been blacklisted. - if api_settings.JWT_ENABLE_BLACKLIST: + if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS: blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index 67729bb6..e13f7128 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -42,7 +42,6 @@ 'JWT_ALLOW_REFRESH': False, 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), 'JWT_AUTH_HEADER_PREFIX': 'JWT', - 'JWT_ENABLE_BLACKLIST': False, } # List of settings that may be in string import notation. diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 1c62bdd4..3d87386e 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -71,7 +71,6 @@ def test_post_json_passing_jwt_auth_blacklist_enabled(self): Ensure POSTing JSON over JWT auth with correct credentials passes and does not require CSRF """ - api_settings.JWT_ENABLE_BLACKLIST = True payload = utils.jwt_payload_handler(self.user) token = utils.jwt_encode_handler(payload) @@ -82,14 +81,10 @@ def test_post_json_passing_jwt_auth_blacklist_enabled(self): self.assertEqual(response.status_code, status.HTTP_200_OK) - api_settings.JWT_ENABLE_BLACKLIST = False - def test_post_blacklisted_token_failing_jwt_auth(self): """ Ensure POSTing over JWT auth with blacklisted token fails """ - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) token = utils.jwt_encode_handler(payload) @@ -108,8 +103,6 @@ def test_post_blacklisted_token_failing_jwt_auth(self): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response['WWW-Authenticate'], 'JWT realm="api"') - api_settings.JWT_ENABLE_BLACKLIST = False - def test_post_form_passing_jwt_auth(self): """ Ensure POSTing form over JWT auth with correct credentials diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 07b386b4..cb63885c 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -112,8 +112,6 @@ def setUp(self): } def test_token_blacklisted(self): - api_settings.JWT_ENABLE_BLACKLIST = True - serializer = BlacklistJSONWebTokenSerializer(data=self.data) is_valid = serializer.is_valid() @@ -123,8 +121,6 @@ def test_token_blacklisted(self): self.assertEqual(self.payload.get('jti'), token.jti) def test_token_blacklist_fail_missing_jti(self): - api_settings.JWT_ENABLE_BLACKLIST = True - self.payload['jti'] = None self.data = { 'token': utils.jwt_encode_handler(self.payload) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4a9324db..c56d313c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -63,8 +63,6 @@ def test_jwt_response_payload(self): self.assertEqual(response_data, dict(token=token)) def test_jwt_blacklist_get_success(self): - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) # Create blacklisted token. @@ -79,8 +77,6 @@ def test_jwt_blacklist_get_success(self): self.assertEqual(token_created.jti, token_fetched.jti) def test_jwt_blacklist_get_fail(self): - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) # Test that incoming empty jti fails. @@ -91,8 +87,6 @@ def test_jwt_blacklist_get_fail(self): self.assertIsNone(token_fetched) def test_jwt_blacklist_set_success(self): - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) # exp field comes in as seconds since epoch @@ -104,8 +98,6 @@ def test_jwt_blacklist_set_success(self): self.assertEqual(token.jti, payload.get('jti')) def test_jwt_blacklist_set_fail(self): - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) # Create blacklisted token. diff --git a/tests/test_views.py b/tests/test_views.py index 83b4fd70..eaed074f 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from django import get_version +from django.conf import settings from django.test import TestCase from django.test.utils import override_settings from django.utils import unittest @@ -302,8 +303,6 @@ def test_verify_jwt_fails_with_blacklisted_token(self): """ Test that a blacklisted token will fail. """ - api_settings.JWT_ENABLE_BLACKLIST = True - client = APIClient(enforce_csrf_checks=True) user = User.objects.create_user( @@ -394,8 +393,6 @@ def tearDown(self): class BlacklistJSONWebTokenTests(TokenTestCase): def test_blacklist_jwt_successful_blacklist_enabled(self): - api_settings.JWT_ENABLE_BLACKLIST = True - client = APIClient(enforce_csrf_checks=True) user = User.objects.create_user( @@ -425,6 +422,6 @@ def test_blacklist_jwt_fails_blacklist_disabled(self): response = client.post('/auth-token-blacklist/', {'token': token}, format='json') - msg = 'JWT_ENABLE_BLACKLIST is set to False.' + msg = 'The blacklist app is not installed.' self.assertEqual(response.data['non_field_errors'][0], msg) From 41e9955dc351e45ba59328e6ebc3e8911f13c033 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 5 Apr 2015 10:30:36 -0700 Subject: [PATCH 44/93] Fixed typo in blacklist serializer, removed blacklist disabled test as now it's either installed or not. --- rest_framework_jwt/blacklist/serializers.py | 3 +-- tests/test_serializers.py | 1 - tests/test_views.py | 20 +------------------- 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/rest_framework_jwt/blacklist/serializers.py b/rest_framework_jwt/blacklist/serializers.py index a58968b9..591e4e65 100644 --- a/rest_framework_jwt/blacklist/serializers.py +++ b/rest_framework_jwt/blacklist/serializers.py @@ -15,12 +15,11 @@ class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): """ Blacklist an access token. """ - def validate(self, attrs): token = attrs['token'] - if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS: + if 'rest_framework_jwt.blacklist' not in settings.INSTALLED_APPS: msg = _('The blacklist app is not installed.') raise serializers.ValidationError(msg) diff --git a/tests/test_serializers.py b/tests/test_serializers.py index cb63885c..1b673102 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -5,7 +5,6 @@ from django.utils import unittest from django.contrib.auth import get_user_model -from rest_framework_jwt.settings import api_settings from rest_framework_jwt.serializers import JSONWebTokenSerializer from rest_framework_jwt.blacklist.serializers import BlacklistJSONWebTokenSerializer from rest_framework_jwt import utils diff --git a/tests/test_views.py b/tests/test_views.py index eaed074f..b7e367c5 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -4,7 +4,6 @@ from datetime import datetime, timedelta from django import get_version -from django.conf import settings from django.test import TestCase from django.test.utils import override_settings from django.utils import unittest @@ -228,6 +227,7 @@ def create_token(self, user, exp=None, orig_iat=None): payload['orig_iat'] = timegm(orig_iat.utctimetuple()) token = utils.jwt_encode_handler(payload) + return token @@ -407,21 +407,3 @@ def test_blacklist_jwt_successful_blacklist_enabled(self): msg = 'Token successfully blacklisted.' self.assertEqual(response.data['message'], msg) - - def test_blacklist_jwt_fails_blacklist_disabled(self): - api_settings.JWT_ENABLE_BLACKLIST = False - - client = APIClient(enforce_csrf_checks=True) - - user = User.objects.create_user( - email='jsmith@example.com', username='jsmith', password='password') - - token = self.create_token(user) - - # Handle blacklisting the token. - response = client.post('/auth-token-blacklist/', {'token': token}, - format='json') - - msg = 'The blacklist app is not installed.' - - self.assertEqual(response.data['non_field_errors'][0], msg) From 8ac3f8b4c9771fcd225901b6939f5f2c1827e420 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 5 Apr 2015 17:49:41 -0700 Subject: [PATCH 45/93] Fixed merge requirements issue --- requirements.txt | 1 - tox.ini | 1 - 2 files changed, 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5e74e6a2..12c1a878 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,3 @@ flake8==2.2.2 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 -djangorestframework-oauth>=1.0.1 diff --git a/tox.ini b/tox.ini index c274c023..0916c932 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,6 @@ deps = drf2.4.4: djangorestframework==2.4.4 drf3.0.0: djangorestframework==3.0.0 drf3.1.0: djangorestframework==3.1.0 - py27: djangorestframework-oauth==1.0.1 py27-django1.6-drf{2.4.3,2.4.4,3.0.0,3.1.0}: oauth2==1.5.211 py27-django1.6-drf{2.4.3,2.4.4,3.0.0,3.1.0}: django-oauth-plus==2.2.6 py27-django1.6-drf{2.4.3,2.4.4,3.0.0,3.1.0}: django-oauth2-provider==0.2.6.1 From ad153298a609921e632b0d5866a56e5ab28f0cf1 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 5 Apr 2015 18:01:40 -0700 Subject: [PATCH 46/93] Update readme --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index dbdf34e8..3f50f084 100644 --- a/docs/index.md +++ b/docs/index.md @@ -243,7 +243,7 @@ Default is `datetime.timedelta(days=7)` (7 days). Specify a custom function to generate the token payload **Note** -If you have `JWT_ENABLE_BLACKLIST` set to True, *AND* you are using the default blacklist implementation, any custom token payload must include both a `jti` attribute which is a unique UUID hex, and a `exp` attribute which is a timestamp from a POSIX time (e.g. seconds since epoch). +If you have `rest_framework_jwt.blacklist` added to `INSTALLED_APPS` *AND* you are using the default blacklist implementation, any custom token payload must include both a `jti` attribute which is a unique UUID hex, and a `exp` attribute which is a timestamp from a POSIX time (e.g. seconds since epoch). ### JWT_PAYLOAD_GET_USER_ID_HANDLER If you store `user_id` differently than the default payload handler does, implement this function to fetch `user_id` from the payload. From 5fa60770ee692329d6da89ee3fb00ad15c53fbcb Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 21 Apr 2015 11:36:46 -0700 Subject: [PATCH 47/93] Updated readme and admin --- docs/index.md | 6 +++--- rest_framework_jwt/blacklist/admin.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index 3f50f084..bb986637 100644 --- a/docs/index.md +++ b/docs/index.md @@ -162,13 +162,13 @@ JWT_AUTH = { 'rest_framework_jwt.utils.jwt_response_payload_handler', 'JWT_BLACKLIST_GET_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_get_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_get_handler', 'JWT_BLACKLIST_SET_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_set_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_set_handler', 'JWT_BLACKLIST_RESPONSE_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_response_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_response_handler', 'JWT_SECRET_KEY': settings.SECRET_KEY, 'JWT_ALGORITHM': 'HS256', diff --git a/rest_framework_jwt/blacklist/admin.py b/rest_framework_jwt/blacklist/admin.py index 968519d0..5153b4d7 100644 --- a/rest_framework_jwt/blacklist/admin.py +++ b/rest_framework_jwt/blacklist/admin.py @@ -1,7 +1,6 @@ +from django.conf import settings from django.contrib import admin -from rest_framework_jwt.settings import api_settings - from . import models @@ -15,5 +14,5 @@ def is_active(self, obj): is_active.boolean = True is_active.short_description = 'Active' -if api_settings.JWT_ENABLE_BLACKLIST: +if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS: admin.site.register(models.JWTBlacklistToken, JWTBlacklistTokenAdmin) From 83451def2d4acc5fffa2af918099bf275f9a2d7c Mon Sep 17 00:00:00 2001 From: AM Date: Sun, 22 Mar 2015 19:56:56 +0100 Subject: [PATCH 48/93] Added support for jti claim --- requirements.txt | 1 + rest_framework_jwt/serializers.py | 31 +++++++++++++++++++++++++++ rest_framework_jwt/settings.py | 2 ++ rest_framework_jwt/utils.py | 35 ++++++++++++++++++++++++++++--- rest_framework_jwt/views.py | 10 ++++++++- tests/test_utils.py | 16 ++++++++++++++ tests/test_views.py | 29 ++++++++++++++++++++++--- 7 files changed, 117 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1ed7882d..ad0544d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ freezegun==0.3.2 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 +pymongo==2.8 diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index beb5ad4f..9b500420 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -103,6 +103,10 @@ def _check_payload(self, token): msg = _('Error decoding signature.') raise serializers.ValidationError(msg) + if api_settings.JWT_ENABLE_BLACKLIST and utils.jwt_is_blacklisted(payload): + msg = _("Token is blacklisted") + raise serializers.ValidationError(msg) + return payload def _check_user(self, payload): @@ -150,6 +154,7 @@ def validate(self, attrs): payload = self._check_payload(token=token) user = self._check_user(payload=payload) + # Get and check 'orig_iat' orig_iat = payload.get('orig_iat') @@ -178,3 +183,29 @@ def validate(self, attrs): 'token': jwt_encode_handler(new_payload), 'user': user } + + +class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): + """ + Blacklist an access token. + """ + + def validate(self, attrs): + if not api_settings.JWT_ENABLE_BLACKLIST: + msg = _('JWT_ENABLE_BLACKLIST is set to False.') + raise serializers.ValidationError(msg) + + token = attrs['token'] + + payload = self._check_payload(token=token) + user = self._check_user(payload=payload) + # Get and check 'jti' + jti = payload.get('jti') + + if jti: + utils.jwt_blacklist(payload) + + return { + 'token': None, + 'user': user + } diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index 178b4f21..90fd8104 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -35,6 +35,8 @@ 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), 'JWT_AUTH_HEADER_PREFIX': 'JWT', + + 'JWT_ENABLE_BLACKLIST': False, } # List of settings that may be in string import notation. diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 6b18b2cc..495d37c2 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,9 +1,16 @@ -import jwt - +import random +import string from datetime import datetime +import jwt +from django.utils.translation import gettext_lazy as _ + from rest_framework_jwt.settings import api_settings +if api_settings.JWT_ENABLE_BLACKLIST: + import pymongo + jti_collection = pymongo.MongoClient().jwt_db.jti_collection + def get_user_model(): try: @@ -22,13 +29,18 @@ def jwt_payload_handler(user): except AttributeError: username = user.username - return { + payload = { 'user_id': user.pk, 'email': user.email, 'username': username, 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA } + if 'jti' not in payload: + payload['jti'] = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(20)) + + return payload + def jwt_get_user_id_from_payload_handler(payload): """ @@ -78,6 +90,23 @@ def jwt_response_payload_handler(token, user=None, request=None): } """ + return { 'token': token } + + +def jwt_is_blacklisted(payload): + if 'jti' not in payload or api_settings.JWT_ENABLE_BLACKLIST is False: + return False + + return jti_collection.find_one({'jti': payload['jti']}) is not None + + +def jwt_blacklist(payload): + if 'jti' not in payload: + raise ValueError(_("Can't blacklist payloads that don't have a jti claim")) + + if not jwt_is_blacklisted(payload): + jti_collection.insert({'jti': payload['jti'], + 'payload': payload}) diff --git a/rest_framework_jwt/views.py b/rest_framework_jwt/views.py index e951dd51..33ee6d7e 100644 --- a/rest_framework_jwt/views.py +++ b/rest_framework_jwt/views.py @@ -8,7 +8,7 @@ from .serializers import ( JSONWebTokenSerializer, RefreshJSONWebTokenSerializer, - VerifyJSONWebTokenSerializer + VerifyJSONWebTokenSerializer, BlacklistJSONWebTokenSerializer ) jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER @@ -65,6 +65,14 @@ class RefreshJSONWebToken(JSONWebTokenAPIView): serializer_class = RefreshJSONWebTokenSerializer +class BlacklistJSONWebToken(JSONWebTokenAPIView): + """ + API View that blacklists a token + """ + serializer_class = BlacklistJSONWebTokenSerializer + + obtain_jwt_token = ObtainJSONWebToken.as_view() refresh_jwt_token = RefreshJSONWebToken.as_view() verify_jwt_token = VerifyJSONWebToken.as_view() +blacklist_jwt_token = BlacklistJSONWebToken.as_view() diff --git a/tests/test_utils.py b/tests/test_utils.py index 4927948a..b7ed56d1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -67,6 +67,22 @@ def test_jwt_decode_verify_exp(self): api_settings.JWT_VERIFY_EXPIRATION = True + def test_jti_blacklist(self): + api_settings.JWT_ENABLE_BLACKLIST = True + reload(utils) # it will now retrieve the mongo collection for the jti blacklist + payload = utils.jwt_payload_handler(self.user) + utils.jwt_blacklist(payload) + + self.assertEqual(utils.jwt_is_blacklisted(payload), True) + + def test_fail_blacklist_without_jti(self): + api_settings.JWT_ENABLE_BLACKLIST = True + payload = utils.jwt_payload_handler(self.user) + payload.pop('jti') + + with self.assertRaisesMessage(ValueError, "Can't blacklist payloads that don't have a jti claim"): + utils.jwt_blacklist(payload) + class TestAudience(TestCase): def setUp(self): diff --git a/tests/test_views.py b/tests/test_views.py index 820280bf..0382c169 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -27,6 +27,8 @@ (r'^auth-token/$', 'rest_framework_jwt.views.obtain_jwt_token'), (r'^auth-token-refresh/$', 'rest_framework_jwt.views.refresh_jwt_token'), (r'^auth-token-verify/$', 'rest_framework_jwt.views.verify_jwt_token'), + (r'^auth-token-blacklist/$', 'rest_framework_jwt.views.blacklist_jwt_token'), + ) @@ -234,7 +236,7 @@ class VerifyJSONWebTokenTests(TokenTestCase): def test_verify_jwt(self): """ Test that a valid, non-expired token will return a 200 response - and itself when passed to the validation endpoint. + when passed to the validation endpoint. """ client = APIClient(enforce_csrf_checks=True) @@ -247,8 +249,6 @@ def test_verify_jwt(self): format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['token'], orig_token) - def test_verify_jwt_fails_with_expired_token(self): """ Test that an expired token will fail with the correct error. @@ -301,6 +301,29 @@ def test_verify_jwt_fails_with_missing_user(self): self.assertRegexpMatches(response.data['non_field_errors'][0], "User doesn't exist") + def test_verify_jwt_fails_with_blacklisted_token(self): + """ + Test that a blacklisted token will fail. + """ + api_settings.JWT_ENABLE_BLACKLIST = True + client = APIClient(enforce_csrf_checks=True) + + user = User.objects.create_user( + email='jsmith@example.com', username='jsmith', password='password') + + token = self.create_token(user) + + response = client.post('/auth-token-blacklist/', {'token': token}, + format='json') + + self.assertIsNone(response.data['token']) + + response = client.post('/auth-token-verify/', {'token': token}, + format='json') + + self.assertRegexpMatches(response.data['non_field_errors'][0], + "Token is blacklisted") + class RefreshJSONWebTokenTests(TokenTestCase): From 14debb4fcfbe70ea7fbc81d0e3d9c7086cd3c229 Mon Sep 17 00:00:00 2001 From: AM Date: Mon, 23 Mar 2015 11:22:47 +0100 Subject: [PATCH 49/93] reducing the probability of repeated keys by using the user_id:jti pair as the key --- rest_framework_jwt/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 495d37c2..b359952c 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -100,7 +100,7 @@ def jwt_is_blacklisted(payload): if 'jti' not in payload or api_settings.JWT_ENABLE_BLACKLIST is False: return False - return jti_collection.find_one({'jti': payload['jti']}) is not None + return jti_collection.find_one({str(payload['user_id']): payload['jti']}) is not None def jwt_blacklist(payload): @@ -108,5 +108,5 @@ def jwt_blacklist(payload): raise ValueError(_("Can't blacklist payloads that don't have a jti claim")) if not jwt_is_blacklisted(payload): - jti_collection.insert({'jti': payload['jti'], + jti_collection.insert({str(payload['user_id']): payload['jti'], 'payload': payload}) From 15c9b13c47d370423034111dd4de90cfe260a176 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:01:09 -0700 Subject: [PATCH 50/93] Adds JWTBlackListToken model and default implementation for blacklisting jwt tokens --- requirements.txt | 2 +- rest_framework_jwt/models.py | 15 +++++++++- rest_framework_jwt/serializers.py | 25 +++++++++------- rest_framework_jwt/settings.py | 8 ++++++ rest_framework_jwt/utils.py | 47 ++++++++++++++++--------------- rest_framework_jwt/views.py | 13 ++++----- 6 files changed, 68 insertions(+), 42 deletions(-) diff --git a/requirements.txt b/requirements.txt index ad0544d9..6013e18d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ flake8==2.2.2 freezegun==0.3.2 # Optional packages +django-uuidfield>=0.5.0 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 -pymongo==2.8 diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 5b53a526..ebb30349 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -1 +1,14 @@ -# Just to keep things like ./manage.py test happy +from django.db import models + +# Django 1.8 includes UUIDField +if hasattr(models, 'UUIDField'): + import uuid + jti_field = models.UUIDField(editable=False, unique=True) +else: + from uuidfield import UUIDField + jti_field = UUIDField(auto=False, unique=True) + + +class JWTBlackListToken(models.Model): + jti = jti_field + timestamp = models.DateTimeField(auto_now_add=True) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 9b500420..4a5c2ed5 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -16,6 +16,8 @@ jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER jwt_decode_handler = api_settings.JWT_DECODE_HANDLER jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER +jwt_blacklist_get_handler = api_settings.JWT_BLACKLIST_GET_HANDLER +jwt_blacklist_set_handler = api_settings.JWT_BLACKLIST_SET_HANDLER class JSONWebTokenSerializer(Serializer): @@ -103,9 +105,12 @@ def _check_payload(self, token): msg = _('Error decoding signature.') raise serializers.ValidationError(msg) - if api_settings.JWT_ENABLE_BLACKLIST and utils.jwt_is_blacklisted(payload): - msg = _("Token is blacklisted") - raise serializers.ValidationError(msg) + # Check if the token has been blacklisted. + if api_settings.JWT_ENABLE_BLACKLIST: + blacklisted = jwt_blacklist_get_handler(payload) + + if blacklisted: + raise serializers.ValidationError(_('Token is blacklisted.')) return payload @@ -191,19 +196,19 @@ class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): """ def validate(self, attrs): + + token = attrs['token'] + if not api_settings.JWT_ENABLE_BLACKLIST: msg = _('JWT_ENABLE_BLACKLIST is set to False.') raise serializers.ValidationError(msg) - token = attrs['token'] - payload = self._check_payload(token=token) - user = self._check_user(payload=payload) - # Get and check 'jti' - jti = payload.get('jti') + + # Handle blacklisting a token. + jwt_blacklist_set_handler(payload) - if jti: - utils.jwt_blacklist(payload) + user = self._check_user(payload=payload) return { 'token': None, diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index 90fd8104..70da4d35 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -22,6 +22,12 @@ 'JWT_RESPONSE_PAYLOAD_HANDLER': 'rest_framework_jwt.utils.jwt_response_payload_handler', + 'JWT_BLACKLIST_GET_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_get_handler', + + 'JWT_BLACKLIST_DET_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_set_handler', + 'JWT_SECRET_KEY': settings.SECRET_KEY, 'JWT_ALGORITHM': 'HS256', 'JWT_VERIFY': True, @@ -46,6 +52,8 @@ 'JWT_PAYLOAD_HANDLER', 'JWT_PAYLOAD_GET_USER_ID_HANDLER', 'JWT_RESPONSE_PAYLOAD_HANDLER', + 'JWT_BLACKLIST_GET_HANDLER', + 'JWT_BLACKLIST_SET_HANDLER', ) api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index b359952c..b708fcc6 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,15 +1,15 @@ +import jwt +import uuid import random import string + from datetime import datetime -import jwt from django.utils.translation import gettext_lazy as _ from rest_framework_jwt.settings import api_settings -if api_settings.JWT_ENABLE_BLACKLIST: - import pymongo - jti_collection = pymongo.MongoClient().jwt_db.jti_collection +from . import models def get_user_model(): @@ -29,18 +29,14 @@ def jwt_payload_handler(user): except AttributeError: username = user.username - payload = { + return { 'user_id': user.pk, 'email': user.email, 'username': username, - 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA + 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA, + 'jti': uuid.uuid4() } - if 'jti' not in payload: - payload['jti'] = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(20)) - - return payload - def jwt_get_user_id_from_payload_handler(payload): """ @@ -96,17 +92,24 @@ def jwt_response_payload_handler(token, user=None, request=None): } -def jwt_is_blacklisted(payload): - if 'jti' not in payload or api_settings.JWT_ENABLE_BLACKLIST is False: - return False - - return jti_collection.find_one({str(payload['user_id']): payload['jti']}) is not None +def jwt_blacklist_get_handler(payload): + """ + Default implementation to check if a blacklisted jwt token exists. + """ + jti = payload.get('jti') + try: + token = models.JWTBlackListToken.objects.get(jti=jti) + except models.JWTBlackListToken.DoesNotExist: + return False + else: + return True -def jwt_blacklist(payload): - if 'jti' not in payload: - raise ValueError(_("Can't blacklist payloads that don't have a jti claim")) - if not jwt_is_blacklisted(payload): - jti_collection.insert({str(payload['user_id']): payload['jti'], - 'payload': payload}) +def jwt_blacklist_set_handler(payload): + """ + Default implementation that blacklists a jwt token. + """ + jti = payload.get('jti') + + return models.JWTBlackListToken.objects.create(jti=jti) diff --git a/rest_framework_jwt/views.py b/rest_framework_jwt/views.py index 33ee6d7e..6834bd4e 100644 --- a/rest_framework_jwt/views.py +++ b/rest_framework_jwt/views.py @@ -6,10 +6,7 @@ from rest_framework_jwt.settings import api_settings -from .serializers import ( - JSONWebTokenSerializer, RefreshJSONWebTokenSerializer, - VerifyJSONWebTokenSerializer, BlacklistJSONWebTokenSerializer -) +from . import serializers jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER @@ -43,7 +40,7 @@ class ObtainJSONWebToken(JSONWebTokenAPIView): Returns a JSON Web Token that can be used for authenticated requests. """ - serializer_class = JSONWebTokenSerializer + serializer_class = serializers.JSONWebTokenSerializer class VerifyJSONWebToken(JSONWebTokenAPIView): @@ -51,7 +48,7 @@ class VerifyJSONWebToken(JSONWebTokenAPIView): API View that checks the veracity of a token, returning the token if it is valid. """ - serializer_class = VerifyJSONWebTokenSerializer + serializer_class = serializers.VerifyJSONWebTokenSerializer class RefreshJSONWebToken(JSONWebTokenAPIView): @@ -62,14 +59,14 @@ class RefreshJSONWebToken(JSONWebTokenAPIView): If 'orig_iat' field (original issued-at-time) is found, will first check if it's within expiration window, then copy it to the new token """ - serializer_class = RefreshJSONWebTokenSerializer + serializer_class = serializers.RefreshJSONWebTokenSerializer class BlacklistJSONWebToken(JSONWebTokenAPIView): """ API View that blacklists a token """ - serializer_class = BlacklistJSONWebTokenSerializer + serializer_class = serializers.BlacklistJSONWebTokenSerializer obtain_jwt_token = ObtainJSONWebToken.as_view() From 319c6a2a113392925d99467ba79024dcf38d4ff2 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:05:19 -0700 Subject: [PATCH 51/93] Fixed typo --- rest_framework_jwt/settings.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index 70da4d35..d99f8b15 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -25,7 +25,7 @@ 'JWT_BLACKLIST_GET_HANDLER': 'rest_framework_jwt.utils.jwt_blacklist_get_handler', - 'JWT_BLACKLIST_DET_HANDLER': + 'JWT_BLACKLIST_SET_HANDLER': 'rest_framework_jwt.utils.jwt_blacklist_set_handler', 'JWT_SECRET_KEY': settings.SECRET_KEY, @@ -36,12 +36,9 @@ 'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300), 'JWT_AUDIENCE': None, 'JWT_ISSUER': None, - 'JWT_ALLOW_REFRESH': False, 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), - 'JWT_AUTH_HEADER_PREFIX': 'JWT', - 'JWT_ENABLE_BLACKLIST': False, } From 05b083985ea4b17b7b9f3501479b5720267becd6 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:13:46 -0700 Subject: [PATCH 52/93] Disable blacklist tests temporarily --- tests/test_utils.py | 14 ++------------ tests/test_views.py | 19 +------------------ 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index b7ed56d1..09a85934 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -68,20 +68,10 @@ def test_jwt_decode_verify_exp(self): api_settings.JWT_VERIFY_EXPIRATION = True def test_jti_blacklist(self): - api_settings.JWT_ENABLE_BLACKLIST = True - reload(utils) # it will now retrieve the mongo collection for the jti blacklist - payload = utils.jwt_payload_handler(self.user) - utils.jwt_blacklist(payload) - - self.assertEqual(utils.jwt_is_blacklisted(payload), True) + pass def test_fail_blacklist_without_jti(self): - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) - payload.pop('jti') - - with self.assertRaisesMessage(ValueError, "Can't blacklist payloads that don't have a jti claim"): - utils.jwt_blacklist(payload) + pass class TestAudience(TestCase): diff --git a/tests/test_views.py b/tests/test_views.py index 0382c169..747c0720 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -305,24 +305,7 @@ def test_verify_jwt_fails_with_blacklisted_token(self): """ Test that a blacklisted token will fail. """ - api_settings.JWT_ENABLE_BLACKLIST = True - client = APIClient(enforce_csrf_checks=True) - - user = User.objects.create_user( - email='jsmith@example.com', username='jsmith', password='password') - - token = self.create_token(user) - - response = client.post('/auth-token-blacklist/', {'token': token}, - format='json') - - self.assertIsNone(response.data['token']) - - response = client.post('/auth-token-verify/', {'token': token}, - format='json') - - self.assertRegexpMatches(response.data['non_field_errors'][0], - "Token is blacklisted") + pass class RefreshJSONWebTokenTests(TokenTestCase): From b0975946d2d0768de7e6b9762eb1c3674fe32bbe Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:19:07 -0700 Subject: [PATCH 53/93] Added fallback for import error --- rest_framework_jwt/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index ebb30349..f1733966 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -5,8 +5,11 @@ import uuid jti_field = models.UUIDField(editable=False, unique=True) else: - from uuidfield import UUIDField - jti_field = UUIDField(auto=False, unique=True) + try: + from uuidfield import UUIDField + jti_field = UUIDField(auto=False, unique=True) + except ImportError: + jti_field = CharField(max_length=64, editable= False, unique=True) class JWTBlackListToken(models.Model): From 2e118c05c87bb964f7f0bc7b5fac41200f1c2498 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:20:43 -0700 Subject: [PATCH 54/93] Fixed typo --- rest_framework_jwt/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index f1733966..4111f920 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -9,7 +9,7 @@ from uuidfield import UUIDField jti_field = UUIDField(auto=False, unique=True) except ImportError: - jti_field = CharField(max_length=64, editable= False, unique=True) + jti_field = models.CharField(max_length=64, editable= False, unique=True) class JWTBlackListToken(models.Model): From 84c72eaaa15773e51bd03eb3f713f16a998d7867 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 13:27:00 -0700 Subject: [PATCH 55/93] flake8 --- rest_framework_jwt/models.py | 3 ++- rest_framework_jwt/serializers.py | 4 ++-- rest_framework_jwt/utils.py | 2 +- tests/test_views.py | 12 +++++++----- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 4111f920..8eb501f3 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -9,7 +9,8 @@ from uuidfield import UUIDField jti_field = UUIDField(auto=False, unique=True) except ImportError: - jti_field = models.CharField(max_length=64, editable= False, unique=True) + jti_field = models.CharField(max_length=64, + editable=False, unique=True) class JWTBlackListToken(models.Model): diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 4a5c2ed5..c14046eb 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -196,7 +196,7 @@ class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): """ def validate(self, attrs): - + token = attrs['token'] if not api_settings.JWT_ENABLE_BLACKLIST: @@ -204,7 +204,7 @@ def validate(self, attrs): raise serializers.ValidationError(msg) payload = self._check_payload(token=token) - + # Handle blacklisting a token. jwt_blacklist_set_handler(payload) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index b708fcc6..c73f2d0e 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -111,5 +111,5 @@ def jwt_blacklist_set_handler(payload): Default implementation that blacklists a jwt token. """ jti = payload.get('jti') - + return models.JWTBlackListToken.objects.create(jti=jti) diff --git a/tests/test_views.py b/tests/test_views.py index 747c0720..4c3aabce 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -27,9 +27,8 @@ (r'^auth-token/$', 'rest_framework_jwt.views.obtain_jwt_token'), (r'^auth-token-refresh/$', 'rest_framework_jwt.views.refresh_jwt_token'), (r'^auth-token-verify/$', 'rest_framework_jwt.views.verify_jwt_token'), - (r'^auth-token-blacklist/$', 'rest_framework_jwt.views.blacklist_jwt_token'), - - + (r'^auth-token-blacklist/$', + 'rest_framework_jwt.views.blacklist_jwt_token'), ) orig_datetime = datetime @@ -350,8 +349,11 @@ def test_refresh_jwt_after_refresh_expiration(self): """ client = APIClient(enforce_csrf_checks=True) - orig_iat = (datetime.utcnow() - api_settings.JWT_REFRESH_EXPIRATION_DELTA - - timedelta(seconds=5)) + orig_iat = ( + datetime.utcnow() + - api_settings.JWT_REFRESH_EXPIRATION_DELTA + - timedelta(seconds=5) + ) token = self.create_token( self.user, exp=datetime.utcnow() + timedelta(hours=1), From c44bd8740154c4fddf2989f696b5cf36ced70407 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 14:06:57 -0700 Subject: [PATCH 56/93] Cleanup --- rest_framework_jwt/models.py | 1 - rest_framework_jwt/utils.py | 6 +----- tests/test_views.py | 6 +++--- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 8eb501f3..a3a7baa6 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -2,7 +2,6 @@ # Django 1.8 includes UUIDField if hasattr(models, 'UUIDField'): - import uuid jti_field = models.UUIDField(editable=False, unique=True) else: try: diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index c73f2d0e..fd863389 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,12 +1,8 @@ import jwt import uuid -import random -import string from datetime import datetime -from django.utils.translation import gettext_lazy as _ - from rest_framework_jwt.settings import api_settings from . import models @@ -103,7 +99,7 @@ def jwt_blacklist_get_handler(payload): except models.JWTBlackListToken.DoesNotExist: return False else: - return True + return bool(token) def jwt_blacklist_set_handler(payload): diff --git a/tests/test_views.py b/tests/test_views.py index 4c3aabce..bdff723f 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -350,9 +350,9 @@ def test_refresh_jwt_after_refresh_expiration(self): client = APIClient(enforce_csrf_checks=True) orig_iat = ( - datetime.utcnow() - - api_settings.JWT_REFRESH_EXPIRATION_DELTA - - timedelta(seconds=5) + datetime.utcnow() - + api_settings.JWT_REFRESH_EXPIRATION_DELTA - + timedelta(seconds=5) ) token = self.create_token( self.user, From 906896dfa646d5a30e4563727f13ef77f9b27366 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 14:19:23 -0700 Subject: [PATCH 57/93] Use uuid.hex --- rest_framework_jwt/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index fd863389..82163736 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -30,7 +30,7 @@ def jwt_payload_handler(user): 'email': user.email, 'username': username, 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA, - 'jti': uuid.uuid4() + 'jti': uuid.uuid4().hex } From 391928fb873a56f202b696ddc422dce7254a9e67 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 16:24:09 -0700 Subject: [PATCH 58/93] Moved uuid import to compat --- rest_framework_jwt/compat.py | 21 +++++++++++++++++++++ rest_framework_jwt/models.py | 15 ++++----------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/rest_framework_jwt/compat.py b/rest_framework_jwt/compat.py index 01313aae..764899ea 100644 --- a/rest_framework_jwt/compat.py +++ b/rest_framework_jwt/compat.py @@ -1,5 +1,9 @@ import rest_framework + +from django.db import models + from distutils.version import StrictVersion +from functools import partial if StrictVersion(rest_framework.VERSION) < StrictVersion('3.0.0'): @@ -9,3 +13,20 @@ class Serializer(rest_framework.serializers.Serializer): @property def object(self): return self.validated_data + + +def get_uuid_field(object): + """ + Returns a partial object that when called instantiates a UUIDField + either from Django 1.8's native implementation, from django-uuidfield, + or as a CharField as the final fallback. + """ + if hasattr(models, 'UUIDField'): + return partial(models.UUIDField, editable=False, unique=True) + else: + try: + from uuidfield import UUIDField + return partial(UUIDField, auto=False, unique=True) + except ImportError: + return partial(models.CharField, max_length=64, + editable=False, unique=True) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index a3a7baa6..a8d6a2d9 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -1,17 +1,10 @@ from django.db import models -# Django 1.8 includes UUIDField -if hasattr(models, 'UUIDField'): - jti_field = models.UUIDField(editable=False, unique=True) -else: - try: - from uuidfield import UUIDField - jti_field = UUIDField(auto=False, unique=True) - except ImportError: - jti_field = models.CharField(max_length=64, - editable=False, unique=True) +from .compat import get_uuid_field + +UUIDField = get_uuid_field() class JWTBlackListToken(models.Model): - jti = jti_field + jti = UUIDField() timestamp = models.DateTimeField(auto_now_add=True) From f56e95562ee7525455907a80ade1c8e6d95daef4 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 20:14:59 -0700 Subject: [PATCH 59/93] Cleanup --- rest_framework_jwt/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_jwt/compat.py b/rest_framework_jwt/compat.py index 764899ea..4a53eeeb 100644 --- a/rest_framework_jwt/compat.py +++ b/rest_framework_jwt/compat.py @@ -15,7 +15,7 @@ def object(self): return self.validated_data -def get_uuid_field(object): +def get_uuid_field(): """ Returns a partial object that when called instantiates a UUIDField either from Django 1.8's native implementation, from django-uuidfield, From 4b56adc12c1ef4334c06508810662fa6072ab961 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 21:26:42 -0700 Subject: [PATCH 60/93] Added blacklisted token check to authentiate method to verify incoming requests arent using black listed token --- rest_framework_jwt/authentication.py | 8 ++++++++ rest_framework_jwt/utils.py | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index 64d5f9a5..7f7df9dc 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -11,6 +11,7 @@ jwt_decode_handler = api_settings.JWT_DECODE_HANDLER jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER +jwt_blacklist_get_handler = api_settings.JWT_BLACKLIST_GET_HANDLER class BaseJSONWebTokenAuthentication(BaseAuthentication): @@ -38,6 +39,13 @@ def authenticate(self, request): except jwt.InvalidTokenError: raise exceptions.AuthenticationFailed() + # Check if the token has been blacklisted. + if api_settings.JWT_ENABLE_BLACKLIST: + blacklisted = jwt_blacklist_get_handler(payload) + + if blacklisted: + raise serializers.ValidationError(_('Token is blacklisted.')) + user = self.authenticate_credentials(payload) return (user, jwt_value) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 82163736..72c989d5 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -38,8 +38,7 @@ def jwt_get_user_id_from_payload_handler(payload): """ Override this function if user_id is formatted differently in payload """ - user_id = payload.get('user_id') - return user_id + return payload.get('user_id') def jwt_encode_handler(payload): From 6425a2eb8bbda8336aaad84d39209aad3d81eec4 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 21:28:08 -0700 Subject: [PATCH 61/93] Fixed import --- rest_framework_jwt/authentication.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index 7f7df9dc..05353fb2 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -1,7 +1,10 @@ import jwt + from django.utils.encoding import smart_text from django.utils.translation import ugettext as _ + from rest_framework import exceptions +from rest_framework import serializers from rest_framework.authentication import (BaseAuthentication, get_authorization_header) From 3d4c00b76116887422bc79f2ff5bb568bf6cc738 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 21:56:14 -0700 Subject: [PATCH 62/93] Changed ValidationError to Authorization exception --- rest_framework_jwt/authentication.py | 5 +++-- rest_framework_jwt/serializers.py | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index 05353fb2..dfa927e1 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -4,7 +4,6 @@ from django.utils.translation import ugettext as _ from rest_framework import exceptions -from rest_framework import serializers from rest_framework.authentication import (BaseAuthentication, get_authorization_header) @@ -47,7 +46,9 @@ def authenticate(self, request): blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: - raise serializers.ValidationError(_('Token is blacklisted.')) + raise exceptions.AuthenticationFailed( + _('Token is blacklisted.') + ) user = self.authenticate_credentials(payload) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index c14046eb..b471ad0e 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -5,12 +5,13 @@ from django.contrib.auth import authenticate from django.utils.translation import ugettext as _ -from rest_framework import serializers -from .compat import Serializer +from rest_framework import exceptions +from rest_framework import serializers from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings +from .compat import Serializer jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER @@ -110,7 +111,9 @@ def _check_payload(self, token): blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: - raise serializers.ValidationError(_('Token is blacklisted.')) + raise exceptions.AuthenticationFailed( + _('Token is blacklisted.') + ) return payload From c7c04c5f2da212fdc1a365360f15cb3b2cfcd096 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 27 Mar 2015 23:43:33 -0700 Subject: [PATCH 63/93] Cleanup --- rest_framework_jwt/authentication.py | 5 ++--- rest_framework_jwt/serializers.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index dfa927e1..fe0152cd 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -46,9 +46,8 @@ def authenticate(self, request): blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: - raise exceptions.AuthenticationFailed( - _('Token is blacklisted.') - ) + msg = _('Token is blacklisted.') + raise exceptions.AuthenticationFailed(msg) user = self.authenticate_credentials(payload) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index b471ad0e..88cbefdd 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -111,9 +111,8 @@ def _check_payload(self, token): blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: - raise exceptions.AuthenticationFailed( - _('Token is blacklisted.') - ) + msg = _('Token is blacklisted.') + raise exceptions.AuthenticationFailed(msg) return payload From 6294afaf1347d6b61c3a3ec14ad48ae6edcde13c Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 08:03:11 -0700 Subject: [PATCH 64/93] Added expires_at field to JWTBlackListToken model --- rest_framework_jwt/models.py | 1 + rest_framework_jwt/utils.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index a8d6a2d9..e34c19ea 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -7,4 +7,5 @@ class JWTBlackListToken(models.Model): jti = UUIDField() + expires_at = models.DateTimeField() timestamp = models.DateTimeField(auto_now_add=True) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 72c989d5..6f407265 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -90,15 +90,17 @@ def jwt_response_payload_handler(token, user=None, request=None): def jwt_blacklist_get_handler(payload): """ Default implementation to check if a blacklisted jwt token exists. + + Should return a black listed token or None. """ jti = payload.get('jti') try: token = models.JWTBlackListToken.objects.get(jti=jti) except models.JWTBlackListToken.DoesNotExist: - return False + return None else: - return bool(token) + return token def jwt_blacklist_set_handler(payload): @@ -106,5 +108,6 @@ def jwt_blacklist_set_handler(payload): Default implementation that blacklists a jwt token. """ jti = payload.get('jti') + exp = datetime.fromtimestamp(payload.get('exp')) - return models.JWTBlackListToken.objects.create(jti=jti) + return models.JWTBlackListToken.objects.create(jti=jti, expires_at=exp) From 617c43635b4601dc5dfb1b4fb64016858f468f3e Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 09:48:30 -0700 Subject: [PATCH 65/93] Code tweaks --- rest_framework_jwt/models.py | 6 ++++++ rest_framework_jwt/serializers.py | 4 ++-- rest_framework_jwt/utils.py | 20 ++++++++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index e34c19ea..242c7f03 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -1,3 +1,5 @@ +import datetime + from django.db import models from .compat import get_uuid_field @@ -9,3 +11,7 @@ class JWTBlackListToken(models.Model): jti = UUIDField() expires_at = models.DateTimeField() timestamp = models.DateTimeField(auto_now_add=True) + + def is_expired(self): + now = datetime.datetime.now() + return expires_at < now diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 88cbefdd..25bcc360 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -208,11 +208,11 @@ def validate(self, attrs): payload = self._check_payload(token=token) # Handle blacklisting a token. - jwt_blacklist_set_handler(payload) + token = jwt_blacklist_set_handler(payload) user = self._check_user(payload=payload) return { - 'token': None, + 'token': token, 'user': user } diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 6f407265..3dfe17de 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -3,6 +3,9 @@ from datetime import datetime +from django.utils.translations import ugettext_lazy as _ + +from rest_framework import exceptions from rest_framework_jwt.settings import api_settings from . import models @@ -106,8 +109,17 @@ def jwt_blacklist_get_handler(payload): def jwt_blacklist_set_handler(payload): """ Default implementation that blacklists a jwt token. - """ - jti = payload.get('jti') - exp = datetime.fromtimestamp(payload.get('exp')) - return models.JWTBlackListToken.objects.create(jti=jti, expires_at=exp) + Should return a black listed token. + """ + data = { + 'jti': payload.get('jti') + } + try: + data.update({ + 'exp': datetime.fromtimestamp(payload.get('exp')) + }) + except TypeError: + return None + else: + return models.JWTBlackListToken.objects.create(**data) From 255a91abd9ea313f1b3d48288ff2c9da2f645c40 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 09:53:15 -0700 Subject: [PATCH 66/93] cleanup --- rest_framework_jwt/models.py | 2 +- rest_framework_jwt/utils.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 242c7f03..c966b285 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -14,4 +14,4 @@ class JWTBlackListToken(models.Model): def is_expired(self): now = datetime.datetime.now() - return expires_at < now + return self.expires_at < now diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 3dfe17de..07794acf 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -3,9 +3,6 @@ from datetime import datetime -from django.utils.translations import ugettext_lazy as _ - -from rest_framework import exceptions from rest_framework_jwt.settings import api_settings from . import models @@ -110,7 +107,7 @@ def jwt_blacklist_set_handler(payload): """ Default implementation that blacklists a jwt token. - Should return a black listed token. + Should return a black listed token or None. """ data = { 'jti': payload.get('jti') From 285cfb2821f66f3a6ab8b45f66a721109f06da70 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 09:57:04 -0700 Subject: [PATCH 67/93] Fix expires_at --- rest_framework_jwt/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 07794acf..89b14bae 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -114,7 +114,7 @@ def jwt_blacklist_set_handler(payload): } try: data.update({ - 'exp': datetime.fromtimestamp(payload.get('exp')) + 'expires_at': datetime.fromtimestamp(payload.get('exp')) }) except TypeError: return None From df83277ff863e0b795d88fd706072e485d7a5a96 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 10:02:15 -0700 Subject: [PATCH 68/93] Changed back to return None --- rest_framework_jwt/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 25bcc360..88cbefdd 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -208,11 +208,11 @@ def validate(self, attrs): payload = self._check_payload(token=token) # Handle blacklisting a token. - token = jwt_blacklist_set_handler(payload) + jwt_blacklist_set_handler(payload) user = self._check_user(payload=payload) return { - 'token': token, + 'token': None, 'user': user } From e6c38431ee71f2c1cec45a017965594c930d47f9 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 13:10:12 -0700 Subject: [PATCH 69/93] Added admin for default implementation --- rest_framework_jwt/admin.py | 15 +++++++++++++++ rest_framework_jwt/models.py | 14 ++++++++++---- rest_framework_jwt/utils.py | 4 +++- 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 rest_framework_jwt/admin.py diff --git a/rest_framework_jwt/admin.py b/rest_framework_jwt/admin.py new file mode 100644 index 00000000..ecab732a --- /dev/null +++ b/rest_framework_jwt/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin + +from . import models + + +class JWTBlackListTokenAdmin(admin.ModelAdmin): + list_display = ('jti', 'expires', 'created', 'is_expired') + fields = ('jti', 'expires', 'created', 'is_expired') + readonly_fields = ('jti', 'expires', 'created', 'is_expired') + + def is_expired(self, obj): + return obj.is_expired() + is_expired.boolean = True + +admin.site.register(models.JWTBlackListToken, JWTBlackListTokenAdmin) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index c966b285..d3ce810a 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -1,6 +1,8 @@ +import pytz import datetime from django.db import models +from django.utils.translation import ugettext_lazy as _ from .compat import get_uuid_field @@ -9,9 +11,13 @@ class JWTBlackListToken(models.Model): jti = UUIDField() - expires_at = models.DateTimeField() - timestamp = models.DateTimeField(auto_now_add=True) + expires = models.DateTimeField() + created = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = _('JWT Blacklist Token') + verbose_name_plural = _('JWT Blacklist Tokens') def is_expired(self): - now = datetime.datetime.now() - return self.expires_at < now + now = datetime.datetime.now(pytz.utc) + return self.expires < now diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 89b14bae..5df357d0 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,4 +1,5 @@ import jwt +import pytz import uuid from datetime import datetime @@ -114,7 +115,8 @@ def jwt_blacklist_set_handler(payload): } try: data.update({ - 'expires_at': datetime.fromtimestamp(payload.get('exp')) + 'expires': datetime.fromtimestamp(payload.get('exp')), + 'created': datetime.now(pytz.utc) }) except TypeError: return None From 8efe5ca91c656db22fca612bfb482e71f30c5f37 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 13:13:16 -0700 Subject: [PATCH 70/93] Added pytz to req --- rest_framework_jwt/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 5df357d0..c74b20fb 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -111,12 +111,12 @@ def jwt_blacklist_set_handler(payload): Should return a black listed token or None. """ data = { - 'jti': payload.get('jti') + 'jti': payload.get('jti'), + 'created': datetime.now(pytz.utc) } try: data.update({ - 'expires': datetime.fromtimestamp(payload.get('exp')), - 'created': datetime.now(pytz.utc) + 'expires': datetime.fromtimestamp(payload.get('exp')) }) except TypeError: return None From 4312e8765cf8575cbd767727ae4464260f788d9e Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 13:49:29 -0700 Subject: [PATCH 71/93] Make admin dependent on settings --- rest_framework_jwt/admin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rest_framework_jwt/admin.py b/rest_framework_jwt/admin.py index ecab732a..6aaa9653 100644 --- a/rest_framework_jwt/admin.py +++ b/rest_framework_jwt/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin +from rest_framework_jwt.settings import api_settings + from . import models @@ -12,4 +14,5 @@ def is_expired(self, obj): return obj.is_expired() is_expired.boolean = True -admin.site.register(models.JWTBlackListToken, JWTBlackListTokenAdmin) +if api_settings.JWT_ENABLE_BLACKLIST: + admin.site.register(models.JWTBlackListToken, JWTBlackListTokenAdmin) From 27667e19d901c435b4a66641d7d80ed61faf027b Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 18:20:52 -0700 Subject: [PATCH 72/93] Updated tests and requirements --- requirements.txt | 1 + rest_framework_jwt/compat.py | 7 +++++-- tests/test_authentication.py | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6013e18d..9dd7fc89 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ django-uuidfield>=0.5.0 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 +djangorestframework-oauth>=1.0.1 diff --git a/rest_framework_jwt/compat.py b/rest_framework_jwt/compat.py index 4a53eeeb..17981bb2 100644 --- a/rest_framework_jwt/compat.py +++ b/rest_framework_jwt/compat.py @@ -5,11 +5,13 @@ from distutils.version import StrictVersion from functools import partial +from rest_framework import serializers + if StrictVersion(rest_framework.VERSION) < StrictVersion('3.0.0'): from rest_framework.serializers import Serializer else: - class Serializer(rest_framework.serializers.Serializer): + class Serializer(serializers.Serializer): @property def object(self): return self.validated_data @@ -26,7 +28,8 @@ def get_uuid_field(): else: try: from uuidfield import UUIDField - return partial(UUIDField, auto=False, unique=True) + return partial(UUIDField, editable=False, + auto=False, unique=True) except ImportError: return partial(models.CharField, max_length=64, editable=False, unique=True) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 3cad6f95..f56d5e17 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model from rest_framework import permissions, status + try: from rest_framework_oauth.authentication import OAuth2Authentication except ImportError: @@ -35,6 +36,8 @@ from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings, DEFAULTS from rest_framework_jwt.authentication import JSONWebTokenAuthentication +from rest_framework_oauth.authentication import oauth2_provider +from rest_framework_oauth.authentication import OAuth2Authentication User = get_user_model() From 7eb10196740d7e6b8a3e8a467c863568168720ba Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 28 Mar 2015 22:57:40 -0700 Subject: [PATCH 73/93] Refactored view to allow custom payload handler, added default black list token serializer, fixed import issue with tests --- rest_framework_jwt/serializers.py | 17 +++++++++++++++-- rest_framework_jwt/settings.py | 4 ++++ rest_framework_jwt/utils.py | 15 +++++++++++++++ rest_framework_jwt/views.py | 5 ++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 88cbefdd..4825d133 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -13,6 +13,8 @@ from .compat import Serializer +from . import models + jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER jwt_decode_handler = api_settings.JWT_DECODE_HANDLER @@ -208,11 +210,22 @@ def validate(self, attrs): payload = self._check_payload(token=token) # Handle blacklisting a token. - jwt_blacklist_set_handler(payload) + token = jwt_blacklist_set_handler(payload) user = self._check_user(payload=payload) return { - 'token': None, + 'token': token, 'user': user } + + +class JWTBlackListTokenSerializer(serializers.ModelSerializer): + jti = serializers.SerializerMethodField('get_jti_value') + + class Meta: + model = models.JWTBlackListToken + + def get_jti_value(self, obj): + """Returns obj.jti manually due to py3 bug in django-uuidfield""" + return obj.jti diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index d99f8b15..c9ec568c 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -28,6 +28,9 @@ 'JWT_BLACKLIST_SET_HANDLER': 'rest_framework_jwt.utils.jwt_blacklist_set_handler', + 'JWT_BLACKLIST_RESPONSE_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_response_handler', + 'JWT_SECRET_KEY': settings.SECRET_KEY, 'JWT_ALGORITHM': 'HS256', 'JWT_VERIFY': True, @@ -51,6 +54,7 @@ 'JWT_RESPONSE_PAYLOAD_HANDLER', 'JWT_BLACKLIST_GET_HANDLER', 'JWT_BLACKLIST_SET_HANDLER', + 'JWT_BLACKLIST_RESPONSE_HANDLER', ) api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index c74b20fb..2005b4c4 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -2,6 +2,8 @@ import pytz import uuid +from django.utils.translation import ugettext_lazy as _ + from datetime import datetime from rest_framework_jwt.settings import api_settings @@ -122,3 +124,16 @@ def jwt_blacklist_set_handler(payload): return None else: return models.JWTBlackListToken.objects.create(**data) + + +def jwt_blacklist_response_handler(token, user=None, request=None): + """ + Default blacklist token response data. Override to provide a + custom response. + """ + from . import serializers + + return { + 'token': serializers.JWTBlackListTokenSerializer(token).data, + 'message': _('Token successfully blacklisted.') + } diff --git a/rest_framework_jwt/views.py b/rest_framework_jwt/views.py index 6834bd4e..a067091e 100644 --- a/rest_framework_jwt/views.py +++ b/rest_framework_jwt/views.py @@ -9,6 +9,7 @@ from . import serializers jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER +jwt_blacklist_response_handler = api_settings.JWT_BLACKLIST_RESPONSE_HANDLER class JSONWebTokenAPIView(APIView): @@ -20,6 +21,7 @@ class JSONWebTokenAPIView(APIView): authentication_classes = () parser_classes = (parsers.FormParser, parsers.JSONParser,) renderer_classes = (renderers.JSONRenderer,) + response_payload_handler = staticmethod(jwt_response_payload_handler) def post(self, request): serializer = self.serializer_class(data=request.DATA) @@ -27,7 +29,7 @@ def post(self, request): if serializer.is_valid(): user = serializer.object.get('user') or request.user token = serializer.object.get('token') - response_data = jwt_response_payload_handler(token, user, request) + response_data = self.response_payload_handler(token, user, request) return Response(response_data) @@ -67,6 +69,7 @@ class BlacklistJSONWebToken(JSONWebTokenAPIView): API View that blacklists a token """ serializer_class = serializers.BlacklistJSONWebTokenSerializer + response_payload_handler = staticmethod(jwt_blacklist_response_handler) obtain_jwt_token = ObtainJSONWebToken.as_view() From 335bc621a147bff330e16cd4cba6d7fa96b82825 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 29 Mar 2015 01:03:00 -0700 Subject: [PATCH 74/93] Add check for successful blacklist token --- rest_framework_jwt/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 4825d133..3a38bcc4 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -212,6 +212,10 @@ def validate(self, attrs): # Handle blacklisting a token. token = jwt_blacklist_set_handler(payload) + if not token: + msg = _('Could not blacklist token.') + raise serializers.ValidationError(msg) + user = self._check_user(payload=payload) return { From c399a3a68f05050fc7cfb09eb3f6399678c8c2da Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 29 Mar 2015 10:17:05 -0700 Subject: [PATCH 75/93] Fix oauth import in tox --- tests/test_authentication.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index f56d5e17..d5c00e63 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -36,8 +36,16 @@ from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings, DEFAULTS from rest_framework_jwt.authentication import JSONWebTokenAuthentication -from rest_framework_oauth.authentication import oauth2_provider -from rest_framework_oauth.authentication import OAuth2Authentication + +try: + from rest_framework.authentication import oauth2_provider +except ImportError: + from rest_framework_oauth.authentication import oauth2_provider + +try: + from rest_framework.authentication import OAuth2Authentication +except ImportError: + from rest_framework_oauth.authentication import OAuth2Authentication User = get_user_model() From 14085e5e1bed61cec1deb664e53eda218f2b0f54 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Mon, 30 Mar 2015 10:47:27 -0700 Subject: [PATCH 76/93] Removed pytz and django-uuidfields --- requirements.txt | 1 - rest_framework_jwt/compat.py | 12 +++--------- rest_framework_jwt/models.py | 7 ++----- rest_framework_jwt/utils.py | 4 ++-- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9dd7fc89..3cfbe45d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,6 @@ flake8==2.2.2 freezegun==0.3.2 # Optional packages -django-uuidfield>=0.5.0 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 diff --git a/rest_framework_jwt/compat.py b/rest_framework_jwt/compat.py index 17981bb2..02f9f06c 100644 --- a/rest_framework_jwt/compat.py +++ b/rest_framework_jwt/compat.py @@ -20,16 +20,10 @@ def object(self): def get_uuid_field(): """ Returns a partial object that when called instantiates a UUIDField - either from Django 1.8's native implementation, from django-uuidfield, - or as a CharField as the final fallback. + either from Django 1.8's native implementation, or as a CharField. """ if hasattr(models, 'UUIDField'): return partial(models.UUIDField, editable=False, unique=True) else: - try: - from uuidfield import UUIDField - return partial(UUIDField, editable=False, - auto=False, unique=True) - except ImportError: - return partial(models.CharField, max_length=64, - editable=False, unique=True) + return partial(models.CharField, max_length=64, + editable=False, unique=True) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index d3ce810a..3e4de5f3 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -1,7 +1,5 @@ -import pytz -import datetime - from django.db import models +from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from .compat import get_uuid_field @@ -19,5 +17,4 @@ class Meta: verbose_name_plural = _('JWT Blacklist Tokens') def is_expired(self): - now = datetime.datetime.now(pytz.utc) - return self.expires < now + return self.expires < now() diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index 2005b4c4..e4206756 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,7 +1,7 @@ import jwt -import pytz import uuid +from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from datetime import datetime @@ -114,7 +114,7 @@ def jwt_blacklist_set_handler(payload): """ data = { 'jti': payload.get('jti'), - 'created': datetime.now(pytz.utc) + 'created': now() } try: data.update({ From ec73b3e55cfb54588deb59e483ddc071ced50f6d Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 31 Mar 2015 10:34:51 -0700 Subject: [PATCH 77/93] Added blacklist auth test --- tests/conftest.py | 1 + tests/test_authentication.py | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index fe079bc7..158ea419 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,7 @@ def pytest_configure(): 'django.contrib.staticfiles', 'tests', + 'rest_framework_jwt', ), PASSWORD_HASHERS=( 'django.contrib.auth.hashers.MD5PasswordHasher', diff --git a/tests/test_authentication.py b/tests/test_authentication.py index d5c00e63..15e2a685 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -3,6 +3,7 @@ from django.utils import unittest from django.conf.urls import patterns from django.contrib.auth import get_user_model +from django.utils.timezone import now from rest_framework import permissions, status @@ -34,6 +35,7 @@ from rest_framework.views import APIView from rest_framework_jwt import utils +from rest_framework_jwt.models import JWTBlackListToken from rest_framework_jwt.settings import api_settings, DEFAULTS from rest_framework_jwt.authentication import JSONWebTokenAuthentication @@ -78,6 +80,42 @@ def post(self, request): OAuth2Authentication, JSONWebTokenAuthentication])), ) +class BlacklistTokenAuthenticationTest(TestCase): + urls = 'tests.test_authentication' + + def setUp(self): + self.csrf_client = APIClient(enforce_csrf_checks=True) + self.username = 'jpueblo' + self.email = 'jpueblo@example.com' + self.user = User.objects.create_user(self.username, self.email) + + api_settings.JWT_ENABLE_BLACKLIST = True + + def test_post_blacklisted_token_failing_jwt_auth(self): + """ + Ensure POSTing over JWT auth with blacklisted token fails + """ + payload = utils.jwt_payload_handler(self.user) + token = utils.jwt_encode_handler(payload) + + # Create blacklist token which effectively blacklists the token. + JWTBlackListToken.objects.create(jti=payload.get('jti'), + created=now(), expires=now()) + + auth = 'JWT {0}'.format(token) + response = self.csrf_client.post( + '/jwt/', {'example': 'example'}, + HTTP_AUTHORIZATION=auth, format='json') + + msg = 'Token is blacklisted.' + + self.assertEqual(response.data['detail'], msg) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(response['WWW-Authenticate'], 'JWT realm="api"') + + def tearDown(self): + api_settings.JWT_ENABLE_BLACKLIST = False + class JSONWebTokenAuthenticationTests(TestCase): """JSON Web Token Authentication""" From 8ce2fe22a517ed5ec4c14ceddf0e8aa6700a2789 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 31 Mar 2015 11:20:57 -0700 Subject: [PATCH 78/93] Added another auth test --- tests/test_authentication.py | 37 +++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 15e2a685..b76a9b4e 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -80,7 +80,9 @@ def post(self, request): OAuth2Authentication, JSONWebTokenAuthentication])), ) -class BlacklistTokenAuthenticationTest(TestCase): + +class JSONWebTokenAuthenticationTests(TestCase): + """JSON Web Token Authentication""" urls = 'tests.test_authentication' def setUp(self): @@ -88,19 +90,36 @@ def setUp(self): self.username = 'jpueblo' self.email = 'jpueblo@example.com' self.user = User.objects.create_user(self.username, self.email) - + + def test_post_json_passing_jwt_auth_blacklist_enabled(self): + """ + Ensure POSTing JSON over JWT auth with correct credentials + passes and does not require CSRF + """ api_settings.JWT_ENABLE_BLACKLIST = True + payload = utils.jwt_payload_handler(self.user) + token = utils.jwt_encode_handler(payload) + + auth = 'JWT {0}'.format(token) + response = self.csrf_client.post( + '/jwt/', {'example': 'example'}, + HTTP_AUTHORIZATION=auth, format='json') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + api_settings.JWT_ENABLE_BLACKLIST = False def test_post_blacklisted_token_failing_jwt_auth(self): """ Ensure POSTing over JWT auth with blacklisted token fails """ + api_settings.JWT_ENABLE_BLACKLIST = True payload = utils.jwt_payload_handler(self.user) token = utils.jwt_encode_handler(payload) # Create blacklist token which effectively blacklists the token. JWTBlackListToken.objects.create(jti=payload.get('jti'), - created=now(), expires=now()) + created=now(), expires=now()) auth = 'JWT {0}'.format(token) response = self.csrf_client.post( @@ -113,20 +132,8 @@ def test_post_blacklisted_token_failing_jwt_auth(self): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response['WWW-Authenticate'], 'JWT realm="api"') - def tearDown(self): api_settings.JWT_ENABLE_BLACKLIST = False - -class JSONWebTokenAuthenticationTests(TestCase): - """JSON Web Token Authentication""" - urls = 'tests.test_authentication' - - def setUp(self): - self.csrf_client = APIClient(enforce_csrf_checks=True) - self.username = 'jpueblo' - self.email = 'jpueblo@example.com' - self.user = User.objects.create_user(self.username, self.email) - def test_post_form_passing_jwt_auth(self): """ Ensure POSTing form over JWT auth with correct credentials From 29bdfa62bc61e9ff8bf52020e0aa44c4ec9e9aa6 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 31 Mar 2015 18:00:17 -0700 Subject: [PATCH 79/93] Added other blacklist unit tests --- rest_framework_jwt/utils.py | 16 ++++----- tests/test_authentication.py | 1 + tests/test_serializers.py | 45 +++++++++++++++++++++++++ tests/test_utils.py | 61 +++++++++++++++++++++++++++------ tests/test_views.py | 65 ++++++++++++++++++++++++++++++++++-- 5 files changed, 166 insertions(+), 22 deletions(-) diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index e4206756..b8439a12 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,6 +1,7 @@ import jwt import uuid +from django.db import IntegrityError from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ @@ -112,18 +113,15 @@ def jwt_blacklist_set_handler(payload): Should return a black listed token or None. """ - data = { - 'jti': payload.get('jti'), - 'created': now() - } try: - data.update({ + data = { + 'jti': payload.get('jti'), + 'created': now(), 'expires': datetime.fromtimestamp(payload.get('exp')) - }) - except TypeError: - return None - else: + } return models.JWTBlackListToken.objects.create(**data) + except (TypeError, IntegrityError, Exception): + return None def jwt_blacklist_response_handler(token, user=None, request=None): diff --git a/tests/test_authentication.py b/tests/test_authentication.py index b76a9b4e..dd4bf304 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -114,6 +114,7 @@ def test_post_blacklisted_token_failing_jwt_auth(self): Ensure POSTing over JWT auth with blacklisted token fails """ api_settings.JWT_ENABLE_BLACKLIST = True + payload = utils.jwt_payload_handler(self.user) token = utils.jwt_encode_handler(payload) diff --git a/tests/test_serializers.py b/tests/test_serializers.py index a7b4fd3b..8df52c08 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -5,7 +5,9 @@ from django.utils import unittest from django.contrib.auth import get_user_model +from rest_framework_jwt.settings import api_settings from rest_framework_jwt.serializers import JSONWebTokenSerializer +from rest_framework_jwt.serializers import BlacklistJSONWebTokenSerializer from rest_framework_jwt import utils User = get_user_model() @@ -93,3 +95,46 @@ def test_required_fields(self): self.assertFalse(is_valid) self.assertEqual(serializer.errors, expected_error) + + +class BlacklistJSONWebTokenSerializerTests(TestCase): + + 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.payload = utils.jwt_payload_handler(self.user) + self.data = { + 'token': utils.jwt_encode_handler(self.payload) + } + + def test_token_blacklisted(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + serializer = BlacklistJSONWebTokenSerializer(data=self.data) + is_valid = serializer.is_valid() + + token = serializer.object['token'] + + self.assertTrue(is_valid) + self.assertEqual(self.payload.get('jti'), token.jti) + + def test_token_blacklist_fail_missing_jti(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + self.payload['jti'] = None + self.data = { + 'token': utils.jwt_encode_handler(self.payload) + } + + serializer = BlacklistJSONWebTokenSerializer(data=self.data) + is_valid = serializer.is_valid() + + self.assertFalse(is_valid) + + msg = 'Could not blacklist token.' + + self.assertEqual(serializer.errors['non_field_errors'][0], msg) diff --git a/tests/test_utils.py b/tests/test_utils.py index 09a85934..c19d91cf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,10 +1,14 @@ import json +import time import base64 +import jwt.exceptions from django.contrib.auth import get_user_model +from django.utils.timezone import now from django.test import TestCase -import jwt.exceptions + from rest_framework_jwt import utils +from rest_framework_jwt.models import JWTBlackListToken from rest_framework_jwt.settings import api_settings, DEFAULTS User = get_user_model() @@ -57,21 +61,56 @@ def test_jwt_response_payload(self): self.assertEqual(response_data, dict(token=token)) - def test_jwt_decode_verify_exp(self): - api_settings.JWT_VERIFY_EXPIRATION = False + def test_jwt_blacklist_get_success(self): + api_settings.JWT_ENABLE_BLACKLIST = True payload = utils.jwt_payload_handler(self.user) - payload['exp'] = 1 - token = utils.jwt_encode_handler(payload) - utils.jwt_decode_handler(token) - api_settings.JWT_VERIFY_EXPIRATION = True + # Create blacklisted token. + token_created = JWTBlackListToken.objects.create( + jti=payload.get('jti'), + expires=now(), + created=now() + ) + + token_fetched = utils.jwt_blacklist_get_handler(payload) + + self.assertEqual(token_created.jti, token_fetched.jti) + + def test_jwt_blacklist_get_fail(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + payload = utils.jwt_payload_handler(self.user) + + # Test that incoming empty jti fails. + payload['jti'] = None + + token_fetched = utils.jwt_blacklist_get_handler(payload) + + self.assertIsNone(token_fetched) + + def test_jwt_blacklist_set_success(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + payload = utils.jwt_payload_handler(self.user) + + # exp field comes in as seconds since epoch + payload['exp'] = int(time.time()) + + # Create blacklisted token. + token = utils.jwt_blacklist_set_handler(payload) + + self.assertEqual(token.jti, payload.get('jti')) + + def test_jwt_blacklist_set_fail(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + payload = utils.jwt_payload_handler(self.user) - def test_jti_blacklist(self): - pass + # Create blacklisted token. + token = utils.jwt_blacklist_set_handler(payload) - def test_fail_blacklist_without_jti(self): - pass + self.assertIsNone(token) class TestAudience(TestCase): diff --git a/tests/test_views.py b/tests/test_views.py index bdff723f..568d5c5b 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -28,7 +28,7 @@ (r'^auth-token-refresh/$', 'rest_framework_jwt.views.refresh_jwt_token'), (r'^auth-token-verify/$', 'rest_framework_jwt.views.verify_jwt_token'), (r'^auth-token-blacklist/$', - 'rest_framework_jwt.views.blacklist_jwt_token'), + 'rest_framework_jwt.views.blacklist_jwt_token'), ) orig_datetime = datetime @@ -304,7 +304,29 @@ def test_verify_jwt_fails_with_blacklisted_token(self): """ Test that a blacklisted token will fail. """ - pass + api_settings.JWT_ENABLE_BLACKLIST = True + + client = APIClient(enforce_csrf_checks=True) + + user = User.objects.create_user( + email='jsmith@example.com', username='jsmith', password='password') + + token = self.create_token(user) + + # Handle blacklisting the token. + response = client.post('/auth-token-blacklist/', {'token': token}, + format='json') + + msg = 'Token successfully blacklisted.' + + self.assertEqual(response.data['message'], msg) + + response = client.post('/auth-token-verify/', {'token': token}, + format='json') + + msg = 'Token is blacklisted.' + + self.assertEqual(response.data['detail'], msg) class RefreshJSONWebTokenTests(TokenTestCase): @@ -369,3 +391,42 @@ def test_refresh_jwt_after_refresh_expiration(self): def tearDown(self): # Restore original settings api_settings.JWT_ALLOW_REFRESH = DEFAULTS['JWT_ALLOW_REFRESH'] + + +class BlacklistJSONWebTokenTests(TokenTestCase): + + def test_blacklist_jwt_successful_blacklist_enabled(self): + api_settings.JWT_ENABLE_BLACKLIST = True + + client = APIClient(enforce_csrf_checks=True) + + user = User.objects.create_user( + email='jsmith@example.com', username='jsmith', password='password') + + token = self.create_token(user) + + # Handle blacklisting the token. + response = client.post('/auth-token-blacklist/', {'token': token}, + format='json') + + msg = 'Token successfully blacklisted.' + + self.assertEqual(response.data['message'], msg) + + def test_blacklist_jwt_fails_blacklist_disabled(self): + api_settings.JWT_ENABLE_BLACKLIST = False + + client = APIClient(enforce_csrf_checks=True) + + user = User.objects.create_user( + email='jsmith@example.com', username='jsmith', password='password') + + token = self.create_token(user) + + # Handle blacklisting the token. + response = client.post('/auth-token-blacklist/', {'token': token}, + format='json') + + msg = 'JWT_ENABLE_BLACKLIST is set to False.' + + self.assertEqual(response.data['non_field_errors'][0], msg) From cc3c32a370040f1bb5bf42dc508caea32a343f8e Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 31 Mar 2015 23:38:57 -0700 Subject: [PATCH 80/93] Updated documentation and did another pass through cleanup --- docs/index.md | 81 +++++++++++++++++++++++++++++++ rest_framework_jwt/admin.py | 4 +- rest_framework_jwt/models.py | 2 +- rest_framework_jwt/serializers.py | 4 +- rest_framework_jwt/utils.py | 8 +-- tests/test_authentication.py | 4 +- tests/test_utils.py | 4 +- 7 files changed, 94 insertions(+), 13 deletions(-) diff --git a/docs/index.md b/docs/index.md index b5300ec4..8b0911d5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -101,6 +101,7 @@ 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. + ## 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. @@ -116,6 +117,28 @@ Passing a token to the verification endpoint will return a 200 response and the $ curl -X POST -H "Content-Type: application/json" -d '{"token":""}' http://localhost:8000/api-token-verify/ ``` +## Blacklist Token +If `JWT_ENABLE_BLACKLIST` is True, tokens can be made invalid (prior to expiration) by blacklisting them. More information on the concept of JTI (JWT ID) and blacklisting tokens can be read [here](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#jtiDef) and [here](https://auth0.com/blog/2015/03/10/blacklist-json-web-token-api-keys/). + +This package comes with a default implementation that stores the blacklisted tokens in the configured django database and includes an admin integration. + +To use this feature, add a URL like so: + +```python + url(r'^api-token-blacklist/', 'rest_framework_jwt.views.blacklist_jwt_token'), +``` + +Now to blacklist a token, send a POST request with a non-expired token to the blacklist endpoint: + +```bash +$ curl -X POST -H "Content-Type: application/json" -d '{"token":""}' http://localhost:8000/api-token-blacklist/ +``` + +If the blacklisting was successful, the response will contain the default implementation response data which is the token and a success message. + +The typical use case for this feature is forcefully logging a user out due to inactivity. Many applications, especially ones with sensitive information, may implement an activity-based countdown timer and wish to instantly inactivate the user's auth token. The default implementation stores a record in the database with the unique JTI (JWT ID). However there are configurable handlers for getting and setting the blacklisted token which leaves it up to the user to decide how or where they are stored. The only requirement are that both `JWT_BLACKLIST_GET_HANDLER` and `JWT_BLACKLIST_SET_HANDLER` return a valid blacklisted token or None. + + ## Additional Settings There are some additional settings that you can override similar to how you'd do it with Django REST framework itself. Here are all the available defaults. @@ -136,6 +159,15 @@ JWT_AUTH = { 'JWT_RESPONSE_PAYLOAD_HANDLER': 'rest_framework_jwt.utils.jwt_response_payload_handler', + 'JWT_BLACKLIST_GET_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_get_handler', + + 'JWT_BLACKLIST_SET_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_set_handler', + + 'JWT_BLACKLIST_RESPONSE_HANDLER': + 'rest_framework_jwt.utils.jwt_blacklist_response_handler', + 'JWT_SECRET_KEY': settings.SECRET_KEY, 'JWT_ALGORITHM': 'HS256', 'JWT_VERIFY': True, @@ -149,6 +181,8 @@ JWT_AUTH = { 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), 'JWT_AUTH_HEADER_PREFIX': 'JWT', + + 'JWT_ENABLE_BLACKLIST': False, } ``` This packages uses the JSON Web Token Python implementation, [PyJWT](https://github.com/jpadilla/pyjwt) and allows to modify some of it's available options. @@ -227,6 +261,53 @@ def jwt_response_payload_handler(token, user=None, request=None): Default is `{'token': token}` +### JWT_BLACKLIST_GET_HANDLER +Responsible for fetching a blacklisted JWT token. This function should return either a valid blacklisted token or None. + +The default included implementation is as follows: +``` +def jwt_blacklist_get_handler(payload): + jti = payload.get('jti') + + try: + token = models.JWTBlackListToken.objects.get(jti=jti) + except models.JWTBlackListToken.DoesNotExist: + return None + else: + return token +``` + +### JWT_BLACKLIST_SET_HANDLER +Responsible for setting a blacklisted JWT token. This function should return either a valid blacklisted token or None. + +The default included implementation is as follows: +``` +def jwt_blacklist_set_handler(payload): + try: + data = { + 'jti': payload.get('jti'), + 'created': now(), + 'expires': datetime.fromtimestamp(payload.get('exp')) + } + return models.JWTBlackListToken.objects.create(\**data) + except (TypeError, IntegrityError, Exception): + return None +``` + +### JWT_BLACKLIST_RESPONSE_HANDLER +Controls what the response data for a request to the JWT blacklist endpoint returns. + +The default implementation is as follows: +``` +def jwt_blacklist_response_handler(token, user=None, request=None): + from . import serializers + + return { + 'token': serializers.JWTBlackListTokenSerializer(token).data, + 'message': _('Token successfully blacklisted.') + } +``` + ### JWT_AUTH_HEADER_PREFIX You can modify the Authorization header value prefix that is required to be sent together with the token. The default value is `JWT`. This decision was introduced in PR [#4](https://github.com/GetBlimp/django-rest-framework-jwt/pull/4) to allow using both this package and OAuth2 in DRF. diff --git a/rest_framework_jwt/admin.py b/rest_framework_jwt/admin.py index 6aaa9653..cf41026b 100644 --- a/rest_framework_jwt/admin.py +++ b/rest_framework_jwt/admin.py @@ -5,7 +5,7 @@ from . import models -class JWTBlackListTokenAdmin(admin.ModelAdmin): +class JWTBlacklistTokenAdmin(admin.ModelAdmin): list_display = ('jti', 'expires', 'created', 'is_expired') fields = ('jti', 'expires', 'created', 'is_expired') readonly_fields = ('jti', 'expires', 'created', 'is_expired') @@ -15,4 +15,4 @@ def is_expired(self, obj): is_expired.boolean = True if api_settings.JWT_ENABLE_BLACKLIST: - admin.site.register(models.JWTBlackListToken, JWTBlackListTokenAdmin) + admin.site.register(models.JWTBlacklistToken, JWTBlacklistTokenAdmin) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 3e4de5f3..105161ce 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -7,7 +7,7 @@ UUIDField = get_uuid_field() -class JWTBlackListToken(models.Model): +class JWTBlacklistToken(models.Model): jti = UUIDField() expires = models.DateTimeField() created = models.DateTimeField(auto_now_add=True) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 3a38bcc4..82994af3 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -224,11 +224,11 @@ def validate(self, attrs): } -class JWTBlackListTokenSerializer(serializers.ModelSerializer): +class JWTBlacklistTokenSerializer(serializers.ModelSerializer): jti = serializers.SerializerMethodField('get_jti_value') class Meta: - model = models.JWTBlackListToken + model = models.JWTBlacklistToken def get_jti_value(self, obj): """Returns obj.jti manually due to py3 bug in django-uuidfield""" diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index b8439a12..7db71244 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -100,8 +100,8 @@ def jwt_blacklist_get_handler(payload): jti = payload.get('jti') try: - token = models.JWTBlackListToken.objects.get(jti=jti) - except models.JWTBlackListToken.DoesNotExist: + token = models.JWTBlacklistToken.objects.get(jti=jti) + except models.JWTBlacklistToken.DoesNotExist: return None else: return token @@ -119,7 +119,7 @@ def jwt_blacklist_set_handler(payload): 'created': now(), 'expires': datetime.fromtimestamp(payload.get('exp')) } - return models.JWTBlackListToken.objects.create(**data) + return models.JWTBlacklistToken.objects.create(**data) except (TypeError, IntegrityError, Exception): return None @@ -132,6 +132,6 @@ def jwt_blacklist_response_handler(token, user=None, request=None): from . import serializers return { - 'token': serializers.JWTBlackListTokenSerializer(token).data, + 'token': serializers.JWTBlacklistTokenSerializer(token).data, 'message': _('Token successfully blacklisted.') } diff --git a/tests/test_authentication.py b/tests/test_authentication.py index dd4bf304..e391021a 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -35,7 +35,7 @@ from rest_framework.views import APIView from rest_framework_jwt import utils -from rest_framework_jwt.models import JWTBlackListToken +from rest_framework_jwt.models import JWTBlacklistToken from rest_framework_jwt.settings import api_settings, DEFAULTS from rest_framework_jwt.authentication import JSONWebTokenAuthentication @@ -119,7 +119,7 @@ def test_post_blacklisted_token_failing_jwt_auth(self): token = utils.jwt_encode_handler(payload) # Create blacklist token which effectively blacklists the token. - JWTBlackListToken.objects.create(jti=payload.get('jti'), + JWTBlacklistToken.objects.create(jti=payload.get('jti'), created=now(), expires=now()) auth = 'JWT {0}'.format(token) diff --git a/tests/test_utils.py b/tests/test_utils.py index c19d91cf..33990f3c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,7 +8,7 @@ from django.test import TestCase from rest_framework_jwt import utils -from rest_framework_jwt.models import JWTBlackListToken +from rest_framework_jwt.models import JWTBlacklistToken from rest_framework_jwt.settings import api_settings, DEFAULTS User = get_user_model() @@ -67,7 +67,7 @@ def test_jwt_blacklist_get_success(self): payload = utils.jwt_payload_handler(self.user) # Create blacklisted token. - token_created = JWTBlackListToken.objects.create( + token_created = JWTBlacklistToken.objects.create( jti=payload.get('jti'), expires=now(), created=now() From 62efabc5f68e494b193764ef32a877d4aff00f08 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 31 Mar 2015 23:50:52 -0700 Subject: [PATCH 81/93] Proofread docs --- docs/index.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 8b0911d5..0af92b5d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -134,9 +134,9 @@ Now to blacklist a token, send a POST request with a non-expired token to the bl $ curl -X POST -H "Content-Type: application/json" -d '{"token":""}' http://localhost:8000/api-token-blacklist/ ``` -If the blacklisting was successful, the response will contain the default implementation response data which is the token and a success message. +If the blacklisting was successful, the response will contain the default implementation response data which is the token and a success message. Any future requests using that token will be denied. -The typical use case for this feature is forcefully logging a user out due to inactivity. Many applications, especially ones with sensitive information, may implement an activity-based countdown timer and wish to instantly inactivate the user's auth token. The default implementation stores a record in the database with the unique JTI (JWT ID). However there are configurable handlers for getting and setting the blacklisted token which leaves it up to the user to decide how or where they are stored. The only requirement are that both `JWT_BLACKLIST_GET_HANDLER` and `JWT_BLACKLIST_SET_HANDLER` return a valid blacklisted token or None. +The typical use case for this feature is forcefully logging a user out due to inactivity. Many applications, especially ones with sensitive information, may implement an activity-based countdown timer and wish to instantly inactivate the user's auth token. The default implementation stores a record in the database with the unique JTI (JWT ID). However there are configurable handlers for getting and setting the blacklisted token which leaves it up to the user to decide how or where they are stored. The only requirements are that both `JWT_BLACKLIST_GET_HANDLER` and `JWT_BLACKLIST_SET_HANDLER` return a valid blacklisted token or None. ## Additional Settings @@ -242,6 +242,9 @@ Default is `datetime.timedelta(days=7)` (7 days). ### JWT_PAYLOAD_HANDLER Specify a custom function to generate the token payload +**Note** +If you have `JWT_ENABLE_BLACKLIST` set to True, *AND* you are using the default blacklist implementation, any custom token payload must include both a `jti` attribute which is a unique UUID hex, and a `exp` attribute which is a timestamp from a POSIX time (e.g. seconds since epoch). + ### JWT_PAYLOAD_GET_USER_ID_HANDLER If you store `user_id` differently than the default payload handler does, implement this function to fetch `user_id` from the payload. From 869e902b12a5b2e369269ac46f97d190d2edbf15 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Wed, 1 Apr 2015 10:09:20 -0700 Subject: [PATCH 82/93] Update index.md --- docs/index.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index 0af92b5d..aba3d303 100644 --- a/docs/index.md +++ b/docs/index.md @@ -271,7 +271,6 @@ The default included implementation is as follows: ``` def jwt_blacklist_get_handler(payload): jti = payload.get('jti') - try: token = models.JWTBlackListToken.objects.get(jti=jti) except models.JWTBlackListToken.DoesNotExist: @@ -292,7 +291,7 @@ def jwt_blacklist_set_handler(payload): 'created': now(), 'expires': datetime.fromtimestamp(payload.get('exp')) } - return models.JWTBlackListToken.objects.create(\**data) + return models.JWTBlackListToken.objects.create(**data) except (TypeError, IntegrityError, Exception): return None ``` @@ -303,11 +302,9 @@ Controls what the response data for a request to the JWT blacklist endpoint retu The default implementation is as follows: ``` def jwt_blacklist_response_handler(token, user=None, request=None): - from . import serializers - return { - 'token': serializers.JWTBlackListTokenSerializer(token).data, - 'message': _('Token successfully blacklisted.') + 'token': JWTBlackListTokenSerializer(token).data, + 'message': 'Token successfully blacklisted.' } ``` From 5ca120d9b7d6769dcb88ce479088761fa71887da Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Wed, 1 Apr 2015 10:10:56 -0700 Subject: [PATCH 83/93] Update index.md --- docs/index.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/index.md b/docs/index.md index aba3d303..7a6aa526 100644 --- a/docs/index.md +++ b/docs/index.md @@ -308,6 +308,11 @@ def jwt_blacklist_response_handler(token, user=None, request=None): } ``` +### JWT_ENABLE_BLACKLIST +Designates whether JWT token blacklisting is turned on or off. + +Defaults to False. + ### JWT_AUTH_HEADER_PREFIX You can modify the Authorization header value prefix that is required to be sent together with the token. The default value is `JWT`. This decision was introduced in PR [#4](https://github.com/GetBlimp/django-rest-framework-jwt/pull/4) to allow using both this package and OAuth2 in DRF. From a12f8e720ed3d3942cd1a86a0df9abce465960ca Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Wed, 1 Apr 2015 10:23:20 -0700 Subject: [PATCH 84/93] Changed model method to is_active --- rest_framework_jwt/admin.py | 12 ++++++------ rest_framework_jwt/models.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rest_framework_jwt/admin.py b/rest_framework_jwt/admin.py index cf41026b..9a41e408 100644 --- a/rest_framework_jwt/admin.py +++ b/rest_framework_jwt/admin.py @@ -6,13 +6,13 @@ class JWTBlacklistTokenAdmin(admin.ModelAdmin): - list_display = ('jti', 'expires', 'created', 'is_expired') - fields = ('jti', 'expires', 'created', 'is_expired') - readonly_fields = ('jti', 'expires', 'created', 'is_expired') + list_display = ('jti', 'expires', 'created', 'is_active') + fields = ('jti', 'expires', 'created', 'is_active') + readonly_fields = ('jti', 'expires', 'created', 'is_active') - def is_expired(self, obj): - return obj.is_expired() - is_expired.boolean = True + def is_active(self, obj): + return obj.is_active() + is_active.boolean = True if api_settings.JWT_ENABLE_BLACKLIST: admin.site.register(models.JWTBlacklistToken, JWTBlacklistTokenAdmin) diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 105161ce..9e6e0223 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -16,5 +16,5 @@ class Meta: verbose_name = _('JWT Blacklist Token') verbose_name_plural = _('JWT Blacklist Tokens') - def is_expired(self): - return self.expires < now() + def is_active(self): + return self.expires > now() From 73d3d1af86b3afcde7e15b9639b2898360173a31 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Wed, 1 Apr 2015 10:23:59 -0700 Subject: [PATCH 85/93] Changed model method to is_active --- rest_framework_jwt/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework_jwt/admin.py b/rest_framework_jwt/admin.py index 9a41e408..968519d0 100644 --- a/rest_framework_jwt/admin.py +++ b/rest_framework_jwt/admin.py @@ -13,6 +13,7 @@ class JWTBlacklistTokenAdmin(admin.ModelAdmin): def is_active(self, obj): return obj.is_active() is_active.boolean = True + is_active.short_description = 'Active' if api_settings.JWT_ENABLE_BLACKLIST: admin.site.register(models.JWTBlacklistToken, JWTBlacklistTokenAdmin) From f0f6b5706b4932c4e1faa20717a76e5ea1b0a2ef Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Fri, 3 Apr 2015 14:33:38 -0700 Subject: [PATCH 86/93] Separated blacklist token feature into subapp --- docs/index.md | 4 +++- rest_framework_jwt/blacklist/__init__.py | 0 rest_framework_jwt/{ => blacklist}/admin.py | 0 rest_framework_jwt/{ => blacklist}/models.py | 2 +- rest_framework_jwt/blacklist/serializers.py | 14 ++++++++++++++ rest_framework_jwt/serializers.py | 13 ------------- rest_framework_jwt/utils.py | 6 ++---- tests/conftest.py | 1 + tests/test_authentication.py | 2 +- tests/test_utils.py | 2 +- 10 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 rest_framework_jwt/blacklist/__init__.py rename rest_framework_jwt/{ => blacklist}/admin.py (100%) rename rest_framework_jwt/{ => blacklist}/models.py (90%) create mode 100644 rest_framework_jwt/blacklist/serializers.py diff --git a/docs/index.md b/docs/index.md index 7a6aa526..5cdafd01 100644 --- a/docs/index.md +++ b/docs/index.md @@ -118,7 +118,7 @@ $ curl -X POST -H "Content-Type: application/json" -d '{"token":" Date: Fri, 3 Apr 2015 17:05:03 -0700 Subject: [PATCH 87/93] Separated blacklist view and utils into the subapp, updated docs --- docs/index.md | 2 +- rest_framework_jwt/blacklist/serializers.py | 43 ++++++++++++++--- rest_framework_jwt/blacklist/utils.py | 52 +++++++++++++++++++++ rest_framework_jwt/blacklist/views.py | 19 ++++++++ rest_framework_jwt/serializers.py | 31 ------------ rest_framework_jwt/settings.py | 6 +-- rest_framework_jwt/utils.py | 50 -------------------- rest_framework_jwt/views.py | 10 ---- tests/test_serializers.py | 2 +- tests/test_utils.py | 9 ++-- tests/test_views.py | 2 +- 11 files changed, 119 insertions(+), 107 deletions(-) create mode 100644 rest_framework_jwt/blacklist/utils.py create mode 100644 rest_framework_jwt/blacklist/views.py diff --git a/docs/index.md b/docs/index.md index 5cdafd01..6ab93d99 100644 --- a/docs/index.md +++ b/docs/index.md @@ -139,7 +139,7 @@ If the blacklisting was successful, the response will contain the default implem The typical use case for this feature is forcefully logging a user out due to inactivity. Many applications, especially ones with sensitive information, may implement an activity-based countdown timer and wish to instantly inactivate the user's auth token. The default implementation stores a record in the database with the unique JTI (JWT ID). However there are configurable handlers for getting and setting the blacklisted token which leaves it up to the user to decide how or where they are stored. The only requirements are that both `JWT_BLACKLIST_GET_HANDLER` and `JWT_BLACKLIST_SET_HANDLER` return a valid blacklisted token or None. **Note** -Applications that are built with a Serivce-Oriented-Architecture (SOA) may not be able to use the blacklist token feature. +Applications that are built with a Serivce-Oriented-Architecture (SOA) may not be able to use the blacklist token feature due to having to query the auth service on every request to check if the JWT is blacklisted. ## Additional Settings There are some additional settings that you can override similar to how you'd do it with Django REST framework itself. Here are all the available defaults. diff --git a/rest_framework_jwt/blacklist/serializers.py b/rest_framework_jwt/blacklist/serializers.py index 78892886..34a3666f 100644 --- a/rest_framework_jwt/blacklist/serializers.py +++ b/rest_framework_jwt/blacklist/serializers.py @@ -1,14 +1,45 @@ +from django.utils.translation import ugettext as _ + from rest_framework import serializers +from rest_framework_jwt.settings import api_settings +from rest_framework_jwt.serializers import VerificationBaseSerializer + from . import models +jwt_blacklist_set_handler = api_settings.JWT_BLACKLIST_SET_HANDLER -class JWTBlacklistTokenSerializer(serializers.ModelSerializer): - jti = serializers.SerializerMethodField('get_jti_value') +class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): + """ + Blacklist an access token. + """ + + def validate(self, attrs): + + token = attrs['token'] + + if not api_settings.JWT_ENABLE_BLACKLIST: + msg = _('JWT_ENABLE_BLACKLIST is set to False.') + raise serializers.ValidationError(msg) + + payload = self._check_payload(token=token) + + # Handle blacklisting a token. + token = jwt_blacklist_set_handler(payload) + + if not token: + msg = _('Could not blacklist token.') + raise serializers.ValidationError(msg) + + user = self._check_user(payload=payload) + + return { + 'token': token, + 'user': user + } + + +class JWTBlacklistTokenSerializer(serializers.ModelSerializer): class Meta: model = models.JWTBlacklistToken - - def get_jti_value(self, obj): - """Returns obj.jti manually due to py3 bug in django-uuidfield""" - return obj.jti diff --git a/rest_framework_jwt/blacklist/utils.py b/rest_framework_jwt/blacklist/utils.py new file mode 100644 index 00000000..bbe7c387 --- /dev/null +++ b/rest_framework_jwt/blacklist/utils.py @@ -0,0 +1,52 @@ +from datetime import datetime + +from django.db import IntegrityError +from django.utils.timezone import now +from django.utils.translation import ugettext_lazy as _ + +from . import models + + +def jwt_blacklist_get_handler(payload): + """ + Default implementation to check if a blacklisted jwt token exists. + + Should return a black listed token or None. + """ + jti = payload.get('jti') + + try: + token = models.JWTBlacklistToken.objects.get(jti=jti) + except models.JWTBlacklistToken.DoesNotExist: + return None + else: + return token + + +def jwt_blacklist_set_handler(payload): + """ + Default implementation that blacklists a jwt token. + + Should return a black listed token or None. + """ + try: + data = { + 'jti': payload.get('jti'), + 'created': now(), + 'expires': datetime.fromtimestamp(payload.get('exp')) + } + return models.JWTBlacklistToken.objects.create(**data) + except (TypeError, IntegrityError, Exception): + return None + + +def jwt_blacklist_response_handler(token, user=None, request=None): + """ + Default blacklist token response data. Override to provide a + custom response. + """ + from . import serializers + return { + 'token': serializers.JWTBlacklistTokenSerializer(token).data, + 'message': _('Token successfully blacklisted.') + } diff --git a/rest_framework_jwt/blacklist/views.py b/rest_framework_jwt/blacklist/views.py new file mode 100644 index 00000000..d5c75f81 --- /dev/null +++ b/rest_framework_jwt/blacklist/views.py @@ -0,0 +1,19 @@ +from rest_framework_jwt.settings import api_settings +from rest_framework_jwt.views import JSONWebTokenAPIView + +from . import serializers + +jwt_blacklist_response_handler = api_settings.JWT_BLACKLIST_RESPONSE_HANDLER + + +class BlacklistJSONWebToken(JSONWebTokenAPIView): + """ + API View that blacklists a token + """ + serializer_class = serializers.BlacklistJSONWebTokenSerializer + response_payload_handler = staticmethod( + jwt_blacklist_response_handler + ) + + +blacklist_jwt_token = BlacklistJSONWebToken.as_view() diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index fba95007..305a2e26 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -18,7 +18,6 @@ jwt_decode_handler = api_settings.JWT_DECODE_HANDLER jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER jwt_blacklist_get_handler = api_settings.JWT_BLACKLIST_GET_HANDLER -jwt_blacklist_set_handler = api_settings.JWT_BLACKLIST_SET_HANDLER class JSONWebTokenSerializer(Serializer): @@ -190,33 +189,3 @@ def validate(self, attrs): 'token': jwt_encode_handler(new_payload), 'user': user } - - -class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): - """ - Blacklist an access token. - """ - - def validate(self, attrs): - - token = attrs['token'] - - if not api_settings.JWT_ENABLE_BLACKLIST: - msg = _('JWT_ENABLE_BLACKLIST is set to False.') - raise serializers.ValidationError(msg) - - payload = self._check_payload(token=token) - - # Handle blacklisting a token. - token = jwt_blacklist_set_handler(payload) - - if not token: - msg = _('Could not blacklist token.') - raise serializers.ValidationError(msg) - - user = self._check_user(payload=payload) - - return { - 'token': token, - 'user': user - } diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index c9ec568c..67729bb6 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -23,13 +23,13 @@ 'rest_framework_jwt.utils.jwt_response_payload_handler', 'JWT_BLACKLIST_GET_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_get_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_get_handler', 'JWT_BLACKLIST_SET_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_set_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_set_handler', 'JWT_BLACKLIST_RESPONSE_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_response_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_response_handler', 'JWT_SECRET_KEY': settings.SECRET_KEY, 'JWT_ALGORITHM': 'HS256', diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index ef9b49e4..3877a147 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -1,15 +1,9 @@ import jwt import uuid -from django.db import IntegrityError -from django.utils.timezone import now -from django.utils.translation import ugettext_lazy as _ - from datetime import datetime from rest_framework_jwt.settings import api_settings -from rest_framework_jwt.blacklist import models -from rest_framework_jwt.blacklist import serializers def get_user_model(): @@ -89,47 +83,3 @@ def jwt_response_payload_handler(token, user=None, request=None): return { 'token': token } - - -def jwt_blacklist_get_handler(payload): - """ - Default implementation to check if a blacklisted jwt token exists. - - Should return a black listed token or None. - """ - jti = payload.get('jti') - - try: - token = models.JWTBlacklistToken.objects.get(jti=jti) - except models.JWTBlacklistToken.DoesNotExist: - return None - else: - return token - - -def jwt_blacklist_set_handler(payload): - """ - Default implementation that blacklists a jwt token. - - Should return a black listed token or None. - """ - try: - data = { - 'jti': payload.get('jti'), - 'created': now(), - 'expires': datetime.fromtimestamp(payload.get('exp')) - } - return models.JWTBlacklistToken.objects.create(**data) - except (TypeError, IntegrityError, Exception): - return None - - -def jwt_blacklist_response_handler(token, user=None, request=None): - """ - Default blacklist token response data. Override to provide a - custom response. - """ - return { - 'token': serializers.JWTBlacklistTokenSerializer(token).data, - 'message': _('Token successfully blacklisted.') - } diff --git a/rest_framework_jwt/views.py b/rest_framework_jwt/views.py index a067091e..ed8c09ee 100644 --- a/rest_framework_jwt/views.py +++ b/rest_framework_jwt/views.py @@ -9,7 +9,6 @@ from . import serializers jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER -jwt_blacklist_response_handler = api_settings.JWT_BLACKLIST_RESPONSE_HANDLER class JSONWebTokenAPIView(APIView): @@ -64,15 +63,6 @@ class RefreshJSONWebToken(JSONWebTokenAPIView): serializer_class = serializers.RefreshJSONWebTokenSerializer -class BlacklistJSONWebToken(JSONWebTokenAPIView): - """ - API View that blacklists a token - """ - serializer_class = serializers.BlacklistJSONWebTokenSerializer - response_payload_handler = staticmethod(jwt_blacklist_response_handler) - - obtain_jwt_token = ObtainJSONWebToken.as_view() refresh_jwt_token = RefreshJSONWebToken.as_view() verify_jwt_token = VerifyJSONWebToken.as_view() -blacklist_jwt_token = BlacklistJSONWebToken.as_view() diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 8df52c08..07b386b4 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -7,7 +7,7 @@ from rest_framework_jwt.settings import api_settings from rest_framework_jwt.serializers import JSONWebTokenSerializer -from rest_framework_jwt.serializers import BlacklistJSONWebTokenSerializer +from rest_framework_jwt.blacklist.serializers import BlacklistJSONWebTokenSerializer from rest_framework_jwt import utils User = get_user_model() diff --git a/tests/test_utils.py b/tests/test_utils.py index 31377608..4a9324db 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -9,6 +9,7 @@ from rest_framework_jwt import utils from rest_framework_jwt.settings import api_settings, DEFAULTS +from rest_framework_jwt.blacklist import utils as blacklist_utils from rest_framework_jwt.blacklist.models import JWTBlacklistToken User = get_user_model() @@ -73,7 +74,7 @@ def test_jwt_blacklist_get_success(self): created=now() ) - token_fetched = utils.jwt_blacklist_get_handler(payload) + token_fetched = blacklist_utils.jwt_blacklist_get_handler(payload) self.assertEqual(token_created.jti, token_fetched.jti) @@ -85,7 +86,7 @@ def test_jwt_blacklist_get_fail(self): # Test that incoming empty jti fails. payload['jti'] = None - token_fetched = utils.jwt_blacklist_get_handler(payload) + token_fetched = blacklist_utils.jwt_blacklist_get_handler(payload) self.assertIsNone(token_fetched) @@ -98,7 +99,7 @@ def test_jwt_blacklist_set_success(self): payload['exp'] = int(time.time()) # Create blacklisted token. - token = utils.jwt_blacklist_set_handler(payload) + token = blacklist_utils.jwt_blacklist_set_handler(payload) self.assertEqual(token.jti, payload.get('jti')) @@ -108,7 +109,7 @@ def test_jwt_blacklist_set_fail(self): payload = utils.jwt_payload_handler(self.user) # Create blacklisted token. - token = utils.jwt_blacklist_set_handler(payload) + token = blacklist_utils.jwt_blacklist_set_handler(payload) self.assertIsNone(token) diff --git a/tests/test_views.py b/tests/test_views.py index 568d5c5b..6b2baf88 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -28,7 +28,7 @@ (r'^auth-token-refresh/$', 'rest_framework_jwt.views.refresh_jwt_token'), (r'^auth-token-verify/$', 'rest_framework_jwt.views.verify_jwt_token'), (r'^auth-token-blacklist/$', - 'rest_framework_jwt.views.blacklist_jwt_token'), + 'rest_framework_jwt.blacklist.views.blacklist_jwt_token'), ) orig_datetime = datetime From ce72d610c313a19ff7eb7e8867f4901cf740ac42 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sat, 4 Apr 2015 10:22:01 -0700 Subject: [PATCH 88/93] Removed JWT_ENABLE_BLACKLIST - tests broken --- rest_framework_jwt/authentication.py | 3 ++- rest_framework_jwt/blacklist/serializers.py | 5 +++-- rest_framework_jwt/serializers.py | 3 ++- rest_framework_jwt/settings.py | 1 - tests/test_authentication.py | 7 ------- tests/test_serializers.py | 4 ---- tests/test_utils.py | 8 -------- tests/test_views.py | 7 ++----- 8 files changed, 9 insertions(+), 29 deletions(-) diff --git a/rest_framework_jwt/authentication.py b/rest_framework_jwt/authentication.py index fe0152cd..90b4f261 100644 --- a/rest_framework_jwt/authentication.py +++ b/rest_framework_jwt/authentication.py @@ -1,5 +1,6 @@ import jwt +from django.conf import settings from django.utils.encoding import smart_text from django.utils.translation import ugettext as _ @@ -42,7 +43,7 @@ def authenticate(self, request): raise exceptions.AuthenticationFailed() # Check if the token has been blacklisted. - if api_settings.JWT_ENABLE_BLACKLIST: + if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS: blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: diff --git a/rest_framework_jwt/blacklist/serializers.py b/rest_framework_jwt/blacklist/serializers.py index 34a3666f..a58968b9 100644 --- a/rest_framework_jwt/blacklist/serializers.py +++ b/rest_framework_jwt/blacklist/serializers.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.utils.translation import ugettext as _ from rest_framework import serializers @@ -19,8 +20,8 @@ def validate(self, attrs): token = attrs['token'] - if not api_settings.JWT_ENABLE_BLACKLIST: - msg = _('JWT_ENABLE_BLACKLIST is set to False.') + if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS: + msg = _('The blacklist app is not installed.') raise serializers.ValidationError(msg) payload = self._check_payload(token=token) diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 305a2e26..e8019f0d 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -3,6 +3,7 @@ from calendar import timegm from datetime import datetime, timedelta +from django.conf import settings from django.contrib.auth import authenticate from django.utils.translation import ugettext as _ @@ -106,7 +107,7 @@ def _check_payload(self, token): raise serializers.ValidationError(msg) # Check if the token has been blacklisted. - if api_settings.JWT_ENABLE_BLACKLIST: + if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS: blacklisted = jwt_blacklist_get_handler(payload) if blacklisted: diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index 67729bb6..e13f7128 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -42,7 +42,6 @@ 'JWT_ALLOW_REFRESH': False, 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), 'JWT_AUTH_HEADER_PREFIX': 'JWT', - 'JWT_ENABLE_BLACKLIST': False, } # List of settings that may be in string import notation. diff --git a/tests/test_authentication.py b/tests/test_authentication.py index d0ce8ec4..a25a70e9 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -96,7 +96,6 @@ def test_post_json_passing_jwt_auth_blacklist_enabled(self): Ensure POSTing JSON over JWT auth with correct credentials passes and does not require CSRF """ - api_settings.JWT_ENABLE_BLACKLIST = True payload = utils.jwt_payload_handler(self.user) token = utils.jwt_encode_handler(payload) @@ -107,14 +106,10 @@ def test_post_json_passing_jwt_auth_blacklist_enabled(self): self.assertEqual(response.status_code, status.HTTP_200_OK) - api_settings.JWT_ENABLE_BLACKLIST = False - def test_post_blacklisted_token_failing_jwt_auth(self): """ Ensure POSTing over JWT auth with blacklisted token fails """ - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) token = utils.jwt_encode_handler(payload) @@ -133,8 +128,6 @@ def test_post_blacklisted_token_failing_jwt_auth(self): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response['WWW-Authenticate'], 'JWT realm="api"') - api_settings.JWT_ENABLE_BLACKLIST = False - def test_post_form_passing_jwt_auth(self): """ Ensure POSTing form over JWT auth with correct credentials diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 07b386b4..cb63885c 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -112,8 +112,6 @@ def setUp(self): } def test_token_blacklisted(self): - api_settings.JWT_ENABLE_BLACKLIST = True - serializer = BlacklistJSONWebTokenSerializer(data=self.data) is_valid = serializer.is_valid() @@ -123,8 +121,6 @@ def test_token_blacklisted(self): self.assertEqual(self.payload.get('jti'), token.jti) def test_token_blacklist_fail_missing_jti(self): - api_settings.JWT_ENABLE_BLACKLIST = True - self.payload['jti'] = None self.data = { 'token': utils.jwt_encode_handler(self.payload) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4a9324db..c56d313c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -63,8 +63,6 @@ def test_jwt_response_payload(self): self.assertEqual(response_data, dict(token=token)) def test_jwt_blacklist_get_success(self): - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) # Create blacklisted token. @@ -79,8 +77,6 @@ def test_jwt_blacklist_get_success(self): self.assertEqual(token_created.jti, token_fetched.jti) def test_jwt_blacklist_get_fail(self): - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) # Test that incoming empty jti fails. @@ -91,8 +87,6 @@ def test_jwt_blacklist_get_fail(self): self.assertIsNone(token_fetched) def test_jwt_blacklist_set_success(self): - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) # exp field comes in as seconds since epoch @@ -104,8 +98,6 @@ def test_jwt_blacklist_set_success(self): self.assertEqual(token.jti, payload.get('jti')) def test_jwt_blacklist_set_fail(self): - api_settings.JWT_ENABLE_BLACKLIST = True - payload = utils.jwt_payload_handler(self.user) # Create blacklisted token. diff --git a/tests/test_views.py b/tests/test_views.py index 6b2baf88..bc939e1e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta from django import get_version +from django.conf import settings from django.test import TestCase from django.test.utils import override_settings from django.utils import unittest @@ -304,8 +305,6 @@ def test_verify_jwt_fails_with_blacklisted_token(self): """ Test that a blacklisted token will fail. """ - api_settings.JWT_ENABLE_BLACKLIST = True - client = APIClient(enforce_csrf_checks=True) user = User.objects.create_user( @@ -396,8 +395,6 @@ def tearDown(self): class BlacklistJSONWebTokenTests(TokenTestCase): def test_blacklist_jwt_successful_blacklist_enabled(self): - api_settings.JWT_ENABLE_BLACKLIST = True - client = APIClient(enforce_csrf_checks=True) user = User.objects.create_user( @@ -427,6 +424,6 @@ def test_blacklist_jwt_fails_blacklist_disabled(self): response = client.post('/auth-token-blacklist/', {'token': token}, format='json') - msg = 'JWT_ENABLE_BLACKLIST is set to False.' + msg = 'The blacklist app is not installed.' self.assertEqual(response.data['non_field_errors'][0], msg) From fc994892fb8238783ca6759933b78d4f5b9c08ab Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 5 Apr 2015 10:30:36 -0700 Subject: [PATCH 89/93] Fixed typo in blacklist serializer, removed blacklist disabled test as now it's either installed or not. --- rest_framework_jwt/blacklist/serializers.py | 3 +-- tests/test_serializers.py | 1 - tests/test_views.py | 20 +------------------- 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/rest_framework_jwt/blacklist/serializers.py b/rest_framework_jwt/blacklist/serializers.py index a58968b9..591e4e65 100644 --- a/rest_framework_jwt/blacklist/serializers.py +++ b/rest_framework_jwt/blacklist/serializers.py @@ -15,12 +15,11 @@ class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer): """ Blacklist an access token. """ - def validate(self, attrs): token = attrs['token'] - if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS: + if 'rest_framework_jwt.blacklist' not in settings.INSTALLED_APPS: msg = _('The blacklist app is not installed.') raise serializers.ValidationError(msg) diff --git a/tests/test_serializers.py b/tests/test_serializers.py index cb63885c..1b673102 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -5,7 +5,6 @@ from django.utils import unittest from django.contrib.auth import get_user_model -from rest_framework_jwt.settings import api_settings from rest_framework_jwt.serializers import JSONWebTokenSerializer from rest_framework_jwt.blacklist.serializers import BlacklistJSONWebTokenSerializer from rest_framework_jwt import utils diff --git a/tests/test_views.py b/tests/test_views.py index bc939e1e..d467734f 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta from django import get_version -from django.conf import settings from django.test import TestCase from django.test.utils import override_settings from django.utils import unittest @@ -228,6 +227,7 @@ def create_token(self, user, exp=None, orig_iat=None): payload['orig_iat'] = timegm(orig_iat.utctimetuple()) token = utils.jwt_encode_handler(payload) + return token @@ -409,21 +409,3 @@ def test_blacklist_jwt_successful_blacklist_enabled(self): msg = 'Token successfully blacklisted.' self.assertEqual(response.data['message'], msg) - - def test_blacklist_jwt_fails_blacklist_disabled(self): - api_settings.JWT_ENABLE_BLACKLIST = False - - client = APIClient(enforce_csrf_checks=True) - - user = User.objects.create_user( - email='jsmith@example.com', username='jsmith', password='password') - - token = self.create_token(user) - - # Handle blacklisting the token. - response = client.post('/auth-token-blacklist/', {'token': token}, - format='json') - - msg = 'The blacklist app is not installed.' - - self.assertEqual(response.data['non_field_errors'][0], msg) From e9760dc387c271b4c7e26f02a075fe1fe59e0167 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 5 Apr 2015 17:49:41 -0700 Subject: [PATCH 90/93] Fixed merge requirements issue --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3cfbe45d..1ed7882d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,3 @@ freezegun==0.3.2 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 -djangorestframework-oauth>=1.0.1 From d65459e7510f25f09e592e2dcf5529ab532a2145 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 5 Apr 2015 18:01:40 -0700 Subject: [PATCH 91/93] Update readme --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 6ab93d99..f94e599b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -245,7 +245,7 @@ Default is `datetime.timedelta(days=7)` (7 days). Specify a custom function to generate the token payload **Note** -If you have `JWT_ENABLE_BLACKLIST` set to True, *AND* you are using the default blacklist implementation, any custom token payload must include both a `jti` attribute which is a unique UUID hex, and a `exp` attribute which is a timestamp from a POSIX time (e.g. seconds since epoch). +If you have `rest_framework_jwt.blacklist` added to `INSTALLED_APPS` *AND* you are using the default blacklist implementation, any custom token payload must include both a `jti` attribute which is a unique UUID hex, and a `exp` attribute which is a timestamp from a POSIX time (e.g. seconds since epoch). ### JWT_PAYLOAD_GET_USER_ID_HANDLER If you store `user_id` differently than the default payload handler does, implement this function to fetch `user_id` from the payload. From ae33ba44c85b122cb097cbfd49ffaf96ab1fffd4 Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Tue, 21 Apr 2015 11:36:46 -0700 Subject: [PATCH 92/93] Updated readme and admin --- docs/index.md | 6 +++--- rest_framework_jwt/blacklist/admin.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index f94e599b..02dbdf08 100644 --- a/docs/index.md +++ b/docs/index.md @@ -162,13 +162,13 @@ JWT_AUTH = { 'rest_framework_jwt.utils.jwt_response_payload_handler', 'JWT_BLACKLIST_GET_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_get_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_get_handler', 'JWT_BLACKLIST_SET_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_set_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_set_handler', 'JWT_BLACKLIST_RESPONSE_HANDLER': - 'rest_framework_jwt.utils.jwt_blacklist_response_handler', + 'rest_framework_jwt.blacklist.utils.jwt_blacklist_response_handler', 'JWT_SECRET_KEY': settings.SECRET_KEY, 'JWT_ALGORITHM': 'HS256', diff --git a/rest_framework_jwt/blacklist/admin.py b/rest_framework_jwt/blacklist/admin.py index 968519d0..5153b4d7 100644 --- a/rest_framework_jwt/blacklist/admin.py +++ b/rest_framework_jwt/blacklist/admin.py @@ -1,7 +1,6 @@ +from django.conf import settings from django.contrib import admin -from rest_framework_jwt.settings import api_settings - from . import models @@ -15,5 +14,5 @@ def is_active(self, obj): is_active.boolean = True is_active.short_description = 'Active' -if api_settings.JWT_ENABLE_BLACKLIST: +if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS: admin.site.register(models.JWTBlacklistToken, JWTBlacklistTokenAdmin) From 975811a58d0ba2e1ace567b5bea493aba8a7851c Mon Sep 17 00:00:00 2001 From: Eric Honkanen Date: Sun, 3 May 2015 21:17:46 -0700 Subject: [PATCH 93/93] Clean whitespace --- tests/test_utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index c56d313c..5ae360ef 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -105,6 +105,16 @@ def test_jwt_blacklist_set_fail(self): self.assertIsNone(token) + def test_jwt_decode_verify_exp(self): + api_settings.JWT_VERIFY_EXPIRATION = False + + payload = utils.jwt_payload_handler(self.user) + payload['exp'] = 1 + token = utils.jwt_encode_handler(payload) + utils.jwt_decode_handler(token) + + api_settings.JWT_VERIFY_EXPIRATION = True + class TestAudience(TestCase): def setUp(self):