Skip to content

Two Factor Auth

Sia edited this page May 31, 2026 · 3 revisions

Two-Factor Authentication (TOTP)

RFC 6238 implementation with zero external dependencies — only javax.crypto.Mac (HmacSHA1) + a tiny Base32 encoder. Compatible with Google Authenticator, 1Password, Authy, etc.

Enable at /2fa (left nav: "2단계 인증").

Why TOTP

  • Password-only login was OK for a LAN-only single-operator tool, but vibe-coder-server is now occasionally exposed over SSH tunnels and reverse proxies. A second factor cuts the blast radius of a leaked password.
  • TOTP is offline (no SMS / push provider), free, and supported by every major authenticator app.
  • Implementing RFC 6238 from scratch (about 150 LOC including Base32) was cheaper than adding a com.warrenstrange:googleauth or dev.samstevens.totp dependency.

Enabling 2FA

  1. Sign in normally → click 2단계 인증 in the left navigation.
  2. The page shows your otpauth://… URI plus the Base32 secret in 4-character groups. Either:
    • Convert the URI to a QR code (qrencode -t ANSI '<uri>' in a terminal, or a browser-based QR generator) and scan with Authenticator, or
    • Tap "Enter a setup key" in Authenticator and paste the Base32 secret.
  3. Authenticator shows a 6-digit code that rotates every 30 s. Enter the current code in the form and submit.
  4. The server verifies the code (window ±1 to absorb clock drift) and stores the secret in admin_users.totp_secret. The setup page now shows "✓ 현재 활성" with the enablement timestamp.

Login flow with 2FA enabled

  1. POST /api/auth/login (or the SSR form) with {username, password}.
  2. Server returns 401 totp_required (audited as such — not counted as a failed login).
  3. Client prompts for the 6-digit code, resubmits with totpCode field added. SSR keeps the username + password in hidden inputs for round 2.
  4. Server verifies the code; on success issues the token + cookie as usual. On failure returns 401 invalid_totp (counted against the account / IP brute-force limits).

LoginRequestDto.totpCode defaults to null so clients continue to work for accounts without 2FA. Clients should handle totp_required to surface a second-step UI.

Disabling 2FA

/2fa shows a destructive form requiring a current code. On submit the totp_secret row is cleared. Future logins skip step 2.

If you lose access to the Authenticator (phone wiped, app deleted), the only recovery is to:

  1. Connect to the host with shell access.
  2. docker exec -it vibe-coder-postgres psql -U vibecoder vibecoder
  3. UPDATE admin_users SET totp_secret = NULL, totp_enabled_at = NULL WHERE username = 'admin';

Single-admin tool, no recovery codes / SMS backup. Treat the secret like a keystore.

Threat model

Attack Mitigation
Password leak TOTP code required as second factor
TOTP secret leak Treat as critical — manual UPDATE to rotate; investigate audit log
Clock drift verify() accepts ±1 slot (±30 s)
Replay of a code within the same slot RFC 6238 doesn't prevent this; rate limit + 30 s slot makes practical replay unlikely
Brute force the 6-digit code Combined with existing brute-force limits (10 fails/account/15 min, 30 fails/IP/24 h)

Audit trail

Action Logged as
Enable auth.2fa.enable
Disable auth.2fa.disable
Successful login with 2FA auth.login OK
Wrong TOTP code auth.login FAIL (reason=invalid_totp)
Missing TOTP code (intentional — totp_required is a normal step, not a failure)

Related methods

  • WebAuthn / passkey — see WebAuthn (Passkey). Both methods are available concurrently; the login page exposes a "🔑 Passkey 로 로그인" button next to the password form.

Clone this wiki locally