From f65a5936f9d5136e6fedf9e451ad648a45ab36ab Mon Sep 17 00:00:00 2001 From: Samir Ahmed Date: Sat, 30 Jan 2016 10:43:17 +0600 Subject: [PATCH 01/13] Send out signals upon login and logout --- knox/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/knox/views.py b/knox/views.py index 54682487..92df88fa 100644 --- a/knox/views.py +++ b/knox/views.py @@ -1,3 +1,4 @@ +from django.contrib.auth.signals import user_logged_in, user_logged_out from rest_framework import status from rest_framework.authentication import BasicAuthentication from rest_framework.permissions import IsAuthenticated, AllowAny @@ -17,6 +18,7 @@ class LoginView(APIView): def post(self, request, format=None): token = AuthToken.objects.create(request.user) + user_logged_in.send(sender=request.user.__class__, request=request, user=request.user) return Response({ "user": UserSerializer(request.user).data, "token": token, @@ -28,6 +30,7 @@ class LogoutView(APIView): def post(self, request, format=None): request._auth.delete() + user_logged_out.send(sender=request.user.__class__, request=request, user=request.user) return Response(None, status=status.HTTP_204_NO_CONTENT) class LogoutAllView(APIView): @@ -40,4 +43,5 @@ class LogoutAllView(APIView): def post(self, request, format=None): request.user.auth_token_set.all().delete() + user_logged_out.send(sender=request.user.__class__, request=request, user=request.user) return Response(None, status=status.HTTP_204_NO_CONTENT) From 73aef41ffd2be2fbed11cf75f75393a80322bdcb Mon Sep 17 00:00:00 2001 From: Hyungsuk Yoon Date: Thu, 11 Feb 2016 17:11:10 -0800 Subject: [PATCH 02/13] separate default authentication from the DRF's one --- knox/settings.py | 3 ++- knox/views.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/knox/settings.py b/knox/settings.py index 2a2791a2..cc8fc12e 100644 --- a/knox/settings.py +++ b/knox/settings.py @@ -1,11 +1,12 @@ from datetime import timedelta from django.conf import settings from django.test.signals import setting_changed -from rest_framework.settings import APISettings +from rest_framework.settings import api_settings, APISettings USER_SETTINGS = getattr(settings, 'REST_KNOX', None) DEFAULTS = { + 'LOGIN_AUTHENTICATION_CLASSES': api_settings.DEFAULT_AUTHENTICATION_CLASSES, 'SECURE_HASH_ALGORITHM': 'cryptography.hazmat.primitives.hashes.SHA512', 'AUTH_TOKEN_CHARACTER_LENGTH': 64, 'TOKEN_TTL': timedelta(hours=10), diff --git a/knox/views.py b/knox/views.py index 54682487..6cd4a2c5 100644 --- a/knox/views.py +++ b/knox/views.py @@ -12,7 +12,7 @@ UserSerializer = knox_settings.USER_SERIALIZER class LoginView(APIView): - authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES + authentication_classes = knox_settings.LOGIN_AUTHENTICATION_CLASSES permission_classes = (IsAuthenticated,) def post(self, request, format=None): From 3f5b308093367a61796dff8a2f29f8a86cd43806 Mon Sep 17 00:00:00 2001 From: Hyungsuk Yoon Date: Thu, 11 Feb 2016 17:12:12 -0800 Subject: [PATCH 03/13] Remove unnecessary import --- knox/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/knox/views.py b/knox/views.py index 6cd4a2c5..2217dec0 100644 --- a/knox/views.py +++ b/knox/views.py @@ -2,7 +2,6 @@ from rest_framework.authentication import BasicAuthentication from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response -from rest_framework.settings import api_settings from rest_framework.views import APIView from knox.auth import TokenAuthentication From c928368c7f63ab1e8d73df5fff2a2e1ecbc015d6 Mon Sep 17 00:00:00 2001 From: Hyungsuk Yoon Date: Thu, 11 Feb 2016 17:27:21 -0800 Subject: [PATCH 04/13] 'TOKEN_TTL' is an instance of 'datetime.timedelta' --- docs/settings.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/settings.md b/docs/settings.md index 84dff1cf..944e19cf 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -8,10 +8,11 @@ Example `settings.py` ```python #...snip... # These are the default values if none are set +from datetime import timedelta 'REST_KNOX' = { 'SECURE_HASH_ALGORITHM': 'cryptography.hazmat.primitives.hashes.SHA512', 'AUTH_TOKEN_CHARACTER_LENGTH': 64, - 'TOKEN_TTL': 10, + 'TOKEN_TTL': timedelta(hours=10), 'USER_SERIALIZER': 'knox.serializers.UserSerializer', } #...snip... From 913336d5ec81564a1fed7f7857bb8183802601fa Mon Sep 17 00:00:00 2001 From: Raphael Jasjukaitis Date: Thu, 18 Aug 2016 11:24:41 +0200 Subject: [PATCH 05/13] Preparations for more performance: Introducing token_key. The first 8 characters of a token will be saved as token_key, so in future knox won't iterate over all hashes, only over all with the first 8 chars. For a smooth migration, this minor release doesn't include the actual performance improvements. If a token is valid, it will fill the token_key, but is still running over all tokens. In the next (I would say) major release, a breaking change will come, because the token_key will become not null. All reused tokens between this and the upcoming (major) release will be updated automatically and are not affected. Any other inactive user must reauthenticate. Then the actual performance improvement will work. Bonus: Code cleanup, more PEP8. --- knox/auth.py | 23 +++++++---- knox/crypto.py | 20 ++++----- knox/migrations/0005_authtoken_token_key.py | 20 +++++++++ knox/models.py | 25 +++++++---- knox/settings.py | 8 ++-- knox/views.py | 10 +++-- setup.py | 7 ++-- tests/tests.py | 46 +++++++++++++++++---- 8 files changed, 115 insertions(+), 44 deletions(-) create mode 100644 knox/migrations/0005_authtoken_token_key.py diff --git a/knox/auth.py b/knox/auth.py index 33341d71..c6526234 100644 --- a/knox/auth.py +++ b/knox/auth.py @@ -1,16 +1,20 @@ from django.conf import settings -from django.contrib.auth import get_user_model from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from rest_framework import exceptions -from rest_framework.authentication import BaseAuthentication, get_authorization_header +from rest_framework.authentication import ( + BaseAuthentication, + get_authorization_header +) from knox.crypto import hash_token from knox.models import AuthToken +from knox.settings import CONSTANTS User = settings.AUTH_USER_MODEL + class TokenAuthentication(BaseAuthentication): ''' This authentication scheme uses Knox AuthTokens for authentication. @@ -30,15 +34,20 @@ def authenticate(self, request): if not auth or auth[0].lower() != b'token': return None - if len(auth) == 1: msg = _('Invalid token header. No credentials provided.') raise exceptions.AuthenticationFailed(msg) elif len(auth) > 2: - msg = _('Invalid token header. Token string should not contain spaces.') + msg = _('Invalid token header. ' + 'Token string should not contain spaces.') raise exceptions.AuthenticationFailed(msg) - return self.authenticate_credentials(auth[1]) + user, auth_token = self.authenticate_credentials(auth[1]) + # For a smooth migration to enforce the token_key + if not auth_token.token_key: + auth_token.token_key = auth[1][:CONSTANTS.TOKEN_KEY_LENGTH] + auth_token.save() + return (user, auth_token) def authenticate_credentials(self, token): ''' @@ -60,8 +69,8 @@ def authenticate_credentials(self, token): def validate_user(self, auth_token): if not auth_token.user.is_active: - raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) - + raise exceptions.AuthenticationFailed( + _('User inactive or deleted.')) return (auth_token.user, auth_token) def authenticate_header(self, request): diff --git a/knox/crypto.py b/knox/crypto.py index ed9ded20..1d3f6487 100644 --- a/knox/crypto.py +++ b/knox/crypto.py @@ -8,21 +8,17 @@ sha = knox_settings.SECURE_HASH_ALGORITHM + def create_token_string(): - return ( - binascii.hexlify( - generate_bytes( - int(knox_settings.AUTH_TOKEN_CHARACTER_LENGTH/2) - ) - ).decode()) + return binascii.hexlify( + generate_bytes(int(knox_settings.AUTH_TOKEN_CHARACTER_LENGTH / 2)) + ).decode() + def create_salt_string(): - return ( - binascii.hexlify( - generate_bytes( - int(CONSTANTS.SALT_LENGTH/2) - ) - ).decode()) + return binascii.hexlify( + generate_bytes(int(CONSTANTS.SALT_LENGTH / 2))).decode() + def hash_token(token, salt): ''' diff --git a/knox/migrations/0005_authtoken_token_key.py b/knox/migrations/0005_authtoken_token_key.py new file mode 100644 index 00000000..5e117246 --- /dev/null +++ b/knox/migrations/0005_authtoken_token_key.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-08-18 09:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('knox', '0004_authtoken_expires'), + ] + + operations = [ + migrations.AddField( + model_name='authtoken', + name='token_key', + field=models.CharField(blank=True, db_index=True, max_length=8, null=True), + ), + ] diff --git a/knox/models.py b/knox/models.py index ba130894..f1525e1d 100644 --- a/knox/models.py +++ b/knox/models.py @@ -7,6 +7,7 @@ User = settings.AUTH_USER_MODEL + class AuthTokenManager(models.Manager): def create(self, user, expires=knox_settings.TOKEN_TTL): token = crypto.create_token_string() @@ -14,20 +15,30 @@ def create(self, user, expires=knox_settings.TOKEN_TTL): digest = crypto.hash_token(token, salt) if expires is not None: - expires = timezone.now() + expires + expires = timezone.now() + expires + + super(AuthTokenManager, self).create( + token_key=token[:CONSTANTS.TOKEN_KEY_LENGTH], digest=digest, + salt=salt, user=user, expires=expires) + # Note only the token - not the AuthToken object - is returned + return token - auth_token = super(AuthTokenManager, self).create(digest=digest, salt=salt, user=user, expires=expires) - return token # Note only the token - not the AuthToken object - is returned class AuthToken(models.Model): objects = AuthTokenManager() - digest = models.CharField(max_length=CONSTANTS.DIGEST_LENGTH, primary_key=True) - salt = models.CharField(max_length=CONSTANTS.SALT_LENGTH, unique=True) - user = models.ForeignKey(User, null=False, blank=False, related_name="auth_token_set") + digest = models.CharField( + max_length=CONSTANTS.DIGEST_LENGTH, primary_key=True) + token_key = models.CharField( + max_length=CONSTANTS.TOKEN_KEY_LENGTH, db_index=True, + null=True, blank=True) + salt = models.CharField( + max_length=CONSTANTS.SALT_LENGTH, unique=True) + user = models.ForeignKey( + User, null=False, blank=False, related_name='auth_token_set') created = models.DateTimeField(auto_now_add=True) expires = models. DateTimeField(null=True, blank=True) def __str__(self): - return "%s : %s" % (self.digest, self.user) + return '%s : %s' % (self.digest, self.user) diff --git a/knox/settings.py b/knox/settings.py index 2a2791a2..c40ae884 100644 --- a/knox/settings.py +++ b/knox/settings.py @@ -19,6 +19,7 @@ knox_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) + def reload_api_settings(*args, **kwargs): global knox_settings setting, value = kwargs['setting'], kwargs['value'] @@ -27,17 +28,18 @@ def reload_api_settings(*args, **kwargs): setting_changed.connect(reload_api_settings) + class CONSTANTS: ''' Constants cannot be changed at runtime ''' + TOKEN_KEY_LENGTH = 8 DIGEST_LENGTH = 128 SALT_LENGTH = 16 - def __setattr__ (self, *_, **__): + def __setattr__(self, *args, **kwargs): raise RuntimeException(''' Constant values must NEVER be changed at runtime, as they are integral to the structure of database tables - ''' - ) + ''') CONSTANTS = CONSTANTS() diff --git a/knox/views.py b/knox/views.py index 54682487..1d64fd4f 100644 --- a/knox/views.py +++ b/knox/views.py @@ -1,6 +1,5 @@ from rest_framework import status -from rest_framework.authentication import BasicAuthentication -from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView @@ -11,6 +10,7 @@ UserSerializer = knox_settings.USER_SERIALIZER + class LoginView(APIView): authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES permission_classes = (IsAuthenticated,) @@ -18,10 +18,11 @@ class LoginView(APIView): def post(self, request, format=None): token = AuthToken.objects.create(request.user) return Response({ - "user": UserSerializer(request.user).data, - "token": token, + 'user': UserSerializer(request.user).data, + 'token': token, }) + class LogoutView(APIView): authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) @@ -30,6 +31,7 @@ def post(self, request, format=None): request._auth.delete() return Response(None, status=status.HTTP_204_NO_CONTENT) + class LogoutAllView(APIView): ''' Log the user out of all sessions diff --git a/setup.py b/setup.py index 12a1a681..1da6890d 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version='2.2.0', + version='2.3.0', description='Authentication for django rest framework', long_description=long_description, @@ -56,13 +56,14 @@ # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). - packages=find_packages(exclude=['contrib', 'docs', 'tests*', 'knox_project']), + packages=find_packages( + exclude=['contrib', 'docs', 'tests*', 'knox_project']), # List run-time dependencies here. These will be installed by pip when # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html - install_requires=['django', 'djangorestframework', 'pyOpenSSL',], + install_requires=['django', 'djangorestframework', 'pyOpenSSL'], # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax, diff --git a/tests/tests.py b/tests/tests.py index 0c45520a..a6516d17 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,17 +1,22 @@ -import json import base64 import datetime from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse -from rest_framework.test import APITestCase as TestCase +from rest_framework.test import APIRequestFactory, APITestCase as TestCase + +from knox.auth import TokenAuthentication from knox.models import AuthToken +from knox.settings import CONSTANTS User = get_user_model() + def get_basic_auth_header(username, password): - return 'Basic %s' % base64.b64encode(('%s:%s' % (username, password)).encode('ascii')).decode() + return 'Basic %s' % base64.b64encode( + ('%s:%s' % (username, password)).encode('ascii')).decode() + class AuthTestCase(TestCase): @@ -20,16 +25,19 @@ def test_login_creates_keys(self): username, password = 'root', 'toor' User.objects.create_user(username, 'root@localhost.com', password) url = reverse('knox_login') - self.client.credentials(HTTP_AUTHORIZATION=get_basic_auth_header(username, password)) + self.client.credentials( + HTTP_AUTHORIZATION=get_basic_auth_header(username, password)) for _ in range(5): self.client.post(url, {}, format='json') self.assertEqual(AuthToken.objects.count(), 5) + self.assertTrue(all(e.token_key for e in AuthToken.objects.all())) def test_logout_deletes_keys(self): self.assertEqual(AuthToken.objects.count(), 0) username, password = 'root', 'toor' - user = User.objects.create_user(username, 'root@localhost.com', password) + user = User.objects.create_user( + username, 'root@localhost.com', password) token = AuthToken.objects.create(user=user) self.assertEqual(AuthToken.objects.count(), 1) @@ -41,7 +49,8 @@ def test_logout_deletes_keys(self): def test_logout_all_deletes_keys(self): self.assertEqual(AuthToken.objects.count(), 0) username, password = 'root', 'toor' - user = User.objects.create_user(username, 'root@localhost.com', password) + user = User.objects.create_user( + username, 'root@localhost.com', password) for _ in range(10): token = AuthToken.objects.create(user=user) self.assertEqual(AuthToken.objects.count(), 10) @@ -54,9 +63,12 @@ def test_logout_all_deletes_keys(self): def test_expired_tokens_deleted(self): self.assertEqual(AuthToken.objects.count(), 0) username, password = 'root', 'toor' - user = User.objects.create_user(username, 'root@localhost.com', password) + user = User.objects.create_user( + username, 'root@localhost.com', password) for _ in range(10): - token = AuthToken.objects.create(user=user, expires=datetime.timedelta(seconds=0)) #0 TTL gives an expired token + # 0 TTL gives an expired token + token = AuthToken.objects.create( + user=user, expires=datetime.timedelta(seconds=0)) self.assertEqual(AuthToken.objects.count(), 10) # Attempting a single logout should delete all tokens @@ -65,3 +77,21 @@ def test_expired_tokens_deleted(self): self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token)) self.client.post(url, {}, format='json') self.assertEqual(AuthToken.objects.count(), 0) + + def test_update_token_key(self): + self.assertEqual(AuthToken.objects.count(), 0) + username, password = 'root', 'toor' + user = User.objects.create_user( + username, 'root@localhost.com', password) + token = AuthToken.objects.create(user) + auth_token = AuthToken.objects.first() + auth_token.token_key = None + auth_token.save() + rf = APIRequestFactory() + request = rf.get('/') + request.META = {'HTTP_AUTHORIZATION': 'Token {}'.format(token)} + TokenAuthentication().authenticate(request) + auth_token = AuthToken.objects.get(digest=auth_token.digest) + self.assertEqual( + token[:CONSTANTS.TOKEN_KEY_LENGTH], + auth_token.token_key) From e6f7f937f75717adc7c0303d951fdced4426afc3 Mon Sep 17 00:00:00 2001 From: Phil Ngo Date: Sat, 12 Nov 2016 13:17:27 -0800 Subject: [PATCH 06/13] Typo fix Spell Authentication --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index a5f1eddd..435e628e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,6 +7,6 @@ pages: - API Guide: - Views: 'views.md' - URLs: 'urls.md' - - Authentcation: 'auth.md' + - Authentication: 'auth.md' - Settings: 'settings.md' - Changes: 'changes.md' From 15ae24d4f07dc142ce828d9ea93271ce02f0ba83 Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Mon, 7 Nov 2016 19:13:18 +0100 Subject: [PATCH 07/13] Move UserSerializer definition into class to avoid error when extending LoginView: django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet --- knox/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/knox/views.py b/knox/views.py index 53e48db1..a6a5d9b1 100644 --- a/knox/views.py +++ b/knox/views.py @@ -1,7 +1,6 @@ from django.contrib.auth.signals import user_logged_in, user_logged_out from rest_framework import status -from rest_framework.authentication import BasicAuthentication -from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -9,7 +8,6 @@ from knox.models import AuthToken from knox.settings import knox_settings -UserSerializer = knox_settings.USER_SERIALIZER class LoginView(APIView): authentication_classes = knox_settings.LOGIN_AUTHENTICATION_CLASSES @@ -18,11 +16,13 @@ class LoginView(APIView): def post(self, request, format=None): token = AuthToken.objects.create(request.user) user_logged_in.send(sender=request.user.__class__, request=request, user=request.user) + UserSerializer = knox_settings.USER_SERIALIZER return Response({ "user": UserSerializer(request.user).data, "token": token, }) + class LogoutView(APIView): authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) @@ -32,6 +32,7 @@ def post(self, request, format=None): user_logged_out.send(sender=request.user.__class__, request=request, user=request.user) return Response(None, status=status.HTTP_204_NO_CONTENT) + class LogoutAllView(APIView): ''' Log the user out of all sessions From 8fc7b0f3f9b30ef70ce738936027de6745cfab85 Mon Sep 17 00:00:00 2001 From: "David G. Daniel" Date: Sun, 23 Oct 2016 20:30:03 -0400 Subject: [PATCH 08/13] Fixed UserSerializer, so it accounts USERNAME_FIELD --- knox/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/knox/serializers.py b/knox/serializers.py index f11ca691..9c77897b 100644 --- a/knox/serializers.py +++ b/knox/serializers.py @@ -4,7 +4,9 @@ User = get_user_model() +username_field = User.USERNAME_FIELD if hasattr(User, 'USERNAME_FIELD') else 'username' + class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ('username', 'first_name', 'last_name',) + fields = (username_field, 'first_name', 'last_name',) From e0608111bf6719934dc1c66e99468d45138c6c32 Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Sat, 10 Dec 2016 14:40:00 -0500 Subject: [PATCH 09/13] Test for invalid token length --- knox/views.py | 4 ++-- knox_project/urls.py | 3 +++ knox_project/views.py | 12 ++++++++++++ tests/tests.py | 9 ++++++++- 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 knox_project/views.py diff --git a/knox/views.py b/knox/views.py index bcbb9226..361cae3b 100644 --- a/knox/views.py +++ b/knox/views.py @@ -2,6 +2,7 @@ from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.settings import api_settings from rest_framework.views import APIView from knox.auth import TokenAuthentication @@ -9,9 +10,8 @@ from knox.settings import knox_settings - class LoginView(APIView): - authentication_classes = knox_settings.LOGIN_AUTHENTICATION_CLASSES + authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES permission_classes = (IsAuthenticated,) def post(self, request, format=None): diff --git a/knox_project/urls.py b/knox_project/urls.py index a2af32a2..cc2692ee 100644 --- a/knox_project/urls.py +++ b/knox_project/urls.py @@ -16,8 +16,11 @@ from django.conf.urls import include, url from django.contrib import admin +from .views import RootView + urlpatterns = [ url(r'^api/', include('knox.urls')), + url(r'^api/$', RootView.as_view(), name="api-root"), url(r'^admin/', include(admin.site.urls)), url(r'^', include(admin.site.urls)), ] diff --git a/knox_project/views.py b/knox_project/views.py new file mode 100644 index 00000000..add8505b --- /dev/null +++ b/knox_project/views.py @@ -0,0 +1,12 @@ +from knox.auth import TokenAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + + +class RootView(APIView): + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def get(self, request): + return Response("api root") diff --git a/tests/tests.py b/tests/tests.py index a6516d17..ea6f2dcd 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -3,7 +3,6 @@ from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse - from rest_framework.test import APIRequestFactory, APITestCase as TestCase from knox.auth import TokenAuthentication @@ -95,3 +94,11 @@ def test_update_token_key(self): self.assertEqual( token[:CONSTANTS.TOKEN_KEY_LENGTH], auth_token.token_key) + + def test_invalid_token_length_returns_401_code(self): + invalid_token = "1" * (CONSTANTS.TOKEN_KEY_LENGTH - 1) + url = reverse('api-root') + self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % invalid_token)) + response = self.client.post(url, {}, format='json') + self.assertEqual(response.status_code, 401) + self.assertEqual(response.data, {"detail": "Invalid token."}) From 586fab3cdc9e059c082bf209a6113b6bb06f2119 Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Tue, 13 Dec 2016 11:14:10 -0500 Subject: [PATCH 10/13] Revert "separate default authentication from the DRF's one" This reverts commit 73aef41ffd2be2fbed11cf75f75393a80322bdcb. --- knox/settings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/knox/settings.py b/knox/settings.py index bcf9b5d4..c40ae884 100644 --- a/knox/settings.py +++ b/knox/settings.py @@ -1,12 +1,11 @@ from datetime import timedelta from django.conf import settings from django.test.signals import setting_changed -from rest_framework.settings import api_settings, APISettings +from rest_framework.settings import APISettings USER_SETTINGS = getattr(settings, 'REST_KNOX', None) DEFAULTS = { - 'LOGIN_AUTHENTICATION_CLASSES': api_settings.DEFAULT_AUTHENTICATION_CLASSES, 'SECURE_HASH_ALGORITHM': 'cryptography.hazmat.primitives.hashes.SHA512', 'AUTH_TOKEN_CHARACTER_LENGTH': 64, 'TOKEN_TTL': timedelta(hours=10), From 69553c79b2a4138f588aa5d4b0712edd4c39ce47 Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Fri, 16 Dec 2016 10:45:05 -0500 Subject: [PATCH 11/13] Update changelog for 2.2.1 --- CHANGELOG.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2c1344e1..441af3ca 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,11 @@ +###### +2.2.1 +###### +- Introducing token_key to avoid loop over all tokens on login-requests +- Signals are sent on login/logout +- Test for invalid token length +- Cleanup in code and documentation + ###### 2.2.0 ###### From 04c4c0615a2809ed317585038a8ee27019dc22c8 Mon Sep 17 00:00:00 2001 From: belugame Date: Fri, 16 Dec 2016 11:04:59 -0500 Subject: [PATCH 12/13] Make people aware of necessary migration --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 441af3ca..f061f9e8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,8 @@ ###### 2.2.1 ###### +**Please be aware: updating to his version requires applying a database migration** + - Introducing token_key to avoid loop over all tokens on login-requests - Signals are sent on login/logout - Test for invalid token length From 29152e905684dde59d6a5e00f9d08994fb95536e Mon Sep 17 00:00:00 2001 From: belugame Date: Fri, 16 Dec 2016 11:12:14 -0500 Subject: [PATCH 13/13] Set version for bugfix release 2.2.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1da6890d..a102da00 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version='2.3.0', + version='2.2.1', description='Authentication for django rest framework', long_description=long_description,