Production-ready TOTP (Time-based One-Time Password) support for Django and Django REST Framework.
Project description
django-totp
Production-ready TOTP (Time-based One-Time Password) support for Django and Django REST Framework.
django-totp helps you add two-factor authentication (2FA) to your Django project with:
- Secure TOTP secret storage (Fernet encryption)
- Enrollment QR generation (SVG)
- Backup code generation, verification, and rotation
- DRF endpoints for enrollment lifecycle
- Token helpers for two-step authentication flows
This README is the single source of documentation for installation, configuration, integration, and operations.
Table of Contents
- Overview
- Features
- Requirements
- Installation
- Quick Start
- Configuration Reference
- API Endpoints
- Integrating 2FA Into Login Flow
- Security and Production Checklist
- Troubleshooting
- Data Model
- Public Python API
Overview
django-totp stores each user's TOTP secret in encrypted form and provides API actions to:
- Create enrollment and return a QR code
- Confirm enrollment using a valid OTP
- Return one-time backup recovery codes
- Rotate backup codes
- Disable TOTP
You can use it as:
- A drop-in REST API module in an existing Django + DRF project
- A building block for custom authentication endpoints
Features
- Encrypted secret storage using cryptography.Fernet
- Configurable issuer name for authenticator apps
- One-to-one user-to-TOTP mapping
- Configurable number of backup codes per user
- Backup code verification with one-time-use marking
- Rate limiting for TOTP endpoints
- Signed short-lived token helpers for step-up login flows
Requirements
- Python 3.12+
- Django 5.0+
- Django REST Framework 3.15+
Installed dependencies used by this package:
- cryptography
- pyotp
- qrcode
Installation
Install from PyPI:
pip install django-totp
Quick Start
1. Add apps
In Django settings:
INSTALLED_APPS = [
# Django apps...
"rest_framework",
"django_totp",
]
2. Set encryption key (required)
Generate a Fernet key once:
python -c "from django_totp.encryption import generate_fernet_key; print(generate_fernet_key())"
Add it as an environment variable:
TOTP_ENCRYPTION_KEY=your-generated-key
And load in settings:
import os
TOTP_ENCRYPTION_KEY = os.environ["TOTP_ENCRYPTION_KEY"]
Important:
- Do not generate a new key on each start in production
- Changing this key later makes previously encrypted TOTP data unreadable
3. Include URLs
In your project URL configuration:
from django.urls import include, path
urlpatterns = [
# your routes...
path("api/", include("django_totp.urls")),
]
4. Run migrations
python manage.py migrate
5. Call endpoints as authenticated user
Enrollment create:
POST /api/totp/create/
Confirm enrollment:
POST /api/totp/confirm/
Disable TOTP:
POST /api/totp/disable/
Rotate backup codes:
POST /api/totp/rotate_backup_codes/
Configuration Reference
All settings below are read from Django settings.
TOTP_ENCRYPTION_KEY
- Required: Yes
- Type: string (valid Fernet key)
- Purpose: Encrypts TOTP secrets and backup codes at rest
If missing or invalid, django-totp raises ImproperlyConfigured.
TOTP_ISSUER
- Required: No
- Default: MyApp
- Type: string
- Purpose: Issuer label shown in authenticator apps
Example:
TOTP_ISSUER = "XYZ Platform"
TOTP_MAX_BACKUP_CODES
- Required: No
- Default: 10
- Type: integer
- Purpose: Number of backup codes generated per user set
Example:
TOTP_MAX_BACKUP_CODES = 12
TOTP_THROTTLE_RATE
- Required: No
- Default: 10/minute
- Type: DRF throttle rate string
- Purpose: Rate limit for all django-totp endpoint actions
Example:
TOTP_THROTTLE_RATE = "5/minute"
TOTP_TOKEN_SALT
- Required: No
- Default: django-totp-token-salt
- Type: string
- Purpose: Salt used for signed temporary token helpers
TOTP_TOKEN_MAX_AGE
- Required: No
- Default: 120
- Type: integer (seconds)
- Purpose: Token expiry for signed temporary token helpers
API Endpoints
Base path assumes you include django_totp.urls at /api/.
All endpoints:
- Require authenticated user
- Use DRF user throttle (configured by TOTP_THROTTLE_RATE)
- Return error payload as JSON with detail field on validation/service errors
POST /api/totp/create/
Starts TOTP enrollment. Creates an encrypted secret and returns QR SVG.
Request body:
- Empty
Success response (201):
{
"svg": "<svg ...>...</svg>"
}
Error examples (400):
- TOTP already exists for this user
POST /api/totp/confirm/
Confirms enrollment using a valid code from authenticator app and returns backup codes.
Request body:
{
"input_code": "123456"
}
Success response (200):
{
"backup_codes": ["code1", "code2", "..."]
}
Error examples (400):
- User does not have an associated TOTP secret
- Invalid TOTP code
POST /api/totp/disable/
Disables TOTP and deletes associated backup codes.
Request body:
- Empty
Success response:
- 204 No Content
Error examples (400):
- User does not have an associated TOTP secret
POST /api/totp/rotate_backup_codes/
Replaces all existing backup codes with a new set.
Request body:
- Empty
Success response (200):
{
"backup_codes": ["new1", "new2", "..."]
}
Integrating 2FA Into Login Flow
Typical 2-step login flow:
- Validate username/password
- If user has TOTP enabled, issue short-lived signed challenge token
- Ask user for TOTP code (or backup code)
- Verify code
- Issue final session/JWT only after successful 2FA verification
Helper functions are available in django_totp.auth and django_totp.totp.
Compatibility note:
- If your project uses a custom AUTH_USER_MODEL, prefer implementing your own token-to-user resolver with django.contrib.auth.get_user_model() rather than directly using get_user_from_challenge_token.
Example DRF views (reference pattern):
from django.contrib.auth import authenticate
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework import status
from django_totp.auth import (
generate_challenge_token,
get_user_from_challenge_token,
is_totp_enabled,
)
from django_totp.totp import verify_totp_code
from django_totp.backup_code_utils import verify_backup_code
@api_view(["POST"])
@permission_classes([AllowAny])
def login_password_step(request):
user = authenticate(
request,
username=request.data.get("username"),
password=request.data.get("password"),
)
if not user:
return Response({"detail": "Invalid credentials."}, status=401)
if not is_totp_enabled(user):
# Issue your final auth token/session here
return Response({"requires_2fa": False}, status=200)
challenge_token = generate_challenge_token(user)
return Response(
{
"requires_2fa": True,
"totp_challenge_token": challenge_token,
},
status=200,
)
@api_view(["POST"])
@permission_classes([AllowAny])
def login_totp_step(request):
challenge_token = request.data.get("totp_challenge_token")
otp_code = request.data.get("otp_code")
backup_code = request.data.get("backup_code")
if not challenge_token:
return Response({"detail": "Missing challenge token."}, status=400)
try:
user = get_user_from_challenge_token(challenge_token)
except Exception:
return Response({"detail": "Invalid or expired token."}, status=400)
ok = False
if otp_code:
ok = verify_totp_code(user, otp_code)
elif backup_code:
ok = verify_backup_code(user, backup_code)
if not ok:
return Response({"detail": "Invalid 2FA code."}, status=400)
# Issue your final auth token/session here
return Response({"authenticated": True}, status=200)
Security and Production Checklist
- Set TOTP_ENCRYPTION_KEY from secure secret manager or environment
- Rotate application secrets using planned migration strategy
- Enforce HTTPS everywhere (especially authentication APIs)
- Use strict throttling for login and TOTP verification endpoints
- Store backup codes only in secure client context right after generation
- Never log plaintext OTP or backup codes
- Add audit logging for enrollment, disable, and backup code rotation events
- Add monitoring for brute-force patterns and unusual failure rates
- Keep Django and cryptography dependency versions current
Troubleshooting
ImproperlyConfigured: TOTP_ENCRYPTION_KEY must be set
Cause:
- Missing or invalid Fernet key
Fix:
- Generate a valid Fernet key
- Set TOTP_ENCRYPTION_KEY in environment
- Restart app processes
Confirm endpoint always returns Invalid TOTP code
Cause candidates:
- Device clock drift
- Wrong issuer/account scanned
- Code copied late/expired
Fixes:
- Ensure server time is synchronized (NTP)
- Re-run enrollment create and rescan QR
- Submit current active code from authenticator app
Backup code rejected
Cause:
- Backup code already used (one-time)
- Input mismatch due to copy/paste/whitespace issues
Fix:
- Rotate backup codes and securely redistribute
Data Model
django_totp creates two models:
- Totp
- user (one-to-one with AUTH_USER_MODEL)
- secret_key (encrypted)
- created_at
- BackupCode
- totp (foreign key)
- code (encrypted)
- is_used
- created_at
Public Python API
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)
Contributing
Contributions are welcome! Please open issues for bugs or feature requests, and submit pull requests for any improvements.
Maintainers
- Kumar Sahil
- GitHub: @krsahil8825
- Email: krsahil8825@gmail.com
License
MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_totp-0.1.0a2.tar.gz.
File metadata
- Download URL: django_totp-0.1.0a2.tar.gz
- Upload date:
- Size: 50.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
915726d76bfef57661d136ee10b3fe68d2d54c405225b2ea266a13d895f5b2a3
|
|
| MD5 |
e066ecc9df8a0d75bc57e3d72cbf5ceb
|
|
| BLAKE2b-256 |
9d37c3648c8aaa10362fd2a3b53c29a789442dd74bca2662e1864219503c5f19
|
File details
Details for the file django_totp-0.1.0a2-py3-none-any.whl.
File metadata
- Download URL: django_totp-0.1.0a2-py3-none-any.whl
- Upload date:
- Size: 14.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c33580a434ca844a346ea868bd3a737447177b643b779b559eb2278382bb1e6e
|
|
| MD5 |
9f3340bb03e3a89e15356693ca2c5a2e
|
|
| BLAKE2b-256 |
9a38b683e648f3ef6e8dc61697e0c4aaa9331f4793d26c6cfa404b3aeb1d870b
|