Skip to content

Commit

Permalink
rename backup token to otp bypass token
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Sep 29, 2023
1 parent 5065842 commit 5647b3c
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 98 deletions.
2 changes: 1 addition & 1 deletion codeforlife/settings/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
AUTHENTICATION_BACKENDS = [
"codeforlife.user.auth.backends.EmailAndPasswordBackend",
"codeforlife.user.auth.backends.OtpBackend",
"codeforlife.user.auth.backends.TokenBackend",
"codeforlife.user.auth.backends.OtpBypassTokenBackend",
"codeforlife.user.auth.backends.UserIdAndLoginIdBackend",
"codeforlife.user.auth.backends.UsernameAndPasswordAndClassIdBackend",
]
Expand Down
2 changes: 1 addition & 1 deletion codeforlife/user/auth/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .email_and_password import EmailAndPasswordBackend
from .otp import OtpBackend
from .token import TokenBackend
from .otp_bypass_token import OtpBypassTokenBackend
from .user_id_and_login_id import UserIdAndLoginIdBackend
from .username_and_password_and_class_id import (
UsernameAndPasswordAndClassIdBackend,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ...models import AuthFactor, User


class TokenBackend(BaseBackend):
class OtpBypassTokenBackend(BaseBackend):
def authenticate(
self,
request: WSGIRequest,
Expand All @@ -22,8 +22,8 @@ def authenticate(
):
return

for backup_token in request.user.backup_tokens.all():
if backup_token.check_token(token):
for otp_bypass_token in request.user.otp_bypass_tokens.all():
if otp_bypass_token.check_token(token):
# Delete OTP auth factor from session.
request.user.session.session_auth_factors.filter(
auth_factor__type=AuthFactor.Type.OTP
Expand Down
6 changes: 3 additions & 3 deletions codeforlife/user/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 3.2.20 on 2023-09-28 15:43
# Generated by Django 3.2.20 on 2023-09-29 17:53

import django.contrib.auth.models
import django.core.validators
Expand Down Expand Up @@ -66,11 +66,11 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='BackupToken',
name='OtpBypassToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(max_length=8, validators=[django.core.validators.MinLengthValidator(8)])),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='backup_tokens', to='user.user')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='otp_bypass_tokens', to='user.user')),
],
options={
'unique_together': {('user', 'token')},
Expand Down
2 changes: 1 addition & 1 deletion codeforlife/user/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# from .teacher_invitation import SchoolTeacherInvitation
# from .teacher import Teacher
from .auth_factor import AuthFactor
from .backup_token import BackupToken
from .otp_bypass_token import OtpBypassToken
from .session import Session
from .session_auth_factor import SessionAuthFactor
from .user import User
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,44 @@
from . import user


class BackupToken(models.Model):
class OtpBypassToken(models.Model):
max_count = 10
max_count_validation_error = ValidationError(
f"Exceeded max count of {max_count}"
)

class Manager(models.Manager["BackupToken"]):
class Manager(models.Manager["OtpBypassToken"]):
def create(self, token: str, **kwargs):
return super().create(token=make_password(token), **kwargs)

def bulk_create(
self,
backup_tokens: t.List["BackupToken"],
otp_bypass_tokens: t.List["OtpBypassToken"],
*args,
**kwargs,
):
def key(backup_token: BackupToken):
return backup_token.user.id
def key(otp_bypass_token: OtpBypassToken):
return otp_bypass_token.user.id

backup_tokens.sort(key=key)
for user_id, group in groupby(backup_tokens, key=key):
otp_bypass_tokens.sort(key=key)
for user_id, group in groupby(otp_bypass_tokens, key=key):
if (
len(list(group))
+ BackupToken.objects.filter(user_id=user_id).count()
> BackupToken.max_count
+ OtpBypassToken.objects.filter(user_id=user_id).count()
> OtpBypassToken.max_count
):
raise BackupToken.max_count_validation_error
raise OtpBypassToken.max_count_validation_error

for backup_token in backup_tokens:
backup_token.token = make_password(backup_token.token)
for otp_bypass_token in otp_bypass_tokens:
otp_bypass_token.token = make_password(otp_bypass_token.token)

return super().bulk_create(backup_tokens, *args, **kwargs)
return super().bulk_create(otp_bypass_tokens, *args, **kwargs)

objects: Manager = Manager()

user: "user.User" = models.ForeignKey(
"user.User",
related_name="backup_tokens",
related_name="otp_bypass_tokens",
on_delete=models.CASCADE,
)

Expand All @@ -61,10 +61,10 @@ class Meta:
def save(self, *args, **kwargs):
if self.id is None:
if (
BackupToken.objects.filter(user=self.user).count()
>= BackupToken.max_count
OtpBypassToken.objects.filter(user=self.user).count()
>= OtpBypassToken.max_count
):
raise BackupToken.max_count_validation_error
raise OtpBypassToken.max_count_validation_error

return super().save(*args, **kwargs)

Expand Down
4 changes: 2 additions & 2 deletions codeforlife/user/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@
from django.db.models.query import QuerySet
from django.utils.translation import gettext_lazy as _

from . import auth_factor, backup_token, session
from . import auth_factor, otp_bypass_token, session


class User(_User):
auth_factors: QuerySet["auth_factor.AuthFactor"]
backup_tokens: QuerySet["backup_token.BackupToken"]
otp_bypass_tokens: QuerySet["otp_bypass_token.OtpBypassToken"]
session: "session.Session"
userprofile: UserProfile

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@
from django.utils import timezone
from django.utils.crypto import get_random_string

from ....auth.backends import TokenBackend
from ....models import AuthFactor, BackupToken, Session, SessionAuthFactor, User
from ....auth.backends import OtpBypassTokenBackend
from ....models import (
AuthFactor,
OtpBypassToken,
Session,
SessionAuthFactor,
User,
)


class TestTokenBackend(TestCase):
def setUp(self):
self.backend = TokenBackend()
self.backend = OtpBypassTokenBackend()
self.request_factory = RequestFactory()

self.user = User.objects.get(id=2)
Expand All @@ -33,10 +39,13 @@ def setUp(self):
)

self.tokens = [
get_random_string(8) for _ in range(BackupToken.max_count)
get_random_string(8) for _ in range(OtpBypassToken.max_count)
]
self.backup_tokens = BackupToken.objects.bulk_create(
[BackupToken(user=self.user, token=token) for token in self.tokens]
self.otp_bypass_tokens = OtpBypassToken.objects.bulk_create(
[
OtpBypassToken(user=self.user, token=token)
for token in self.tokens
]
)

def test_authenticate(self):
Expand All @@ -46,4 +55,4 @@ def test_authenticate(self):
user = self.backend.authenticate(request, token=self.tokens[0])

assert user == self.user
assert self.backup_tokens[0].id is None
assert self.otp_bypass_tokens[0].id is None
63 changes: 0 additions & 63 deletions codeforlife/user/tests/models/test_backup_token.py

This file was deleted.

67 changes: 67 additions & 0 deletions codeforlife/user/tests/models/test_otp_bypass_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from django.contrib.auth.hashers import check_password
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils.crypto import get_random_string

from ...models import OtpBypassToken, User


class TestOtpBypassToken(TestCase):
def setUp(self):
self.user = User.objects.get(id=2)

def test_bulk_create(self):
token = get_random_string(8)
otp_bypass_tokens = OtpBypassToken.objects.bulk_create(
[OtpBypassToken(user=self.user, token=token)]
)

assert check_password(token, otp_bypass_tokens[0].token)
with self.assertRaises(ValidationError):
OtpBypassToken.objects.bulk_create(
[
OtpBypassToken(
user=self.user,
token=get_random_string(8),
)
for _ in range(OtpBypassToken.max_count)
]
)

def test_create(self):
token = get_random_string(8)
otp_bypass_token = OtpBypassToken.objects.create(
user=self.user, token=token
)

assert check_password(token, otp_bypass_token.token)

OtpBypassToken.objects.bulk_create(
[
OtpBypassToken(
user=self.user,
token=get_random_string(8),
)
for _ in range(OtpBypassToken.max_count - 1)
]
)

with self.assertRaises(ValidationError):
OtpBypassToken.objects.create(
user=self.user,
token=get_random_string(8),
)

def test_check_token(self):
token = get_random_string(8)
otp_bypass_token = OtpBypassToken.objects.create(
user=self.user, token=token
)

assert otp_bypass_token.check_token(token)
assert otp_bypass_token.id is None
with self.assertRaises(OtpBypassToken.DoesNotExist):
OtpBypassToken.objects.get(
user=otp_bypass_token.user,
token=otp_bypass_token.token,
)

0 comments on commit 5647b3c

Please sign in to comment.