A pluggable framework for adding two-factor authentication to Django using one-time passwords.
—
Abstract base models and mixins that provide the foundation for all OTP devices, including security features like throttling, cooldowns, and timestamp tracking.
The abstract base model for all OTP devices, providing core functionality for device management and token verification.
class Device(models.Model):
"""
Abstract base model for OTP devices.
Fields:
- user: ForeignKey to User model - The user this device belongs to
- name: CharField(max_length=64) - Human-readable device name
- confirmed: BooleanField(default=True) - Whether device is confirmed/active
"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
name = models.CharField(max_length=64)
confirmed = models.BooleanField(default=True)
# Properties
@property
def persistent_id(self) -> str:
"""Stable device identifier for session storage."""
# Class methods
@classmethod
def model_label(cls) -> str:
"""Returns the model label string."""
@classmethod
def from_persistent_id(cls, persistent_id, for_verify=False):
"""Load device from persistent ID."""
# Instance methods
def is_interactive(self) -> bool:
"""Returns True if device supports challenges."""
def generate_is_allowed(self):
"""Check if challenge generation is allowed."""
def generate_challenge(self):
"""Generate a challenge for the user."""
def verify_is_allowed(self):
"""Check if token verification is allowed."""
def verify_token(self, token) -> bool:
"""Verify an OTP token."""Abstract base model for devices that send tokens through side channels (email, SMS, etc.).
class SideChannelDevice(Device):
"""
Abstract base model for side-channel devices.
Fields:
- token: CharField(max_length=16) - Current token value
- valid_until: DateTimeField - Token expiration timestamp
"""
token = models.CharField(max_length=16)
valid_until = models.DateTimeField(default=timezone.now)
def generate_token(self, length=6, valid_secs=300, commit=True):
"""
Generate a new token for this device.
Parameters:
- length: int - Token length in characters
- valid_secs: int - Token validity in seconds
- commit: bool - Whether to save the device immediately
Returns:
str - The generated token
"""
def verify_token(self, token) -> bool:
"""
Verify token by content and expiry.
Parameters:
- token: str - Token to verify
Returns:
bool - True if token is valid and not expired
"""Adds cooldown functionality to prevent rapid challenge generation.
class CooldownMixin(models.Model):
"""
Mixin that adds cooldown functionality between challenge generations.
Fields:
- last_generated_timestamp: DateTimeField - Last generation time
"""
last_generated_timestamp = models.DateTimeField(null=True, default=None)
@property
def cooldown_enabled(self) -> bool:
"""Returns True if cooldown duration > 0."""
def generate_is_allowed(self):
"""Check if generation is allowed based on cooldown status."""
def cooldown_reset(self, commit=True):
"""Reset cooldown by clearing the timestamp."""
def cooldown_set(self, commit=True):
"""Set cooldown timestamp to current time."""
def get_cooldown_duration(self) -> int:
"""Abstract method to return cooldown duration in seconds."""Adds exponential back-off for failed verification attempts.
class ThrottlingMixin(models.Model):
"""
Mixin that adds exponential back-off for failed verifications.
Fields:
- throttling_failure_timestamp: DateTimeField - Last failure time
- throttling_failure_count: PositiveIntegerField - Consecutive failures
"""
throttling_failure_timestamp = models.DateTimeField(null=True, default=None)
throttling_failure_count = models.PositiveIntegerField(default=0)
@property
def throttling_enabled(self) -> bool:
"""Returns True if throttle factor > 0."""
def verify_is_allowed(self):
"""Check if verification is allowed based on throttling status."""
def throttle_reset(self, commit=True):
"""Reset throttling by clearing failure count and timestamp."""
def throttle_increment(self, commit=True):
"""Increment failure count and update timestamp."""
def get_throttle_factor(self) -> int:
"""Abstract method to return throttle factor."""Adds creation and last-used timestamps to devices.
class TimestampMixin(models.Model):
"""
Mixin that adds creation and usage timestamps.
Fields:
- created_at: DateTimeField(auto_now_add=True) - Creation time
- last_used_at: DateTimeField(null=True) - Last usage time
"""
created_at = models.DateTimeField(auto_now_add=True)
last_used_at = models.DateTimeField(null=True, default=None)
def set_last_used_timestamp(self, commit=True):
"""Update last used timestamp to current time."""Manager class for Device models with custom query methods.
class DeviceManager(models.Manager):
"""Manager for Device models."""
def devices_for_user(self, user, confirmed=None):
"""
Returns queryset for user's devices.
Parameters:
- user: User - The user whose devices to retrieve
- confirmed: bool or None - Filter by confirmation status
Returns:
QuerySet - Filtered device queryset
"""Constants for challenge generation restriction reasons.
class GenerateNotAllowed(Enum):
"""Enum for generation restriction reasons."""
COOLDOWN_DURATION_PENDING = 'COOLDOWN_DURATION_PENDING'Constants for token verification restriction reasons.
class VerifyNotAllowed(Enum):
"""Enum for verification restriction reasons."""
N_FAILED_ATTEMPTS = 'N_FAILED_ATTEMPTS'from django_otp.models import Device, ThrottlingMixin, TimestampMixin
from django.db import models
class CustomDevice(TimestampMixin, ThrottlingMixin, Device):
"""Custom OTP device implementation."""
secret_key = models.CharField(max_length=32)
class Meta:
verbose_name = "Custom OTP Device"
def verify_token(self, token):
# Custom verification logic
if self.verify_is_allowed() != True:
return False
# Your token verification logic here
is_valid = self._verify_custom_token(token)
if is_valid:
self.throttle_reset()
self.set_last_used_timestamp()
return True
else:
self.throttle_increment()
return False
def get_throttle_factor(self):
return 1 # 1 second base throttle
def _verify_custom_token(self, token):
# Implement your verification logic
passfrom django_otp.models import SideChannelDevice, CooldownMixin, ThrottlingMixin
class EmailOTPDevice(CooldownMixin, ThrottlingMixin, SideChannelDevice):
"""Email-based OTP device with cooldown and throttling."""
email = models.EmailField()
def generate_challenge(self):
if self.generate_is_allowed() != True:
return False
# Generate and send token
token = self.generate_token()
self.send_email(token)
self.cooldown_set()
return True
def get_cooldown_duration(self):
return 60 # 60 second cooldown between emails
def get_throttle_factor(self):
return 2 # 2 second base throttle, exponential backofffrom django_otp.models import Device
def get_device_info(device):
"""Get comprehensive device information."""
info = {
'id': device.persistent_id,
'name': device.name,
'type': device.__class__.__name__,
'confirmed': device.confirmed,
'model_label': device.model_label(),
'is_interactive': device.is_interactive(),
}
# Add timestamp info if available
if hasattr(device, 'created_at'):
info['created_at'] = device.created_at
if hasattr(device, 'last_used_at'):
info['last_used_at'] = device.last_used_at
# Add security status
if hasattr(device, 'throttling_enabled'):
info['throttling_enabled'] = device.throttling_enabled
if hasattr(device, 'cooldown_enabled'):
info['cooldown_enabled'] = device.cooldown_enabled
return infoInstall with Tessl CLI
npx tessl i tessl/pypi-django-otp