TOTP / MFA¶
NeoZen supports Time-based One-Time Password (TOTP) as a second authentication factor. Users enroll via a QR code in an authenticator app (Google Authenticator, Authy, etc.) and verify with 6-digit codes.
Setup¶
from neozen.core.factors.totp import TOTPFactor, TOTPConfig
factor = TOTPFactor(
config=TOTPConfig(
issuer="My App",
digits=6,
interval=30,
backup_code_count=8,
backup_code_length=8,
),
subject_repository=user_repo_adapter,
backup_code_repository=backup_code_repo_adapter,
hasher=password_hasher,
)
Enrollment¶
TOTP enrollment is a two-step process:
Step 1: Begin Enrollment¶
enrollment = await factor.begin_enrollment(
user,
account_name=user.email,
)
# enrollment.provisioning_uri -- for QR code
# enrollment.secret -- base32 secret (manual entry)
# enrollment.qr_code_data_uri -- data URI for <img> tag (if qrcode installed)
Step 2: Confirm with Code¶
The user enters a code from their authenticator app:
updated_user, backup_codes = await factor.confirm_enrollment(
user,
code="123456", # from authenticator app
)
# backup_codes = ["ABCD1234", "EFGH5678", ...]
# Store/display these for the user
Authentication¶
After enrollment, verify TOTP codes during login:
Backup Codes¶
If a user loses their authenticator, they can use a backup code:
valid = await factor.authenticate(user, code="ABCD1234")
# Backup codes are single-use -- consumed on verification
Login Flow with 2FA¶
A typical two-step login flow looks like this:
# Step 1: Password
user = await password_factor.authenticate({
"identifier": email,
"password": password,
})
# Step 2: Check if TOTP is enabled
if user.totp_enabled:
# Store user ID in session, redirect to TOTP page
session["pending_2fa_user_id"] = user.id
return redirect("/login/totp")
# No 2FA -- log in directly
login_user(user)
# TOTP verification page
@app.route("/login/totp", methods=["POST"])
def verify_totp():
user_id = session.pop("pending_2fa_user_id")
user = User.get(user_id)
code = request.form["code"]
if await totp_factor.authenticate(user, code):
login_user(user)
return redirect("/dashboard")
flash("Invalid code", "error")
return redirect("/login/totp")
Configuration¶
| Setting | Default | Description |
|---|---|---|
issuer |
(required) | App name shown in authenticator |
digits |
6 | Code length |
interval |
30 | Code rotation interval (seconds) |
backup_code_count |
8 | Number of backup codes |
backup_code_length |
8 | Length of each backup code |
Dependencies¶
TOTP requires the totp extra:
This installs pyotp. QR code generation optionally uses qrcode.