Skip to content

feat(vault): interactive TOTP two-factor auth#21

Merged
UnbreakableMJ merged 1 commit into
mainfrom
totp-2fa
Jun 15, 2026
Merged

feat(vault): interactive TOTP two-factor auth#21
UnbreakableMJ merged 1 commit into
mainfrom
totp-2fa

Conversation

@UnbreakableMJ

Copy link
Copy Markdown
Contributor

What

A 2FA-enabled account is no longer a dead end on the password grant. On
Error::TwoFactorRequired, Vault now prompts for the authenticator (TOTP)
code
and resubmits the token grant with it — the non-API-key 2FA path.

How

  • vault-api: login_password gains two_factor: Option<&TwoFactor> and
    appends twoFactorToken/twoFactorProvider/twoFactorRemember. A wrong code
    re-challenges, so callers can re-prompt.
  • vault-ipc: TwoFactorCode + serde-defaulted two_factor on
    Request::Unlock (forward-compatible, like api_key/device_id).
  • vault-agent: threads it into the password grant as provider 0
    (authenticator). The api-key / persisted-key branches ignore it (not
    2FA-challenged).
  • vault-cli: vault login/unlock resubmit on a challenge, reading the
    code from the controlling terminal (/dev/tty) — necessary because
    read_password consumes stdin to EOF (the doc'd pass/gpg piping). --totp
    / $BW_TOTP supply it non-interactively. An empty line aborts.
  • vault-tui: an "Authenticator code" step in the unlock screen — stashes
    the typed password, resubmits with the code; Tab (PIN toggle) is inert
    mid-2FA.

Scope

Authenticator/TOTP provider only. Other providers (email, Duo, WebAuthn) and
"remember this device" are tracked follow-ups. An API key still bypasses 2FA
entirely (docs/apikey.md cross-references both paths).

Tests

  • vault-api: wiremock challenge → coded resubmit → token (up_to_n_times(1)
    on the challenge mock so the retry falls through to success).
  • vault-ipc: Unlock.two_factor transport round-trip + absent-decodes-None.
  • vault-tui: begin_2fa stashes the password and clears the field; the
    follow-up request() carries the stashed password + the code; Tab stays
    inert mid-2FA.

Local CI-exact gates all green: fmt, fresh isolated clippy -D warnings,
cargo test --workspace --all-targets under RUSTFLAGS=-D warnings,
cargo deny check (no new deps), both headless builds.

🤖 Generated with Claude Code

A 2FA-enabled account no longer dead-ends on the password grant: on
TwoFactorRequired, prompt for the authenticator code and resubmit.

- vault-api: login_password gains two_factor: Option<&TwoFactor>; appends
  twoFactorToken/Provider/Remember (a wrong code re-challenges).
- vault-ipc: TwoFactorCode + serde-defaulted two_factor on Request::Unlock.
- vault-agent: threads it into the password grant (provider 0 = authenticator).
- vault-cli: login/unlock resubmit on a challenge, reading the code from the
  controlling terminal (/dev/tty) so it works with the password piped to
  stdin; --totp / $BW_TOTP for non-interactive use.
- vault-tui: an "Authenticator code" unlock step (stash password, resubmit
  with the code; Tab disabled mid-2FA).

Scope: authenticator/TOTP provider only; other providers and "remember this
device" are future work. An API key still bypasses 2FA entirely.

Tests: vault-api wiremock challenge→resubmit; vault-ipc transport round-trip;
vault-tui begin_2fa + request-with-code units.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@UnbreakableMJ
UnbreakableMJ merged commit 21a61df into main Jun 15, 2026
8 checks passed
@UnbreakableMJ
UnbreakableMJ deleted the totp-2fa branch June 15, 2026 19:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant