A pluggable framework for adding two-factor authentication to Django using one-time passwords.
—
Admin classes and forms for managing OTP devices through Django's admin interface, including a two-factor authentication 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."""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)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)
"""# 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
]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')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 configurationfrom 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/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 %}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)# settings.py
# Hide sensitive data in admin interface
OTP_ADMIN_HIDE_SENSITIVE_DATA = True# 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