From c00c208199e336a30375c8d9896327375fa61f44 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 3 Jun 2020 15:49:38 +0300 Subject: [PATCH] Change to our own token authentication. --- django_etesync/token_auth/__init__.py | 0 django_etesync/token_auth/admin.py | 0 django_etesync/token_auth/apps.py | 5 ++ django_etesync/token_auth/authentication.py | 46 +++++++++++++++++++ .../token_auth/migrations/0001_initial.py | 28 +++++++++++ .../token_auth/migrations/__init__.py | 0 django_etesync/token_auth/models.py | 26 +++++++++++ django_etesync/views.py | 12 +++-- 8 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 django_etesync/token_auth/__init__.py create mode 100644 django_etesync/token_auth/admin.py create mode 100644 django_etesync/token_auth/apps.py create mode 100644 django_etesync/token_auth/authentication.py create mode 100644 django_etesync/token_auth/migrations/0001_initial.py create mode 100644 django_etesync/token_auth/migrations/__init__.py create mode 100644 django_etesync/token_auth/models.py diff --git a/django_etesync/token_auth/__init__.py b/django_etesync/token_auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_etesync/token_auth/admin.py b/django_etesync/token_auth/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/django_etesync/token_auth/apps.py b/django_etesync/token_auth/apps.py new file mode 100644 index 0000000..dc793f2 --- /dev/null +++ b/django_etesync/token_auth/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TokenAuthConfig(AppConfig): + name = 'django_etesync.token_auth' diff --git a/django_etesync/token_auth/authentication.py b/django_etesync/token_auth/authentication.py new file mode 100644 index 0000000..432c8cf --- /dev/null +++ b/django_etesync/token_auth/authentication.py @@ -0,0 +1,46 @@ +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from rest_framework import exceptions +from rest_framework.authentication import TokenAuthentication as DRFTokenAuthentication + +from .models import AuthToken, get_default_expiry + + +AUTO_REFRESH = True +MIN_REFRESH_INTERVAL = 60 + + +class TokenAuthentication(DRFTokenAuthentication): + keyword = 'Token' + model = AuthToken + + def authenticate_credentials(self, key): + msg = _('Invalid token.') + model = self.get_model() + try: + token = model.objects.select_related('user').get(key=key) + except model.DoesNotExist: + raise exceptions.AuthenticationFailed(msg) + + if not token.user.is_active: + raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + + if token.expiry is not None: + if token.expiry < timezone.now(): + token.delete() + raise exceptions.AuthenticationFailed(msg) + + if AUTO_REFRESH: + self.renew_token(token) + + return (token.user, token) + + def renew_token(self, auth_token): + current_expiry = auth_token.expiry + new_expiry = get_default_expiry() + # Throttle refreshing of token to avoid db writes + delta = (new_expiry - current_expiry).total_seconds() + if delta > MIN_REFRESH_INTERVAL: + auth_token.expiry = new_expiry + auth_token.save(update_fields=('expiry',)) diff --git a/django_etesync/token_auth/migrations/0001_initial.py b/django_etesync/token_auth/migrations/0001_initial.py new file mode 100644 index 0000000..f2024e3 --- /dev/null +++ b/django_etesync/token_auth/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.3 on 2020-06-03 12:49 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +from django_etesync.token_auth import models as token_auth_models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AuthToken', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(db_index=True, default=token_auth_models.generate_key, max_length=40, unique=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('expiry', models.DateTimeField(blank=True, default=token_auth_models.get_default_expiry, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auth_token_set', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/django_etesync/token_auth/migrations/__init__.py b/django_etesync/token_auth/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_etesync/token_auth/models.py b/django_etesync/token_auth/models.py new file mode 100644 index 0000000..9ac0955 --- /dev/null +++ b/django_etesync/token_auth/models.py @@ -0,0 +1,26 @@ +from django.contrib.auth import get_user_model +from django.db import models +from django.utils import timezone +from django.utils.crypto import get_random_string + +User = get_user_model() + + +def generate_key(): + return get_random_string(40) + + +def get_default_expiry(): + return timezone.now() + timezone.timedelta(days=14) + + +class AuthToken(models.Model): + + key = models.CharField(max_length=40, unique=True, db_index=True, default=generate_key) + user = models.ForeignKey(User, null=False, blank=False, + related_name='auth_token_set', on_delete=models.CASCADE) + created = models.DateTimeField(auto_now_add=True) + expiry = models.DateTimeField(null=True, blank=True, default=get_default_expiry) + + def __str__(self): + return '{}: {}'.format(self.key, self.user) diff --git a/django_etesync/views.py b/django_etesync/views.py index 71ae93f..88ae7c4 100644 --- a/django_etesync/views.py +++ b/django_etesync/views.py @@ -16,7 +16,7 @@ import json from functools import reduce from django.conf import settings -from django.contrib.auth import get_user_model, user_logged_in +from django.contrib.auth import get_user_model, user_logged_in, user_logged_out from django.core.exceptions import PermissionDenied from django.db import transaction, IntegrityError from django.db.models import Max, Q @@ -28,13 +28,14 @@ from rest_framework import viewsets from rest_framework import parsers from rest_framework.decorators import action as action_decorator from rest_framework.response import Response -from rest_framework.authtoken.models import Token import nacl.encoding import nacl.signing import nacl.secret import nacl.hash +from .token_auth.models import AuthToken + from . import app_settings, permissions from .models import ( Collection, @@ -566,7 +567,7 @@ class AuthenticationViewSet(viewsets.ViewSet): def login_response_data(self, user): return { - 'token': Token.objects.get_or_create(user=user)[0].key, + 'token': AuthToken.objects.create(user=user).key, 'user': UserSerializer(user).data, } @@ -666,8 +667,9 @@ class AuthenticationViewSet(viewsets.ViewSet): @action_decorator(detail=False, methods=['POST'], permission_classes=BaseViewSet.permission_classes) def logout(self, request): - # FIXME: expire the token - we need better token handling - using knox? Something else? - return Response({}, status=status.HTTP_200_OK) + request._auth.delete() + user_logged_out.send(sender=request.user.__class__, request=request, user=request.user) + return Response(status=status.HTTP_204_NO_CONTENT) @action_decorator(detail=False, methods=['POST'], permission_classes=BaseViewSet.permission_classes) def change_password(self, request):