CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-django-otp

A pluggable framework for adding two-factor authentication to Django using one-time passwords.

Pending
Overview
Eval results
Files

device-models.mddocs/

Device Models and Management

Abstract base models and mixins that provide the foundation for all OTP devices, including security features like throttling, cooldowns, and timestamp tracking.

Capabilities

Abstract Base Models

Device

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."""

SideChannelDevice

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
        """

Mixins

CooldownMixin

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."""

ThrottlingMixin

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."""

TimestampMixin

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."""

Managers

DeviceManager

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
        """

Enums

GenerateNotAllowed

Constants for challenge generation restriction reasons.

class GenerateNotAllowed(Enum):
    """Enum for generation restriction reasons."""
    COOLDOWN_DURATION_PENDING = 'COOLDOWN_DURATION_PENDING'

VerifyNotAllowed

Constants for token verification restriction reasons.

class VerifyNotAllowed(Enum):
    """Enum for verification restriction reasons."""
    N_FAILED_ATTEMPTS = 'N_FAILED_ATTEMPTS'

Usage Examples

Creating a Custom Device Type

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
        pass

Using Device Mixins

from 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 backoff

Device Discovery and Management

from 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 info

Install with Tessl CLI

npx tessl i tessl/pypi-django-otp

docs

admin-interface.md

core-authentication.md

device-models.md

django-integration.md

email-devices.md

hotp-devices.md

index.md

oath-algorithms.md

static-tokens.md

totp-devices.md

tile.json