You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
TOTP-Based Two-Factor Authentication (Additive Second Factor) #545
Authentication is entirely wallet-signature-based (src/controllers/auth-controller.ts: challenge/verify against a Stellar keypair). That's a strong primary factor, but it's a single factor — anyone who can produce a valid signature (a compromised device, a phished signing request) gets a full session with no second check. There's a formatOtpMessage/extractOtpCode pair in src/whatsapp/handler.ts, but that's a one-time code for linking a WhatsApp number to an account, not a login-time second factor. This issue adds optional TOTP-based two-factor authentication as an additive layer on top of the existing wallet-signature auth, for users who want it.
Current State
src/controllers/auth-controller.ts — challenge/verify/refresh/logout; session issuance on a valid signature alone.
src/whatsapp/handler.ts — OTP for account linking (a different, narrower mechanism — worth reusing the pattern of short-lived code generation/verification, not the code itself).
No TOTP secret field or verification step exists anywhere in the auth path (confirmed).
Proposed Solution
TotpCredential: userId, secretEncrypted (encrypted at rest, reusing whatever encryption-key pattern src/keys/registry.ts already establishes for sensitive secrets), verifiedAt, recoveryCodesHashed[] (one-time backup codes, hashed, for when the authenticator device is unavailable).
Enrollment: POST /api/v1/2fa/enroll generates a secret + QR-code payload (standard otpauth:// URI), POST /api/v1/2fa/verify-enrollment confirms with a code from the user's authenticator app before it becomes active — never enabled from just generating a secret.
Login flow: when a user has an active TotpCredential, verify (post wallet-signature check) returns a requiresTotp challenge instead of a session; POST /api/v1/auth/2fa/verify with a valid code completes the login. Wallet signature remains factor one; TOTP is factor two, additive.
Recovery: one-time backup codes issued at enrollment (shown once, hashed at rest), each usable exactly once; regenerating the set invalidates all previous codes.
POST /api/v1/2fa/disable requires a fresh wallet-signature challenge (not just an active session) — disabling 2FA is a security downgrade and should require the same proof-of-control as enabling it.
Edge Cases & Failure Modes
User loses authenticator device and backup codes: falls to the guardian-based social recovery flow (Guardian-Based Social Recovery for Account Access #535) if that lands, or a manual, clearly-logged admin-assisted recovery path with strong identity re-verification — documented as a real, acknowledged friction point (2FA that can always be silently bypassed isn't 2FA).
Clock skew on TOTP verification: standard ±1 time-step tolerance window, not wider (wider windows weaken the guarantee).
Code replay: a used code (even if still within its time window) is rejected on reuse — track the last-accepted step per credential.
Enrollment abandoned mid-flow (secret generated, never verified): unverified secrets expire and are never treated as active — no zombie unverified-but-somehow-enforced 2FA.
Backup code reuse: rejected, same as TOTP replay.
Security & Privacy Considerations
This is a security-hardening feature by nature — the main risk to guard against is the feature itself becoming a lockout vector (hence the explicit recovery path requirements) or a downgrade vector (hence requiring fresh proof-of-control to disable, not just an active session which could itself be the compromised asset).
secretEncrypted and backup codes are never returned after initial issuance; no plaintext secret logging.
Fresh-signature-required disable flow; audit logging + security notifications; docs/SESSIONS.md or a new docs/2FA.md + docs/openapi.yaml.
Acceptance Criteria
A user can enroll TOTP 2FA, confirmed by a verification code before activation; the secret is encrypted at rest and never re-exposed
An active TotpCredential requires a valid TOTP code as a second factor after wallet-signature verification, with standard ±1 step clock tolerance and replay protection
One-time hashed backup codes are issued at enrollment, each usable once, regenerable (invalidating the prior set)
Disabling 2FA requires a fresh wallet-signature challenge, not just an active session
Enrollment, verification, and disable events are audit-logged and trigger security notifications
An abandoned/unverified enrollment never becomes active or enforced
docs/2FA.md + docs/openapi.yaml added; unit + integration tests green
Problem Statement
Authentication is entirely wallet-signature-based (
src/controllers/auth-controller.ts: challenge/verify against a Stellar keypair). That's a strong primary factor, but it's a single factor — anyone who can produce a valid signature (a compromised device, a phished signing request) gets a full session with no second check. There's aformatOtpMessage/extractOtpCodepair insrc/whatsapp/handler.ts, but that's a one-time code for linking a WhatsApp number to an account, not a login-time second factor. This issue adds optional TOTP-based two-factor authentication as an additive layer on top of the existing wallet-signature auth, for users who want it.Current State
src/controllers/auth-controller.ts—challenge/verify/refresh/logout; session issuance on a valid signature alone.src/routes/sessions.ts(Session & Device Management API (List, Name, Revoke) #376) — device/session management, the natural place to also manage 2FA enrollment.src/whatsapp/handler.ts— OTP for account linking (a different, narrower mechanism — worth reusing the pattern of short-lived code generation/verification, not the code itself).Proposed Solution
TotpCredential:userId,secretEncrypted(encrypted at rest, reusing whatever encryption-key patternsrc/keys/registry.tsalready establishes for sensitive secrets),verifiedAt,recoveryCodesHashed[](one-time backup codes, hashed, for when the authenticator device is unavailable).POST /api/v1/2fa/enrollgenerates a secret + QR-code payload (standardotpauth://URI),POST /api/v1/2fa/verify-enrollmentconfirms with a code from the user's authenticator app before it becomes active — never enabled from just generating a secret.TotpCredential,verify(post wallet-signature check) returns arequiresTotpchallenge instead of a session;POST /api/v1/auth/2fa/verifywith a valid code completes the login. Wallet signature remains factor one; TOTP is factor two, additive.POST /api/v1/2fa/disablerequires a fresh wallet-signature challenge (not just an active session) — disabling 2FA is a security downgrade and should require the same proof-of-control as enabling it.Edge Cases & Failure Modes
Security & Privacy Considerations
secretEncryptedand backup codes are never returned after initial issuance; no plaintext secret logging.Out of Scope
Suggested Implementation Plan
TotpCredentialmodel (encrypted secret, hashed backup codes) + migration/rollback.requiresTotpchallenge step after wallet-signature verification.docs/SESSIONS.mdor a newdocs/2FA.md+docs/openapi.yaml.Acceptance Criteria
TotpCredentialrequires a valid TOTP code as a second factor after wallet-signature verification, with standard ±1 step clock tolerance and replay protectiondocs/2FA.md+docs/openapi.yamladded; unit + integration tests green