Adding TOTP-Based 2FA to Django REST Framework with django-totp

Two-factor authentication (2FA) is becoming a standard requirement for modern applications, especially for APIs that use JWT authentication or separate frontend/backend architectures.
While working on Django REST Framework projects, I wanted a lightweight and API-focused way to add TOTP authentication without depending heavily on template-based flows or admin integrations.
So I built django-totp.
It is a reusable Django package that provides:
TOTP enrollment
QR generation
backup recovery codes
encrypted secret storage
DRF endpoints
helper utilities for multi-step authentication flows
PyPI: django-totp
Requirements
Python 3.12+
Django 5.0+
Django REST Framework 3.15+
Installation
Install the package from PyPI:
pip install django-totp
Add the apps to your Django settings:
INSTALLED_APPS = [
# Django apps...
"rest_framework",
"django_totp",
]
Configure the Encryption Key
TOTP secrets and backup codes are stored using Fernet encryption.
Generate a key once
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Add it to your environment
TOTP_ENCRYPTION_KEY=your-generated-key
Load it in Django settings
import os
TOTP_ENCRYPTION_KEY = os.environ["TOTP_ENCRYPTION_KEY"]
Include the URLs
from django.urls import include, path
urlpatterns = [
path("api/", include("django_totp.urls")),
]
Run migrations:
python manage.py migrate
Available Endpoints
The package provides endpoints for the full enrollment lifecycle.
Create Enrollment
POST /api/totp/create/
Creates a TOTP secret and returns an SVG QR code.
Example response:
{
"svg": "<svg ...>...</svg>"
}
Confirm Enrollment
POST /api/totp/confirm/
Request:
{
"input_code": "123456"
}
Successful confirmation returns backup recovery codes.
Disable TOTP
POST /api/totp/disable/
Disables TOTP and removes backup codes.
Rotate Backup Codes
POST /api/totp/rotate_backup_codes/
Generates a new backup code set.
Example Login Flow
A common authentication flow looks like this:
1. Validate username/password
2. Check whether the user has TOTP enabled
3. Issue a temporary challenge token
4. Ask for TOTP or backup code
5. Verify the code
6. Issue final JWT/session
The package includes helper utilities for this flow.
Example:
from django_totp.auth import (
generate_challenge_token,
is_totp_enabled,
)
from django_totp.totp import verify_totp_code
Other Utilities
Useful helpers you can import directly:
django_totp.auth
is_totp_enabled(user)
generate_challenge_token(user)
verify_challenge_token(token)
get_user_from_challenge_token(token)
django_totp.totp
generate_totp_secret()
verify_totp_code(user, input_code)
create_totp_setup(user)
confirm_totp_setup(user, input_code)
disable_totp(user)
django_totp.backup_code_utils
store_backup_codes(user, codes)
verify_backup_code(user, input_code)
rotate_backup_codes(user)
django_totp.encryption
generate_fernet_key()
resolve_fernet_key(default=None)
encrypt(value)
decrypt(value)
Features
The package currently includes:
Encrypted TOTP secret storage
QR generation for authenticator apps
Backup code generation and rotation
One-time-use backup code validation
DRF integration
Configurable issuer name
Endpoint throttling support
Signed temporary challenge tokens
Why I Built It
Many existing Django 2FA solutions are designed primarily for server-rendered applications.
I wanted something focused more on:
DRF APIs
JWT authentication flows
SPA/mobile frontends
reusable API endpoints
The goal was to keep the package relatively lightweight while still covering common 2FA requirements.
Project Links
Feedback, issues, and contributions are welcome.
