TOTP & 2FA¶
2 endpoints for RFC 6238 Time-based One-Time Password (TOTP) two-factor authentication — generate a shared secret with a ready-to-scan QR code, and verify codes submitted by users.
| Method | Endpoint | Purpose |
|---|---|---|
GET |
/v1/auth/totp-generate |
Generate a TOTP secret, otpauth URI, and QR code |
POST |
/v1/auth/totp-verify |
Verify a TOTP code against a shared secret |
Python SDK Examples¶
Generate a TOTP secret for a new user¶
totp_generate returns everything needed to onboard a user: the raw secret (store it encrypted in your DB), an otpauth:// URI (for deep-linking into authenticator apps), and a base64-encoded PNG QR code (embed it directly in an <img> tag without storing a file).
from toolkitapi import Auth
auth = Auth(api_key="tk_...")
result = auth.totp_generate(
issuer="MyApp",
account_name="[email protected]",
)
print(result["secret"]) # Base32 secret — store this encrypted per user
print(result["uri"]) # otpauth://totp/MyApp:alice%40toolkitapi.io?...
print(result["current_code"]) # Current valid 6-digit code (for testing)
# result["qr_code"] is a data URI: "data:image/png;base64,iVBOR..."
Embed the QR code directly in an HTML setup page¶
The qr_code field is a data:image/png;base64,... data URI — no file storage or separate image endpoint needed.
from toolkitapi import Auth
from markupsafe import Markup # Flask / Jinja2
auth = Auth(api_key="tk_...")
def get_2fa_setup_data(user_email: str) -> dict:
"""Generate TOTP enrollment data for a user."""
result = auth.totp_generate(
issuer="MyApp",
account_name=user_email,
)
return {
"secret": result["secret"], # Persist (encrypted) before rendering
"qr_code_src": result["qr_code"],
"uri": result["uri"],
}
# In your Jinja2 template:
# <img src="{{ setup.qr_code_src }}" alt="Scan with your authenticator app">
# <p>Manual entry key: <code>{{ setup.secret }}</code></p>
Configure algorithm and period¶
The defaults (SHA1, 30-second window, 6 digits) are compatible with every major authenticator app. Use custom settings only if you control the client app.
from toolkitapi import Auth
auth = Auth(api_key="tk_...")
# 8-digit codes, 60-second window — adds length and convenience for kiosk UIs
result = auth.totp_generate(
issuer="MyApp",
account_name="[email protected]",
digits=8,
period=60,
algorithm="SHA256",
)
Verify a TOTP code at login¶
After enrollment, call totp_verify on every 2FA prompt. The window parameter controls how many periods either side of the current time to accept — 1 allows one period of clock drift (±30 s by default), which accommodates most real-world device clock skew.
from toolkitapi import Auth
auth = Auth(api_key="tk_...")
def verify_2fa(user_totp_secret: str, code_from_user: str) -> bool:
"""Return True if the submitted code is valid."""
result = auth.totp_verify(user_totp_secret, code_from_user)
return result["valid"]
Full 2FA login flow¶
from toolkitapi import Auth
auth = Auth(api_key="tk_...")
def complete_login(
user: dict,
submitted_code: str,
max_drift_periods: int = 1,
) -> dict:
"""
Second step of a 2FA login.
Parameters
----------
user : dict
User record with ``totp_secret`` field (store encrypted, decrypt before use).
submitted_code : str
6-digit code entered by the user.
max_drift_periods : int
Number of periods either side of the current window to accept.
Each period is 30 s by default (TOTP standard).
"""
result = auth.totp_verify(
user["totp_secret"],
submitted_code,
window=max_drift_periods,
)
if not result["valid"]:
raise ValueError("Invalid 2FA code")
# Optional: log drift for monitoring
if abs(result["drift"]) > 0:
print(f"Clock drift detected: {result['drift']} period(s)")
return {"authenticated": True, "user_id": user["id"]}
Regenerate a TOTP secret (user reset)¶
If a user loses their authenticator, issue a new secret. Always invalidate the old secret server-side before returning the new one.
from toolkitapi import Auth
auth = Auth(api_key="tk_...")
def reset_2fa(user_email: str) -> dict:
"""Revoke the old TOTP secret and generate a fresh enrollment."""
result = auth.totp_generate(
issuer="MyApp",
account_name=user_email,
)
# Persist result["secret"] to the user record (encrypted, replace previous)
return {
"new_secret": result["secret"],
"qr_code_src": result["qr_code"],
"uri": result["uri"],
}
Response Fields¶
totp-generate response:
| Field | Type | Description |
|---|---|---|
secret |
string | Base32-encoded shared secret — store this encrypted |
uri |
string | otpauth://totp/... URI for deep-linking into authenticator apps |
qr_code |
string | data:image/png;base64,... data URI of the QR code PNG |
current_code |
string | Current valid code (for testing only) |
totp-verify response:
| Field | Type | Description |
|---|---|---|
valid |
bool | Whether the submitted code is valid |
drift |
int | Number of periods of clock drift detected (0 = current window) |
Tip
Store the TOTP secret encrypted at rest — it is a long-lived credential equivalent to a password. Never expose it after the initial enrollment flow. If a user suspects their authenticator is compromised, use reset_2fa to issue a new secret and invalidate the old one immediately.