Two-Factor Authentication
Overview¶
SpeedPy includes two-factor authentication (2FA) powered by django-otp. Users can enable TOTP (Time-based One-Time Password) with any authenticator app and generate backup codes for recovery.
How It Works¶
- User logs in with email and password (via allauth)
- The custom
AccountAdapterchecks if the user has OTP devices - If OTP is enabled, login is interrupted and the user is redirected to
/accounts/login/otp/ - User enters their TOTP code (or a backup code)
- On successful verification, the login completes
This flow works for both email/password and social logins (GitHub, Google, GitLab).
Installed Apps & Middleware¶
INSTALLED_APPS = [
...
"django_otp",
"django_otp.plugins.otp_totp",
"django_otp.plugins.otp_static",
...
]
MIDDLEWARE = [
...
"django_otp.middleware.OTPMiddleware",
...
]
URL Routes¶
All OTP management routes are in mainapp/urls.py:
| URL | View | Purpose |
|---|---|---|
/accounts/otp/settings/ |
OTPSettingsView |
View 2FA status |
/accounts/otp/setup/ |
OTPSetupView |
Start TOTP setup (shows QR code) |
/accounts/otp/verify-setup/ |
OTPVerifySetupView |
Confirm setup with a code |
/accounts/otp/backup-codes/ |
OTPBackupCodesView |
View backup codes |
/accounts/otp/disable/ |
OTPDisableView |
Disable 2FA |
/accounts/otp/regenerate-backup-codes/ |
OTPRegenerateBackupCodesView |
Generate new backup codes |
/accounts/login/otp/ |
OTPLoginView |
Enter OTP during login |
Configuration¶
OTP_TOTP_ISSUER = env.str("OTP_TOTP_ISSUER", default="uptimefor.me")
OTP_LOGIN_URL = reverse_lazy("account_login_otp")
Set OTP_TOTP_ISSUER to your app name — this is what users see in their authenticator app.
Social logins and MFA¶
MFA does not cover social sign-in. Anyone who turns on two-factor authentication will reasonably assume it guards every way into their account, so this is worth deciding on purpose rather than discovering later.
With SPEEDPY_MFA_BACKEND=django_otp it is not covered because of a bug — see
the note in Authentication. The
pre_social_login hook that would enforce it sits on the wrong adapter and never
runs.
Once that is fixed, you still have a choice to make:
Enforce it. Your MFA promise then holds for every route. Choose this if you sell to customers who expect "2FA on" to mean exactly that, or if you may need to answer a security questionnaire about it.
Accept the bypass. The provider — Google, GitHub, GitLab — is the identity provider on that route and carries its own second factor, usually a stronger one than a TOTP app. Choose this if your users are consumers and the extra prompt would cost more in abandonment than it buys in security.
Either choice is defensible. What is not defensible is not knowing which one you shipped. If you accept the bypass, say so on your security page, because a customer who enabled MFA and later learns it did not cover Google sign-in will read silence as a cover-up rather than a trade-off.
Note that allauth_mfa and django_otp behave differently here, so switching
SPEEDPY_MFA_BACKEND can change this behaviour without you touching any of your
own code.
QR Code Generation¶
SpeedPy includes the qrcode package for generating QR codes during TOTP setup. Users scan the QR code with their authenticator app (Google Authenticator, Authy, 1Password, etc.).
Backup Codes¶
When a user enables 2FA, static backup codes are generated via django_otp.plugins.otp_static. These one-time codes can be used if the user loses access to their authenticator app.