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.