"""MFA (TOTP) client for integration tests.""" import logging logger = logging.getLogger(__name__) class MfaClient: """Wraps TOTP MFA-related API calls.""" def __init__(self, client): self._client = client # ------------------------------------------------------------------ # TOTP Enrollment # ------------------------------------------------------------------ def enroll_totp(self) -> dict: """Begin TOTP enrollment. Returns: Response dict containing ``secret``, ``provisioning_uri``, ``qr_code``, and ``backup_codes``. """ logger.info("[MfaClient] Enrolling TOTP") return self._client.post("/auth/totp/enroll") def verify_enrollment(self, code: str, client_timestamp: str | None = None) -> dict: """Complete TOTP enrollment by verifying the first code. Args: code: 6-digit TOTP code generated from the secret. client_timestamp: Optional ISO-8601 timestamp for drift calc. """ payload = {"code": code} if client_timestamp: payload["client_timestamp"] = client_timestamp logger.info("[MfaClient] Verifying TOTP enrollment") return self._client.post("/auth/totp/verify-enrollment", data=payload) # ------------------------------------------------------------------ # TOTP Verification (during login) # ------------------------------------------------------------------ def verify_totp(self, code: str, is_backup_code: bool = False, client_timestamp: str | None = None) -> dict: """Verify TOTP code during the multi-step login flow. This is called AFTER ``AuthClient.login`` returns ``requires_totp=True`` and stores the pending user id in the server-side session. Args: code: 6-digit TOTP code or backup code. is_backup_code: True if ``code`` is a backup code. client_timestamp: Optional ISO-8601 timestamp. Returns: Response dict containing ``user``, ``token``, ``expires_at``. """ payload = {"code": code, "is_backup_code": is_backup_code} if client_timestamp: payload["client_timestamp"] = client_timestamp logger.info(f"[MfaClient] Verifying TOTP — backup={is_backup_code}") result = self._client.post("/auth/totp/verify", data=payload) token = result.get("data", {}).get("token") if token: self._client.set_token(token) logger.info("[MfaClient] TOTP verification successful — token stored") return result # ------------------------------------------------------------------ # TOTP Management # ------------------------------------------------------------------ def get_totp_status(self) -> dict: """Return current TOTP status and remaining backup codes.""" return self._client.get("/auth/totp/status") def disable_totp(self, password: str) -> dict: """Disable TOTP for the current user. Args: password: Current account password (required for confirmation). """ return self._client.delete("/auth/totp/disable", data={"password": password}) def regenerate_backup_codes(self, password: str) -> dict: """Generate a fresh set of backup codes. Args: password: Current account password (required for confirmation). Returns: Response dict containing ``backup_codes``. """ return self._client.post( "/auth/totp/regenerate-backup-codes", data={"password": password}, )