kumidc/authentication/models/otp.py

104 lines
3 KiB
Python
Raw Normal View History

2022-08-04 11:15:10 +00:00
from django.db import models
from django.contrib.auth import get_user_model
from annoying.fields import AutoOneToOneField
from pyotp import TOTP, random_base32
from datetime import datetime
2022-08-04 11:15:10 +00:00
class TOTPSecret(models.Model):
"""A secret for a user's TOTP device
Attributes:
user: The user to whom this secret belongs
secret: The secret
active: Whether this secret is currently active
"""
2022-08-04 11:15:10 +00:00
user = AutoOneToOneField(get_user_model(), models.CASCADE, primary_key=True)
secret = models.CharField(max_length=32, default=random_base32)
active = models.BooleanField(default=False)
def verify(self, token: str, window: int = 1, set_used=True) -> bool:
"""Verify a TOTP token
Args:
token (str): The token to verify
window (int, optional): The number of tokens to check before and after the current token. Defaults to 1.
Returns:
bool: Whether the token was valid
"""
if not self.active:
return False
if UsedTOTPToken.is_used(self, token):
return False
if TOTP(self.secret).verify(token, valid_window=window):
if set_used:
UsedTOTPToken.objects.create(secret=self, token=token)
return True
return False
def get_uri(self) -> str:
"""Get the provisioning URI for this secret
This is used to generate a QR code for the user to scan with their TOTP device.
2022-08-04 11:15:10 +00:00
Returns:
str: The provisioning URI
"""
return TOTP(self.secret).provisioning_uri(
name=self.user.email, issuer_name="KumiDC"
)
def change_secret(self) -> str:
"""Change the secret for this user
This generates a new secret and saves it to the database.
Returns:
str: The new secret
"""
2022-08-04 11:15:10 +00:00
self.secret = random_base32()
self.save()
return self.secret
class UsedTOTPToken(models.Model):
"""A TOTP token that has been used
We log these so that we can prevent replay attacks.
Attributes:
secret: The secret that was used
token: The token that was used
timestamp: When the token was used
expiry: How long we should keep this token for
"""
secret = models.ForeignKey(TOTPSecret, on_delete=models.CASCADE)
token = models.CharField(max_length=6)
timestamp = models.DateTimeField(auto_now_add=True)
expiry = models.DateTimeField()
def save(self, *args, **kwargs):
self.expiry = self.timestamp + timedelta(seconds=60)
super().save(*args, **kwargs)
@classmethod
def is_used(cls, secret: TOTPSecret, token: str) -> bool:
"""Check whether a token has been used
Args:
secret (TOTPSecret): The secret that was used
token (str): The token that was used
Returns:
bool: Whether the token has recently been used
"""
return cls.objects.filter(secret=secret, token=token, expiry__gte=datetime.now()).exists()