Multi Factor Authentication¶
The Grit SDK provides a Multi-Factor Authentication (MFA) system that adds a second verification step to the login flow. The built-in method is email one-time passcode (OTP); backup codes are issued at setup so users can recover if they lose access to their email.
Overview¶
| Lifecycle Stage | What happens |
|---|---|
| Setup | User visits mfa_setup, receives an email OTP, and confirms it to activate an MFADevice |
| Backup codes | On successful setup, a batch of single-use backup codes is generated and shown once |
| Login challenge | At login, if the user has an active MFA device, a fresh OTP is emailed and the user is redirected to mfa_verify |
| Verification | User submits the OTP (or a backup code); on success they are logged in |
| Enforcement | Optional middleware redirects users without MFA to mfa_setup when MFA_ENFORCEMENT = 'mandatory' |
| Disable | User confirms their password to remove the MFA device and all backup codes |
Enforcement Modes¶
Set via AUTH_SETTINGS['MFA_ENFORCEMENT'] in app/settings.py.
| Mode | Behavior |
|---|---|
disabled |
MFA is effectively off. Existing MFADevice rows are still honored at login, but no new enrollment is expected. |
optional |
Users may enroll via mfa_setup and will then be challenged at login. No enforcement — unenrolled users log in normally. |
mandatory |
Authenticated users without an active MFADevice are redirected to mfa_setup on every request (except MFA, login, logout, and signup paths). Requires MFAEnforcementMiddleware to be installed. |
Settings¶
Overridable under the AUTH_SETTINGS key.
| Setting | Default | Description |
|---|---|---|
MFA_ENFORCEMENT |
'optional' |
One of mandatory, optional, disabled. See Enforcement Modes. |
MFA_METHODS |
['email'] |
Enabled MFA methods. Email OTP is currently the only built-in method. |
MFA_BACKUP_CODE_COUNT |
10 |
Number of backup codes issued at setup and on regeneration. |
MFA_CODE_EXPIRY_SECONDS |
600 |
Email OTP lifetime, in seconds. Applies to both setup and login challenges. |
Data Models¶
MFADevice¶
Represents a user's registered MFA method. A device is is_active=False between the time a setup OTP is sent and the time the user confirms it; only active devices trigger the login challenge.
Fields:
| Field | Type | Description |
|---|---|---|
user |
ForeignKey(CustomUser) |
The owning user. Reverse accessor: user.mfa_devices. |
name |
CharField(max_length=255) |
Human-readable label. Defaults to 'Authenticator App'. |
method |
CharField |
The MFA method — see MFAMethod below. |
is_active |
BooleanField |
True only after the user confirms setup with a valid code. |
metadata |
JSONField |
Arbitrary key-value data. |
MFADevice.MFAMethod¶
BackupCode¶
Single-use recovery codes for when the user cannot receive the email OTP. Codes are stored hashed — plaintext is shown to the user exactly once, at the moment of generation.
Fields:
| Field | Type | Description |
|---|---|---|
user |
ForeignKey(CustomUser) |
The owning user. Reverse accessor: user.backup_codes. |
code_hash |
CharField(max_length=255) |
Hashed with make_password. Verified with check_password. |
is_used |
BooleanField |
Marked True after a successful redemption. Used codes are never re-issued. |
How the Login Flow Works¶
The login flow is split into two phases. Until the OTP is verified, the user is not authenticated — they are in an MFA-limbo state tracked entirely in the session.
- Phase 1 — Credentials. The user submits email and password to
login. If the credentials are valid and the user has an activeMFADevice: - A fresh 6-digit code is generated via
generate_otp_code()and emailed viasend_mfa_email(). - Session limbo keys are written (see below).
- The user is redirected to
mfa_verify. - Phase 2 — Verification. At
mfa_verify, the user submits the OTP (or an unused backup code). On success the session limbo keys are cleared,login()is finally called, and the user is redirected toLOGIN_REDIRECT_URL.
If the user has no active MFADevice, phase 2 is skipped and login() runs immediately after phase 1.
Session limbo keys (internal)¶
During the limbo window, the following keys live on request.session. These are internal implementation details, not a stable API — do not read or write them from application code:
| Session key | Purpose |
|---|---|
_mfa_user_id |
The CustomUser.pk awaiting verification. |
_mfa_backend |
The auth backend dotted path, replayed into login() after success. |
_mfa_code_hash |
Hashed OTP, verified with check_password. |
_mfa_code_expires |
ISO-8601 expiry timestamp, compared against timezone.now(). |
Middleware¶
MFAEnforcementMiddleware¶
Install in MIDDLEWARE after SessionActivityMiddleware. It handles two concerns:
- Limbo redirect. If
_mfa_user_idis present on the session but the user is not yet authenticated, all non-exempt requests are redirected tomfa_verify. This prevents an attacker who somehow acquired a half-authenticated session from browsing the site. - Mandatory enforcement. When
MFA_ENFORCEMENT = 'mandatory', authenticated users without an activeMFADeviceare redirected tomfa_setupon every non-exempt request.
Exempt path prefixes (always bypass both checks):
MFA URLs¶
All MFA URLs are mounted under the auth URL prefix.
Setup¶
Emails the user an OTP, then activates a new MFADevice once the OTP is confirmed. On success, renders the backup codes page exactly once.
| Detail | Value |
|---|---|
| URL name | mfa_setup |
| Methods | GET (send code) / POST (confirm code) |
| Template | customauth/mfa_setup.html |
Verify¶
Phase 2 of login. Accepts either the emailed OTP or an unused backup code.
| Detail | Value |
|---|---|
| URL name | mfa_verify |
| Methods | GET / POST |
| Template | customauth/mfa_verify.html |
Resend¶
Regenerates and re-emails the login OTP for a user in MFA limbo.
| Detail | Value |
|---|---|
| URL name | mfa_resend |
| Methods | GET (performs resend and redirects) |
Disable¶
Removes the user's MFADevice rows and all BackupCode rows. Requires password re-entry.
| Detail | Value |
|---|---|
| URL name | mfa_disable |
| Methods | GET / POST |
| Template | customauth/mfa_disable.html |
Regenerate Backup Codes¶
Deletes existing backup codes and issues a fresh batch. Requires password re-entry.
| Detail | Value |
|---|---|
| URL name | mfa_backup_codes |
| Methods | GET / POST |
| Template | customauth/mfa_regenerate_backup_codes.html |
Requiring MFA in App Code¶
Use these when you want specific views to require an active MFA device — regardless of the global MFA_ENFORCEMENT setting. Users without MFA are redirected to mfa_setup.
mfa_required decorator¶
For function-based views. Composes with login_required.
MFARequiredMixin¶
For class-based views. Extends LoginRequiredMixin.
from grit.auth.decorators import MFARequiredMixin
class BillingSettingsView(MFARequiredMixin, View):
...
Session Metadata¶
After a successful MFA verification, the corresponding UserSession.metadata JSON is stamped with:
| Key | Value |
|---|---|
mfa_method_used |
'email' if verified via OTP, 'backup_code' if a backup code was redeemed. |
mfa_verified_at |
ISO-8601 timestamp of the successful verification. |
See Session Management for the surrounding UserSession model and device-list UI.