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

admin-interface.mddocs/

Admin Interface

Admin classes and forms for managing OTP devices through Django's admin interface, including a two-factor authentication admin site.

Capabilities

OTP Admin Site

Admin site that requires two-factor authentication for access.

class OTPAdminSite(admin.AdminSite):
    """Admin site requiring two-factor authentication."""
    
    name = 'otpadmin'  # Default instance name
    login_form = OTPAdminAuthenticationForm
    login_template = None  # Auto-selected template
    
    def has_permission(self, request) -> bool:
        """Check user permissions including OTP verification."""

Admin Authentication Form

Authentication form for the OTP admin site.

class OTPAdminAuthenticationForm(AdminAuthenticationForm, OTPAuthenticationFormMixin):
    """Admin authentication form with OTP support."""
    
    otp_device = forms.CharField(widget=forms.Select)
    otp_token = forms.CharField(required=False, widget=forms.TextInput)
    otp_challenge = forms.CharField(required=False, widget=forms.TextInput)

Utility Functions

user_model_search_fields

Generate search fields and help text for user model fields.

def user_model_search_fields(field_names):
    """
    Generate search fields and help text for user model fields.
    
    Parameters:
    - field_names: list - List of field names to check
    
    Returns:
    tuple - (search_fields, help_text)
    """

Usage Examples

Setting Up OTP Admin Site

# admin.py
from django_otp.admin import OTPAdminSite
from django.contrib import admin

# Create OTP admin site instance
otp_admin_site = OTPAdminSite(name='otpadmin')

# Register models with OTP admin
from myapp.models import MyModel
otp_admin_site.register(MyModel)

# URLs.py
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('otpadmin/', otp_admin_site.urls),  # OTP-protected admin
]

Custom Device Admin

from django.contrib import admin
from django_otp.plugins.otp_totp.models import TOTPDevice
from django_otp.admin import user_model_search_fields

@admin.register(TOTPDevice)
class CustomTOTPDeviceAdmin(admin.ModelAdmin):
    """Custom admin for TOTP devices."""
    
    list_display = ['user', 'name', 'confirmed', 'created_at']
    list_filter = ['confirmed', 'created_at']
    search_fields = user_model_search_fields(['username', 'email'])
    readonly_fields = ['key', 'created_at', 'last_used_at']
    
    fieldsets = (
        (None, {
            'fields': ('user', 'name', 'confirmed')
        }),
        ('Device Settings', {
            'fields': ('digits', 'step', 'tolerance', 'drift'),
            'classes': ('collapse',)
        }),
        ('Security Info', {
            'fields': ('key', 'last_t', 'throttling_failure_count'),
            'classes': ('collapse',)
        }),
        ('Timestamps', {
            'fields': ('created_at', 'last_used_at'),
            'classes': ('collapse',)
        }),
    )
    
    def get_queryset(self, request):
        """Optimize queryset with select_related."""
        return super().get_queryset(request).select_related('user')

Admin Actions

from django.contrib import admin
from django.contrib import messages

class DeviceAdminMixin:
    """Mixin providing common device admin actions."""
    
    actions = ['reset_throttling', 'confirm_devices', 'reset_drift']
    
    def reset_throttling(self, request, queryset):
        """Reset throttling for selected devices."""
        count = 0
        for device in queryset:
            if hasattr(device, 'throttle_reset'):
                device.throttle_reset()
                count += 1
        
        messages.success(request, f'Reset throttling for {count} devices.')
    reset_throttling.short_description = "Reset throttling"
    
    def confirm_devices(self, request, queryset):
        """Confirm selected devices."""
        count = queryset.update(confirmed=True)
        messages.success(request, f'Confirmed {count} devices.')
    confirm_devices.short_description = "Confirm devices"
    
    def reset_drift(self, request, queryset):
        """Reset drift for TOTP devices."""
        count = 0
        for device in queryset:
            if hasattr(device, 'drift'):
                device.drift = 0
                device.save()
                count += 1
        
        messages.success(request, f'Reset drift for {count} devices.')
    reset_drift.short_description = "Reset drift"

@admin.register(TOTPDevice)
class TOTPDeviceAdmin(DeviceAdminMixin, admin.ModelAdmin):
    # ... admin configuration

Inline Device Management

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin
from django_otp.plugins.otp_totp.models import TOTPDevice

class TOTPDeviceInline(admin.TabularInline):
    """Inline admin for TOTP devices."""
    
    model = TOTPDevice
    extra = 0
    readonly_fields = ['key', 'last_t', 'drift']
    fields = ['name', 'confirmed', 'digits', 'step', 'tolerance']

class CustomUserAdmin(UserAdmin):
    """User admin with OTP device inlines."""
    
    inlines = UserAdmin.inlines + [TOTPDeviceInline]

# Unregister default user admin and register custom one
admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)

Admin Templates

<!-- admin/login.html override for OTP admin -->
{% extends "admin/login.html" %}

{% block content %}
<div id="content-main">
    <form method="post" id="login-form">
        {% csrf_token %}
        
        <div class="form-row">
            {{ form.username.label_tag }}
            {{ form.username }}
        </div>
        
        <div class="form-row">
            {{ form.password.label_tag }}
            {{ form.password }}
        </div>
        
        {% if form.otp_device %}
        <div class="form-row">
            {{ form.otp_device.label_tag }}
            {{ form.otp_device }}
        </div>
        
        <div class="form-row">
            {{ form.otp_token.label_tag }}
            {{ form.otp_token }}
        </div>
        
        {% if form.otp_challenge %}
        <div class="form-row">
            <input type="submit" name="otp_challenge" value="Send Challenge" class="default">
        </div>
        {% endif %}
        {% endif %}
        
        <div class="submit-row">
            <input type="submit" value="Log in">
        </div>
    </form>
</div>
{% endblock %}

Admin Dashboard Customization

from django.contrib import admin
from django.template.response import TemplateResponse
from django.urls import path
from django_otp import devices_for_user

class CustomOTPAdminSite(OTPAdminSite):
    """Custom OTP admin site with dashboard."""
    
    def get_urls(self):
        """Add custom URLs."""
        urls = super().get_urls()
        custom_urls = [
            path('otp-dashboard/', self.admin_view(self.otp_dashboard), name='otp_dashboard'),
        ]
        return custom_urls + urls
    
    def otp_dashboard(self, request):
        """Custom OTP dashboard view."""
        
        context = {
            'title': 'OTP Dashboard',
            'user_devices': list(devices_for_user(request.user)) if request.user.is_authenticated else [],
            'total_users_with_otp': User.objects.filter(
                id__in=Device.objects.values_list('user_id', flat=True).distinct()
            ).count(),
        }
        
        return TemplateResponse(request, 'admin/otp_dashboard.html', context)
    
    def index(self, request, extra_context=None):
        """Customize admin index."""
        extra_context = extra_context or {}
        extra_context['otp_dashboard_url'] = 'admin:otp_dashboard'
        
        return super().index(request, extra_context)

Configuration

Admin Settings

# settings.py

# Hide sensitive data in admin interface
OTP_ADMIN_HIDE_SENSITIVE_DATA = True

Admin Site Registration

# Register with default admin
from django.contrib import admin
from django_otp.plugins.otp_totp.admin import TOTPDeviceAdmin
from django_otp.plugins.otp_totp.models import TOTPDevice

admin.site.register(TOTPDevice, TOTPDeviceAdmin)

# Or use OTP admin site
from django_otp.admin import OTPAdminSite

otp_admin = OTPAdminSite()
otp_admin.register(TOTPDevice, TOTPDeviceAdmin)

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