A pluggable framework for adding two-factor authentication to Django using one-time passwords.
—
Forms, views, middleware, and decorators for integrating OTP authentication into Django applications. These components provide seamless integration with Django's built-in authentication system.
Complete authentication form with username, password, and OTP fields.
class OTPAuthenticationForm(OTPAuthenticationFormMixin, AuthenticationForm):
"""
Complete OTP authentication form with username/password/token.
Fields:
- username: CharField - User identification
- password: CharField - User password
- otp_device: CharField - Device selection
- otp_token: CharField - OTP token input
- otp_challenge: CharField - Challenge generation button
"""
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)Token verification form for already authenticated users.
class OTPTokenForm(OTPAuthenticationFormMixin, forms.Form):
"""
Token verification form for authenticated users.
Fields:
- otp_device: ChoiceField - Device selection
- otp_token: CharField - OTP token input
- otp_challenge: CharField - Challenge generation button
"""
otp_device = forms.ChoiceField()
otp_token = forms.CharField(required=False)
otp_challenge = forms.CharField(required=False)
def get_user(self):
"""Returns the authenticated user."""Shared functionality for OTP-aware authentication forms.
class OTPAuthenticationFormMixin:
"""Shared functionality for OTP-aware authentication forms."""
otp_error_messages = {
'invalid_token': 'Invalid token. Please make sure you have entered it correctly.',
'token_required': 'Please enter your authentication code.',
# ... more error messages
}
def clean_otp(self, user):
"""Process OTP fields and verify token."""
@staticmethod
def device_choices(user):
"""Return device choices for user."""Two-factor authentication login view that handles both password and OTP verification.
class LoginView(auth_views.LoginView):
"""Two-factor authentication login view."""
otp_authentication_form = OTPAuthenticationForm
otp_token_form = OTPTokenForm
@property
def authentication_form(self):
"""Dynamically select form class based on authentication state."""
def form_valid(self, form):
"""Handle successful form submission."""def login(request, **kwargs):
"""Function-based view wrapper for LoginView."""Decorator that requires users to be verified by an OTP device.
def otp_required(view=None, redirect_field_name='next', login_url=None, if_configured=False):
"""
Decorator requiring OTP verification.
Parameters:
- view: Function - View function (when used without parentheses)
- redirect_field_name: str - Field name for redirect URL
- login_url: str - URL to redirect to for authentication
- if_configured: bool - Allow users with no devices if True
Returns:
Decorator function or decorated view
"""Middleware that adds OTP device information to request.user.
class OTPMiddleware:
"""Middleware that adds OTP device information to request.user."""
def __init__(self, get_response=None):
"""Initialize middleware."""
def __call__(self, request):
"""Process request and add OTP info to user."""def is_verified(user) -> bool:
"""
Check if user is verified by OTP device.
Parameters:
- user: User - User instance to check
Returns:
bool - True if user is OTP-verified
"""otp_verification_failed = django.dispatch.Signal()Signal sent when OTP verification fails, providing hooks for logging or additional security measures.
from django_otp.decorators import otp_required
from django.shortcuts import render
@otp_required
def secure_view(request):
"""View requiring OTP verification."""
return render(request, 'secure_page.html')
@otp_required(if_configured=True)
def optional_otp_view(request):
"""View requiring OTP only if user has devices configured."""
return render(request, 'semi_secure_page.html')from django_otp.views import LoginView as OTPLoginView
from django.urls import reverse_lazy
class CustomOTPLoginView(OTPLoginView):
"""Custom login view with additional features."""
template_name = 'custom_login.html'
success_url = reverse_lazy('dashboard')
def form_valid(self, form):
"""Add custom logic after successful login."""
response = super().form_valid(form)
# Log successful OTP login
if hasattr(form, 'get_user') and hasattr(form.get_user(), 'otp_device'):
device = form.get_user().otp_device
print(f"User {form.get_user().username} logged in with {device.name}")
return response# settings.py
MIDDLEWARE = [
# ... other middleware
'django_otp.middleware.OTPMiddleware',
# ... more middleware
]
# In views
from django_otp.middleware import is_verified
def my_view(request):
if is_verified(request.user):
# User is OTP-verified
return render(request, 'secure_content.html')
else:
# User needs OTP verification
return redirect('otp_login')from django_otp.forms import OTPTokenForm
from django import forms
class CustomOTPForm(OTPTokenForm):
"""Custom OTP form with additional fields."""
remember_device = forms.BooleanField(
required=False,
label="Remember this device for 30 days"
)
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
# Customize device choices
device_choices = []
for device in self.device_choices(user):
device_choices.append((device[0], f"{device[1]} ({device[0][:8]}...)"))
self.fields['otp_device'].choices = device_choicesfrom django_otp.forms import otp_verification_failed
from django.dispatch import receiver
import logging
logger = logging.getLogger(__name__)
@receiver(otp_verification_failed)
def handle_otp_failure(sender, **kwargs):
"""Log OTP verification failures."""
request = kwargs.get('request')
user = kwargs.get('user')
if request and user:
logger.warning(
f"OTP verification failed for user {user.username} "
f"from IP {request.META.get('REMOTE_ADDR')}"
)# urls.py
from django_otp.views import LoginView
from django.urls import path
urlpatterns = [
path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
# ... other URLs
]<!-- login.html -->
<form method="post">
{% csrf_token %}
{{ form.username.label_tag }}
{{ form.username }}
{{ form.password.label_tag }}
{{ form.password }}
{% if form.otp_device %}
{{ form.otp_device.label_tag }}
{{ form.otp_device }}
{{ form.otp_token.label_tag }}
{{ form.otp_token }}
{% if form.otp_challenge %}
<button type="submit" name="otp_challenge" value="1">
Send Challenge
</button>
{% endif %}
{% endif %}
<button type="submit">Login</button>
</form># settings.py
# URL for OTP login page (default: /login/)
OTP_LOGIN_URL = '/auth/login/'
# Hide sensitive data in admin (default: False)
OTP_ADMIN_HIDE_SENSITIVE_DATA = TrueInstall with Tessl CLI
npx tessl i tessl/pypi-django-otp