Skip to content

fix(users): prepare-device-replacement needs step-up and identity proofing #245

Open

Description

@Bccorb

Summary

seamless users prepare-device-replacement <id> cannot succeed against any current auth API. POST /admin/users/:userId/recovery/device-replacement has two requirements the CLI does not meet:

  1. Fresh step-up. The route runs requireAdmin('write') and requireStepUp() (src/routes/admin.routes.ts in seamless-auth-api). Without a step-up on the session in the last 5 minutes (DEFAULT_STEP_UP_MAX_AGE_SECONDS) it answers 403 step_up_required. The CLI has no step-up support, and prepareDeviceReplacement (src/core/admin.ts) maps that 403 to "requires an elevated step-up admin session, which the CLI cannot establish yet. Use the dashboard for this action."
  2. Identity proofing. Since @seamless-auth/types 0.9.0 (fells-code/seamless-auth-types, feat(admin): require identity proofing on device replacement recovery), DeviceReplacementRecoverySchema requires a proofing object, and the controller rejects a body without it with 400 Invalid recovery payload. The CLI sends only revokeSessions, removePasskeys and disableTotp, so it would fail even with step-up in place.

The proofing contract, from RecoveryProofingSchema:

  • method: in_person or remote_exception
  • evidenceRef: 1 to 200 characters, a pointer such as a ticket or case number, not the evidence itself (it is written to the audit trail)
  • approver: 1 to 200 characters, required when method is remote_exception

Found while moving the admin module onto the shared schemas (#144, #244). That PR keeps DeviceReplacementOptions as a local type so this gap stays visible.

Proposal

  • Step-up over TOTP. Before the recovery call, prompt for the admin's TOTP code and POST /totp/verify-mfa on the same session, then retry. WebAuthn step-up (/step-up/webauthn/*) needs a browser ceremony, so it stays dashboard-only. If the admin has no TOTP enrolled, keep pointing at the dashboard. Accept the code from a flag or stdin as well as the prompt, so the command still works under requireInteractive.
  • Proofing flags.
    • --proofing in_person|remote_exception and --evidence <ref>, both required
    • --approver <name> when the method is remote_exception
    • Validate locally with RecoveryProofingSchema, so a missing approver is named before any request
    • Prompt for missing values interactively; under --yes / non-TTY, fail naming the flag
  • Type the request with DeviceReplacementRecoveryRequest from @seamless-auth/types in place of the local DeviceReplacementOptions.
  • Name the proofing method and evidence reference in the confirmation prompt, since they go on the audit trail.

Done when

  • prepare-device-replacement completes against a current instance for an admin with TOTP enrolled, and prints the revoked session, removed credential and disabled TOTP counts.
  • A remote_exception without --approver fails locally, naming the flag.
  • An admin without TOTP gets a clear pointer to the dashboard rather than a raw 403.
  • Tests cover the step-up retry, the proofing validation, and the non-interactive path.

Activity

  1. added
    P2Soon
    safeServes the safe promise: no lockout, no lock-in, honest
    v0.20Targets the v0.20 release
    on Oct 17, 2026
  2. added this to the v0.20 milestone on Oct 17, 2026
  3. Bccorb commented on Oct 17, 2026

    @Bccorb
    ContributorAuthor

    Milestoned v0.20 with recovery (2026-10-13). The interim behaviour is already in place: the step-up 403 is mapped to a message pointing at the dashboard, so the command fails clearly rather than with a raw error.

  4. Bccorb commented on Oct 22, 2026

    @Bccorb
    ContributorAuthor

    Widened by fells-code/seamless-auth-api#490 (#437): every admin write now needs a fresh step-up, not only prepare-device-replacement. Affected commands: users delete, all org and member writes, migrate (POST /admin/users/import, which also needs a fresh step-up whenever the 5-minute window lapses mid-run, since it sends 200-row batches), system config patch, and OAuth provider create/update/delete. call() in src/core/admin.ts maps any 403 to a generic PermissionError; step_up_required needs its own message and the TOTP step-up flow (POST /totp/verify-mfa on the same session). Admins without TOTP cannot make these writes from the CLI until a passkey step-up path exists for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Soonarea:cliSeamless CLIbugSomething isn't workingsafeServes the safe promise: no lockout, no lock-in, honestv0.20Targets the v0.20 release

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions