Skip to content

feat(vault): Bitwarden personal API-key auth (2FA accounts)#14

Merged
UnbreakableMJ merged 1 commit into
mainfrom
apikey-auth
Jun 15, 2026
Merged

feat(vault): Bitwarden personal API-key auth (2FA accounts)#14
UnbreakableMJ merged 1 commit into
mainfrom
apikey-auth

Conversation

@UnbreakableMJ

Copy link
Copy Markdown
Contributor

What

Adds a Bitwarden personal API-key authentication path (OAuth2
client_credentials grant), so an account with two-factor auth enabled can
finally log in — that grant is not 2FA-challenged, and Vault has no interactive
TOTP entry, so the password grant was a dead end for such accounts.

The API key authenticates the session only. The Key it returns is still
wrapped under the stretched master key, so the master password is still
required at every unlock
to decrypt the vault. The API key just gets you past
2FA.

How

  • vault login --api-key — reads $BW_CLIENTID / $BW_CLIENTSECRET (like
    the official bw CLI), else prompts, then reads the master password. On
    success the agent authenticates via client_credentials and persists the
    key
    (apikey.json, 0600, in the account data dir) so plain vault unlock
    and the TUI unlock auto-reuse it — no further 2FA, no re-supplying creds.
  • vault apikey status / vault apikey forget — status echoes only the
    non-secret client_id; forget reverts to the password grant.
  • vault-api: BitwardenClient::login_api_key.
  • vault-store: ApiKeyCreds + save/load/delete_apikey_to_dir
    (atomic, 0600, custom Debug that redacts the secret).
  • vault-ipc: optional serde-defaulted api_key on Request::Unlock
    (forward-compatible) + ApiKeyStatus/ApiKeyForget verbs + ApiKeyStatus
    response (never carries the secret).
  • vault-agent: grant selection (request creds → persisted key → password)
    with persistence on enrollment; ensure_online falls back to API-key re-auth
    so a PIN/offline session of an API-key account can still go online for
    sync/edits even when the grant issued no refresh token.

Security note

The key is a sibling 0600 file, not in the encrypted cache envelope: it
must be readable before unlock (it's used during auth), so it can't be wrapped
under the user key — the same trust level as the stored refresh token / an SSH
private key.

Tests

  • vault-api wiremock: the client_credentials form + a bad-key ServerStatus
    (#[ignore]d like the rest of login_sync.rs — ring test-link; CI runs them).
  • vault-store: round-trip / delete / NotFound / 0600-mode / Debug-redaction.
  • vault-ipc: transport round-trips (api_key present + absent-decodes-to-None,
    ApiKeyStatus).
  • Live end-to-end recipe for a real 2FA account in docs/apikey.md.

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

🤖 Generated with Claude Code

Add a `client_credentials`-grant auth path so a 2FA-enabled account can
authenticate without an interactive TOTP prompt (Vault has none). The API
key authenticates the session only; the master password is still required
to decrypt the vault.

- vault-api: BitwardenClient::login_api_key (grant_type=client_credentials,
  scope=api); new Error::Credential for a non-UTF-8 secret.
- vault-store: ApiKeyCreds + save/load/delete_apikey_to_dir (atomic, 0600,
  Debug redacts the secret).
- vault-ipc: optional serde-defaulted api_key on Request::Unlock (forward-
  compat) + ApiKeyStatus/ApiKeyForget verbs + ApiKeyStatus response (never
  carries the secret).
- vault-agent: grant selection (request creds -> persisted key -> password)
  with persistence on enrollment; ensure_online falls back to API-key
  re-auth so a PIN/offline session can still go online.
- vault-cli: `vault login --api-key` (BW_CLIENTID/BW_CLIENTSECRET or prompt,
  master password still read); `vault apikey status/forget`.
- Docs: README section, docs/apikey.md live recipe, CHANGELOG, CLAUDE.md.

The key is stored as a sibling 0600 file (apikey.json), not in the
encrypted envelope: it must be readable before unlock, so it's FS-perms-
protected like the refresh token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@UnbreakableMJ
UnbreakableMJ merged commit 1f15447 into main Jun 15, 2026
8 checks passed
@UnbreakableMJ
UnbreakableMJ deleted the apikey-auth branch June 15, 2026 13:48
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