Skip to content

feat(cli): bgagent change-password + force-rotate on first login (#238) - #680

Open
scottschreckengaust wants to merge 1 commit into
mainfrom
feat/issue-238-change-password
Open

feat(cli): bgagent change-password + force-rotate on first login (#238)#680
scottschreckengaust wants to merge 1 commit into
mainfrom
feat/issue-238-change-password

Conversation

@scottschreckengaust

@scottschreckengaust scottschreckengaust commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Two related additions to the bgagent CLI auth flow (see the Closes line below):

  1. bgagent change-password — a new interactive command that lets any logged-in user rotate their own Cognito password.
  2. Force-rotate on first login — invites now issue a temporary password, and bgagent login handles Cognito's NEW_PASSWORD_REQUIRED challenge so the teammate sets a permanent password only they know on first login.

Closes #238

Root cause / context

  • login had no challenge handling. cli/src/auth.ts login only issued an InitiateAuthCommand and expected AuthenticationResult directly — any Cognito challenge (like NEW_PASSWORD_REQUIRED) fell through to the "Unexpected authentication response" error.
  • Invites set a PERMANENT password. adminInviteUser (cli/src/cognito-admin.ts) called adminSetPermanentPasswordAdminSetUserPasswordCommand({ Permanent: true }) (the Permanent: true lived at cli/src/cognito-admin.ts:151). So the admin-generated string the admin shared over Slack/email stayed a valid credential forever, and there was no change-password command to rotate it.

The fix

Part 1 — change-password (cli/src/commands/change-password.ts, registered in cli/src/bin/bgagent.ts):

  • Prompts for the current password, then the new password twice (masked, via the existing prompt-secret helper).
  • auth.ts::changePassword() re-authenticates with the current password to obtain a short-lived, in-memory access token. ChangePasswordCommand requires an AccessToken, which the CLI deliberately does not persist (only the ID + refresh tokens the REST authorizer needs) — re-auth avoids widening the on-disk credential surface and doubles as verification of the current password (wrong one → NotAuthorizedException → "Current password is incorrect." before any change is attempted). Cognito enforces the password policy server-side; a weak new password surfaces as InvalidPasswordException.

Part 2 — force-rotate on first login:

  • auth.ts::login() now branches on result.ChallengeName === NEW_PASSWORD_REQUIRED: it prompts for a new password (via a caller-supplied callback, keeping TTY concerns in the command layer), answers with RespondToAuthChallengeCommand, and persists the resulting tokens. A non-interactive login (no prompt callback, e.g. --password passed) surfaces a clear error instead of hanging.
  • adminInviteUser drops the permanent-password step — adminCreateUser already sets TemporaryPassword, which lands the user in FORCE_CHANGE_PASSWORD. That's exactly the state that triggers the first-login challenge. admin reset-password keeps its permanent-password path unchanged (admin account recovery is out of scope).

Why the SDK commands are the right call: ChangePasswordCommand and RespondToAuthChallengeCommand are the AWS SDK's first-class primitives for exactly these flows — no hand-rolled crypto, password hashing, SRP, or token storage. @aws-sdk/client-cognito-identity-provider is already a CLI dependency (no new dependency introduced).

Testing

MISE_EXPERIMENTAL=1 mise //cli:build657 tests pass, compile clean, eslint clean; change-password.ts at 100% coverage.

New/updated cases:

  • change-password success — prompts current + new (twice), calls changePassword, reports success; access token never written to disk.
  • wrong current password — surfaces "Current password is incorrect." and never calls ChangePassword.
  • weak new password — surfaces Cognito's InvalidPasswordException message.
  • no active session — clear "Not authenticated" error, no Cognito call.
  • new-password confirmation mismatch / empty — rejected client-side before any Cognito call.
  • login NEW_PASSWORD_REQUIRED — prompts, responds with the challenged USERNAME/NEW_PASSWORD + echoed Session, persists tokens; non-interactive path errors clearly; policy rejection surfaced.
  • inviteadminInviteUser now issues exactly one AdminCreateUser with TemporaryPassword and no Permanent flag (regression guard); UsernameExistsException still maps to a CliError.

Security note

  • No secrets logged. No password or token is ever passed to console.* or debug() — only status strings and non-secret config fields (region, client_id, user_pool_id).
  • Temporary-password rotation is the security win: the admin-shared credential (terminal scrollback / Slack message) stops being valid once the teammate logs in and sets their own password.
  • The ChangePassword access token is minted in memory and discarded — never persisted to ~/.bgagent/credentials.json.
  • SAST (semgrep auto + silent-success-masking) and gitleaks are clean on this diff (0 findings on the changed files; gitleaks git origin/main..HEAD → no leaks). The 3 gitleaks findings on mise run security:secrets are a pre-existing full-history sweep of the shared store, not in this diff.

Dependencies / related

Self-review notes

  • Inline /review_pr self-review complete (security/auth, silent-failures, test coverage, scope): approve-with-nits, zero code blocking. Fixed one blocking finding — the Closes #238 line was previously backticked and did not auto-close; it is now unformatted (closingIssuesReferences resolves to [238]).
  • Deferred (non-blocking): the workflow-SHA lines in the raw git diff origin/main..HEAD are a stale-branch artifact — this branch is 1 commit behind origin/main, which bumped several pinned action SHAs after the branch was cut. HEAD (f4737a6f) touches no .github/workflows/ files; a merge/rebase of main at integration time resolves them automatically.

🤖 Generated with Claude Code

Part 1 — `bgagent change-password`:
- New `cli/src/commands/change-password.ts`, registered in bin/bgagent.ts.
- Interactive: prompts current password + new password (twice, confirmed).
- `auth.ts changePassword()` re-authenticates with the current password to
  mint an in-memory access token (ChangePassword requires an AccessToken,
  which the CLI never persists) then calls `ChangePasswordCommand`. Wrong
  current password → clear error before any change; weak new password →
  Cognito's `InvalidPasswordException` surfaced verbatim.

Part 2 — force-rotate on first login:
- `login()` now handles the `NEW_PASSWORD_REQUIRED` challenge: prompts for a
  new password, answers via `RespondToAuthChallengeCommand`, persists tokens.
  A non-interactive login (no prompt) surfaces a clear error instead of hanging.
- Invites now issue a TEMPORARY password: `adminInviteUser` drops the
  `AdminSetUserPassword` (Permanent: true) call, leaving the user in
  FORCE_CHANGE_PASSWORD so the admin-shared credential stops being valid once
  the teammate logs in once. `admin reset-password` keeps its permanent-password
  path unchanged.

Docs: USER_GUIDE + LINEAR_SETUP_GUIDE teammate-onboarding sections updated for
the temp-password first-login flow; Starlight mirrors regenerated.

Uses the AWS SDK's Cognito commands throughout — no hand-rolled crypto, token
storage, or SRP. No passwords or tokens are logged.

Closes #238

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@scottschreckengaust

Copy link
Copy Markdown
Contributor Author

✅ Acceptance summary (for the reviewer)

#238bgagent change-password + force-rotate on first login.

  • New change-password command: re-auths with the current password to mint an in-memory access token for ChangePasswordCommand (verifies the current password AND avoids persisting an access token).
  • Force-rotate: login() now branches on NEW_PASSWORD_REQUIREDRespondToAuthChallengeCommand and persists tokens; adminInviteUser no longer promotes the temporary password to permanent, so invites land in FORCE_CHANGE_PASSWORD and first login forces a rotation.
  • Security-reviewed inline: no secrets logged, SDK primitives only (no hand-rolled crypto/SRP/token storage), all Cognito errors surfaced (wrong-current, weak-new, no-session, non-interactive challenge). change-password.ts at 100% coverage; 657 tests pass.

/review_pr = approve-with-nits, zero blocking. Out of scope (future): forgot-password, MFA, admin-side reset.

🔀 Merge guidance (for the reviewer)

Cluster ua-broad — must-follow #345. Shares cli/src/bin/bgagent.ts with #345 (~2-line command-registration conflict expected if #345 merges first). Recommend merging #345 first; this rebases trivially. Not strict-up-to-date-gated. Native auto-merge disabled repo-wide; awaits your manual squash-merge.

🤖 orchestrator note (agent) — promotion is orchestrator-driven; merge remains a human action.

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.

feat(cli): bgagent change-password command + force-rotate on first login

1 participant