Fix SEC-01: require password for TOTP enroll

- REST handleTOTPEnroll now requires password field in request body
- gRPC EnrollTOTP updated with password field in proto message
- Both handlers check lockout status and record failures on bad password
- Updated Go, Python, and Rust client libraries to pass password
- Updated OpenAPI specs with new requestBody schema
- Added TestTOTPEnrollRequiresPassword with no-password, wrong-password,
  and correct-password sub-tests

Security: TOTP enrollment now requires the current password to prevent
session-theft escalation to persistent account takeover. Lockout and
failure recording use the same Argon2id constant-time path as login.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 00:48:31 -07:00
parent 586d4e3355
commit 8545473703
13 changed files with 192 additions and 17 deletions

View File

@@ -148,11 +148,15 @@ class Client:
expires_at = str(data["expires_at"])
self.token = token
return token, expires_at
def enroll_totp(self) -> tuple[str, str]:
def enroll_totp(self, password: str) -> tuple[str, str]:
"""POST /v1/auth/totp/enroll — begin TOTP enrollment.
Security (SEC-01): current password is required to prevent session-theft
escalation to persistent account takeover.
Returns (secret, otpauth_uri). The secret is shown only once.
"""
data = self._request("POST", "/v1/auth/totp/enroll")
data = self._request("POST", "/v1/auth/totp/enroll", json={"password": password})
assert data is not None
return str(data["secret"]), str(data["otpauth_uri"])
def confirm_totp(self, code: str) -> None: