Olympus Docs
Identity

TOTP and WebAuthn

Time-based one-time passwords and WebAuthn for second-factor authentication

Overview

Olympus supports TOTP (Time-based One-Time Password) as a second authentication factor, implemented via Kratos's native totp self-service strategy. Users enroll an authenticator app (Google Authenticator, Authy, 1Password, Bitwarden, and any RFC 6238-compatible app), then provide a 6-digit code at every login after the password step.

All TOTP secret generation, code validation, and backup code management are handled by Kratos. Hera provides the UI surfaces. No custom TOTP cryptography is implemented in Hera.


How It Works

TOTP in Olympus has two distinct flows: enrollment (done once in account settings) and the login challenge (triggered on every subsequent login).

Enrollment Flow

Account Settings → Security → Authenticator App


Hera initiates Kratos settings flow (GET /self-service/settings/browser)


Kratos returns flow with TOTP nodes:
  - totp_secret_key  (plaintext secret for manual entry)
  - totp_qr          (otpauth:// URI for QR code rendering)


Hera renders:
  - QR code (SVG, client-side rendering, no external calls)
  - Manual secret key (read-only, autocomplete="off")
  - 6-digit code input (autocomplete="one-time-code")
  - Submit button


User scans QR or enters secret manually into authenticator app
User enters 6-digit verification code and submits


Hera POSTs to Kratos: { method: "totp", totp_code: "123456" }

        ├─ Invalid code → inline error, QR remains visible, no page reload

        └─ Valid code → Kratos marks TOTP credential as active
                     → Kratos generates backup codes (lookup_secret strategy)
                     → Hera extracts backup codes from settings flow response
                     → Hera renders backup codes (displayed once, see below)

Login Challenge Flow

User completes password step (AAL1)


Kratos detects TOTP enrolled + AAL2 required


Kratos redirects to /login?flow=<id> at AAL2 step


Hera detects AAL2 step in flow response, renders TOTP challenge screen:
  - "Enter your authenticator code" heading
  - 6-digit numeric input (auto-focused, inputmode="numeric", pattern="[0-9]*")
  - Submit button
  - "Use a backup code instead" link

        ├─ Invalid code → inline error with remaining attempts, input cleared and re-focused
        │                 (5 consecutive failures invalidate the login session)

        └─ Valid code → Kratos validates, upgrades session to AAL2
                     → Redirect to application callback URL

Backup Codes

Backup codes are generated by Kratos's lookup_secret strategy at the moment of successful TOTP enrollment. They are returned once in the settings flow response and never stored by Hera.

Behavior:

  • Backup codes are displayed immediately after enrollment, with "Download as .txt" and "Copy all" buttons
  • A confirmation checkbox ("I have saved these backup codes") must be checked before the user can proceed, the Continue button is disabled until acknowledged
  • After the user clicks Continue, the codes are cleared from React state
  • Navigating back to settings does NOT re-display the codes, the UI shows "Backup codes generated. Download them now to save." with no way to reveal them again
  • Each backup code is single-use, Kratos invalidates it after use
  • Using a backup code at login follows the same challenge screen (click "Use a backup code instead")

If codes are lost: The user must contact an admin to remove their TOTP credential and re-enroll. There is no self-service recovery path once backup codes are lost and the authenticator app is unavailable.


API Reference

TOTP enrollment and challenge use Kratos self-service flows. Hera calls these endpoints server-side using the user's session cookie.

Enrollment

StepHTTP CallDescription
InitiateGET /self-service/settings/browserCreates a settings flow; returns flow ID and TOTP nodes
Submit codePOST /self-service/settings body: { method: "totp", totp_code: "123456" }Validates the 6-digit code; activates TOTP credential

Login Challenge

StepHTTP CallDescription
Detect AAL2Flow returned from GET /self-service/login/flows?id=<id> includes TOTP nodeHera renders challenge screen
Submit TOTP codePOST /self-service/login body: { totp_code: "123456" }Validates code, upgrades session to AAL2
Submit backup codePOST /self-service/login body: { lookup_secret: "<code>" }Validates backup code, marks it as used, upgrades session

Attempt Limit

Kratos does not natively count TOTP-specific login attempts. Hera enforces a 5-attempt limit using a server-side cookie scoped to the current login flow:

  • Cookie key: totp_attempts_{flow_id}
  • Attributes: httpOnly: true, sameSite: lax, TTL matches Kratos login flow lifespan (10 minutes)
  • After 5 consecutive failures, Hera invalidates the login flow and redirects to the login start page with a message

The maximum attempt count defaults to 5 and is configurable via the TOTP_MAX_ATTEMPTS environment variable (future: migrated to SDK setting mfa.totp_max_attempts). When the SDK setting is present, it takes priority over the environment variable.

Note: The application-layer attempt cookie is a defense-in-depth measure. The primary brute force protection is the reverse proxy (Caddy) rate limiting on the TOTP submission endpoint. The cookie can be cleared by a user with browser DevTools access, this does not bypass the reverse proxy rate limit.


Configuration

Kratos (platform/dev/ciam-kratos/kratos.yml)

TOTP is enabled in the Kratos config with the Olympus issuer name:

selfservice:
  methods:
    totp:
      config:
        issuer: Olympus   # Appears as the account name in authenticator apps
      enabled: true
    lookup_secret:
      enabled: true       # Backup codes, must remain enabled

Issuer name: The issuer value appears as the account label in the user's authenticator app (e.g., "Olympus:alice@example.com"). This is set at enrollment time and embedded in the otpauth:// URI. Changing the issuer in Kratos config does not affect existing enrolled users, their authenticator app continues to show the label from the original enrollment. Only new enrollments pick up the updated issuer.

Clock skew: Kratos accepts TOTP codes from ±30 seconds of the current time window by default (RFC 6238 standard). Do not implement custom clock skew tolerance in Hera.

Environment Variables (Hera)

VariableDefaultDescription
TOTP_MAX_ATTEMPTS5Maximum TOTP attempts before login flow invalidation

Edge Cases

Abandoned enrollment (user closes page before entering verification code)

Kratos's settings flow expires after its configured lifespan (10 minutes). If the user closes the page without completing verification, the pending TOTP enrollment expires automatically. No partial TOTP device is left active. Hera does not need custom cleanup logic.

Expired login flow during TOTP challenge

If the Kratos login flow expires (10-minute TTL) while the user is on the TOTP challenge screen, the next submission returns 410 Gone. Hera detects this and redirects to the login start page with a clear message: "Your login session expired. Please log in again."

Backup code used at login

The "Use a backup code instead" link reveals an additional text input inline, no page reload. The TOTP challenge input is hidden. Submitting a valid backup code completes the AAL2 challenge and invalidates the code. The user is warned to generate new backup codes if they are running low.

QR code rendering failure (JavaScript disabled)

The QR code requires JavaScript for client-side SVG rendering. If JavaScript is disabled, the QR code does not render. The manual entry key is always visible as a fallback, users can enter the secret manually in their authenticator app.


Security Considerations

  • The otpauth:// URI (containing the TOTP secret) is rendered as an inline SVG by a pinned QR library, it is never sent to an external image service.
  • The manual entry key is displayed as a read-only <code> element with autocomplete="off", browsers do not save it in form history.
  • Backup codes are held in React state and never stored in localStorage or sessionStorage. They are cleared from state when the user proceeds past the backup codes screen.
  • Hera does not persist any TOTP enrollment state in the browser beyond the Kratos flow ID (stored in the Kratos-managed session cookie). On page reload during enrollment, a fresh settings flow is initiated.
  • The backup code confirmation gate (checkbox) ensures users acknowledge the codes before proceeding. The Continue button is disabled until checked.
  • Production Kratos config must have log.leak_sensitive_values: false, the dev config enables this flag for debugging but it must never be set in production. TOTP secrets would appear in logs if this flag were enabled in production.

  • platform#13, TOTP config fix: issuer rename from "Kratos" to "Olympus"
  • hera#28, TOTP enrollment UI (QR code + verification)
  • hera#29, TOTP login challenge screen
  • platform#14, MFA policy admin panel (admin toggle for TOTP requirement)

Last updated: 2026-04-01 (Technical Writer, hera#28, hera#29, platform#13)

On this page