From ae2a6eda79a9631359a5ca7b2c9c81e66e3a5eba Mon Sep 17 00:00:00 2001 From: Kumi Date: Sat, 8 Jul 2023 16:11:38 +0200 Subject: [PATCH] Preparations for Android app --- authentication/mixins/session.py | 10 +++++++ authentication/mixins/timeout.py | 5 ++++ authentication/models/app.py | 49 ++++++++++++++++++++++++++++++++ authentication/views/app.py | 2 ++ authentication/views/reverify.py | 9 ++++++ requirements.txt | 1 + 6 files changed, 76 insertions(+) create mode 100644 authentication/models/app.py create mode 100644 authentication/views/app.py diff --git a/authentication/mixins/session.py b/authentication/mixins/session.py index 131e894..47e9706 100644 --- a/authentication/mixins/session.py +++ b/authentication/mixins/session.py @@ -10,6 +10,16 @@ from django.shortcuts import resolve_url from ..models.session import AuthSession +# Somewhat shamelessly copied from django.contrib.auth.views +# +# Original source: +# https://github.com/django/django/blob/main/django/contrib/auth/views.py +# +# License: +# BSD 3-Clause "New" or "Revised" License +# Copyright (c) Django Software Foundation and individual contributors. +# All rights reserved. +# https://github.com/django/django/blob/main/LICENSE class AuthSessionRequiredMixin(RedirectURLMixin): redirect_field_name = REDIRECT_FIELD_NAME diff --git a/authentication/mixins/timeout.py b/authentication/mixins/timeout.py index 9cc8821..3fc64a1 100644 --- a/authentication/mixins/timeout.py +++ b/authentication/mixins/timeout.py @@ -10,6 +10,7 @@ from django.shortcuts import resolve_url from urllib.parse import urlparse from ..models.otp import TOTPSecret +from ..models.app import AppSession class TimeoutMixin: @@ -30,6 +31,9 @@ class TimeoutMixin: elif request.session["LastActivity"] < (timezone.now() - timezone.timedelta(minutes=settings.REVERIFY_AFTER_INACTIVITY_MINUTES)).timestamp(): try: assert request.user.totpsecret.active + + request.session["AppSession"] = AppSession.get_for_user(request.user) + return redirect_to_login(path, resolve_url("auth:reverify"), REDIRECT_FIELD_NAME) except (AssertionError, TOTPSecret.DoesNotExist): messages.error( @@ -39,6 +43,7 @@ class TimeoutMixin: messages.error( request, "Something went wrong, please try logging in again." ) + logout(request) else: request.session["LastActivity"] = timezone.now().timestamp() diff --git a/authentication/models/app.py b/authentication/models/app.py new file mode 100644 index 0000000..49d9365 --- /dev/null +++ b/authentication/models/app.py @@ -0,0 +1,49 @@ +from django.db import models +from django.contrib.auth import get_user_model + +from uuid import uuid4 + +from jwt import decode, InvalidTokenError + +class AppKey(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + user = models.ForeignKey(get_user_model(), models.CASCADE) + device = models.CharField(max_length=255) + key = models.TextField() + active = models.BooleanField(default=True) + + def __str__(self): + return f"{self.user.username} - {self.device}" + + def validateJWT(self, jwt): + try: + return decode(jwt, self.key, algorithms=['HS256']) + except InvalidTokenError: + return False + +class AppSession(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + user = models.ForeignKey(get_user_model(), models.CASCADE) + created = models.DateTimeField(auto_now_add=True) + used = models.DateTimeField(null=True, blank=True) + approved = models.BooleanField(default=False) + + @property + def valid(self): + return self.created > timezone.now() - timezone.timedelta(minutes=5) + + @classmethod + def get_for_user(cls, user, create = True): + assert user + + if not user.appkey_set.filter(active=True).exists(): + return + + user_sessions = cls.objects.filter(user=user) + + for session in user_sessions: + if session.valid and not session.used: + return session + + if create: + return cls.objects.create(user=user) diff --git a/authentication/views/app.py b/authentication/views/app.py new file mode 100644 index 0000000..c49fbe0 --- /dev/null +++ b/authentication/views/app.py @@ -0,0 +1,2 @@ +from django.views.generic import JSONView + diff --git a/authentication/views/reverify.py b/authentication/views/reverify.py index f7218fd..1d7c800 100644 --- a/authentication/views/reverify.py +++ b/authentication/views/reverify.py @@ -4,6 +4,7 @@ from django.http import HttpResponseRedirect from django.urls import reverse_lazy from ..forms.otp import TOTPLoginForm +from ..models.app import AppSession from frontend.mixins.views import TitleMixin @@ -17,4 +18,12 @@ class ReverifyView(TitleMixin, LoginView): def form_valid(self, form): self.request.session["LastActivity"] = timezone.now().timestamp() + + try: + app_session = AppSession.objects.get(id=self.request.session["AppSession"]) + app_session.used = True + app_session.save() + except AppSession.DoesNotExist: + pass + return HttpResponseRedirect(self.get_success_url()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 234d62d..70df506 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ django-crispy-forms pyqrcode pypng django-ajax-datatable +pyjwt # For MySQL: