Skip to content

Commit

Permalink
Merge pull request #33 from James1345/develop
Browse files Browse the repository at this point in the history
Bugfix release 2.2.1
  • Loading branch information
belugame authored Dec 16, 2016
2 parents 0f496ca + 29152e9 commit 66578b7
Show file tree
Hide file tree
Showing 14 changed files with 157 additions and 48 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
######
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
- Cleanup in code and documentation

######
2.2.0
######
Expand Down
3 changes: 2 additions & 1 deletion docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand Down
23 changes: 16 additions & 7 deletions knox/auth.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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):
'''
Expand All @@ -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):
Expand Down
20 changes: 8 additions & 12 deletions knox/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
'''
Expand Down
20 changes: 20 additions & 0 deletions knox/migrations/0005_authtoken_token_key.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
25 changes: 18 additions & 7 deletions knox/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,38 @@

User = settings.AUTH_USER_MODEL


class AuthTokenManager(models.Manager):
def create(self, user, expires=knox_settings.TOKEN_TTL):
token = crypto.create_token_string()
salt = crypto.create_salt_string()
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)
4 changes: 3 additions & 1 deletion knox/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',)
8 changes: 5 additions & 3 deletions knox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -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()
15 changes: 10 additions & 5 deletions knox/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +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.settings import api_settings
from rest_framework.views import APIView
Expand All @@ -9,27 +9,31 @@
from knox.models import AuthToken
from knox.settings import knox_settings

UserSerializer = knox_settings.USER_SERIALIZER

class LoginView(APIView):
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
permission_classes = (IsAuthenticated,)

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,
'user': UserSerializer(request.user).data,
'token': token,
})


class LogoutView(APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)

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):
'''
Log the user out of all sessions
Expand All @@ -40,4 +44,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)
3 changes: 3 additions & 0 deletions knox_project/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
]
12 changes: 12 additions & 0 deletions knox_project/views.py
Original file line number Diff line number Diff line change
@@ -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")
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
7 changes: 4 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.1',

description='Authentication for django rest framework',
long_description=long_description,
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 66578b7

Please sign in to comment.