2026

Passkey security overhaul: step-up, recovery, login history, USD session caps

Backend v1.3.0 ships action-bound step-up for high-risk operations, a recovery flow, login history with first-passkey risk holds, and USD-denominated session caps.

backendv1.3.0

What changed

High-risk actions now require a fresh passkey assertion bound to the exact operation. wallet.send, agent_session.approve, passkey.register.additional, and passkey.remove each have a /stepup preflight that issues an HMAC token committing to action type, target resource, and a canonical parameter hash; the WebAuthn challenge is set to SHA-256(token) and a persisted step-up session prevents replay. Each enforcement point has a Disable*StepUp config flag for operator rollback, and all four step-ups emit activity events that drive a new GET /v1/security/stepup-log audit endpoint (filterable by all, agent_approve, wallet, passkey).

Agent session caps are now USD-denominated. The Assets token allowlist is gone from session delegation — the agent never names a settlement token. Sessions carry a server-stamped Currency field (default USD), and the merchant chooses the settlement stablecoin at transaction time. Tool params and swagger have been reworded to USD/budget language; CLI and skill USD changes follow separately.

Account recovery for the lost-passkey case ships behind POST /v1/recovery/{start,cancel,complete} and GET /v1/recovery/status. start flips the account to recovery_pending (which GetUserPasskeyReadiness exposes, naturally blocking high-risk actions), complete enforces a configurable delay (72h prod default, 10m in nonprod) and atomically revokes credentials plus other auth sessions in a single transaction. The recovery-started email carries a one-shot HMAC cancel link bound to the user and RecoveryStartedAt; clicking it lands on a confirmation page and cancellation happens via explicit POST so link previews and prefetch can't silently cancel. All five lifecycle events (passkey added/disabled, cooldown started, recovery started/completed) now render through a shared branded HTML template with a plain-text fallback.

A login_events table now records every /v1/login/verify attempt with derived device, browser, and MaxMind-resolved city/country (IP is not stored). Users can read their own history via GET /v1/security/login-log with since/until/limit/offset and a 90-day default. On top of this, first-passkey enrollment now evaluates risk: when the registering session's device, browser, and country are all unseen versus the user's prior successful logins, enrollment is held (403 ErrPasskeyEnrollmentRiskHold at POST /v1/passkey/register/options, before WebAuthn options are returned) and a security email is sent. The hold lifts on a familiar re-login or after the cooldown window. GET /v1/passkey lists a user's credentials with a richer display name (Platform passkey (synced, built-in) @ Mac · Chrome), and POST /v1/passkey/delete soft-disables credentials so they stay in the audit trail but no longer count toward readiness. GET /v1/me now surfaces passkey_readiness so clients can drive gating from a single call. Both login_events and first_passkey_risk are gated by config flags and ship enabled in base.yaml; the GeoLite2-City database is bundled into the Docker image.

Included changes:

  • refactor(agent): make session caps USD-denominated, drop token allowlist
  • chore(config): shorten recovery & first-passkey-risk cooldowns to 10m in nonprod
  • fix: address security review findings
  • feat(passkey): richer display name with humanized transports
  • docs(passkey): align login-history web surface with implemented columns
  • docs(recovery): align cancel-email subject with confirm flow + document GET landing
  • fix(recovery): require explicit POST to cancel recovery via email link
  • docs(passkey): correct stale step-up challenge comment and risk-hold endpoint
  • feat(passkey): derive and expose passkey display metadata
  • fix(recovery): accept cancel links without started_at
  • feat(security): brand security notification emails with shared HTML template
  • fix(passkey): use singular login_event table name in partial index migration
  • fix(passkey): review nits — empty cancel-URL fallback, doc comment, docs
  • fix(passkey): run first-passkey risk gate before the step-up check
  • fix(passkey): bind first-passkey risk to the exact session + gate at begin
  • docs(passkey): align remaining design docs with implemented risk model
  • feat(passkey): implement first-passkey enrollment risk hold + enable flags
  • fix(recovery): claim recovery atomically before destructive revoke
  • login_events: fix dev startup - default tag must quote the literal, make migration self-healing
  • config: add stepup + recovery default stanzas to base.yaml
  • login_events: review followups - close geoip on stop, partial email index, rune-safe truncate
  • login_events: P1/P2 review fixes (defaults, EOF whitespace, evaluator ctx)
  • go mod tidy: promote useragent + geoip2 to direct deps
  • login_events: schema + ingestion + GET /v1/security/login-log + first-passkey-risk flag
  • docs(passkey): add login-history + first-passkey-risk design + bundle GeoLite2
  • passkey: soft-disable credentials on delete + load exclusions from DB
  • swagger: fix /mcp empty-responses + enum-tag ActivityEvent fields
  • address Round 12 review findings on passkey-improve
  • address 8 review findings on passkey-improve
  • release agent_session request claim on post-claim error
  • fix two findings: premature approved status, swagger drift
  • fix three race + state-persistence findings from 2026-05-26 review
  • fix three verified bugs from review 2026-05-26
  • add DisableAgentSessionApproveStepUp config flag
  • send email notifications on passkey lifecycle + recovery, with cancel link
  • backend fixes from passkey-improve review (H1-3, M1, M5, L2-3)
  • bind passkey.register.additional and passkey.remove to WebAuthn
  • bind wallet.send to a persisted WebAuthn step-up session
  • enable wallet.send and additional-passkey step-up by default
  • add GET /v1/security/stepup-log for the Security page audit panel
  • add recovery flow for lost-passkey + email-in-hand case
  • add POST /v1/passkey/delete with passkey.remove step-up
  • persist passkey cooldown state on the user and consult it in readiness
  • require step-up to add another passkey when one already exists
  • add wallet.send step-up token flow
  • bind agent_session.approve to action-bound step-up token
  • add GET /v1/passkey to list user's registered passkeys
  • add step-up activity kinds and recordStepUpEvent helper
  • add pkg/stepup for action-bound HMAC tokens
  • add passkey trust state and account readiness computation
  • add prod design doc for passkey-improvement
  • refine syntax
  • add thread-model for current email-otp + passkey design
  • refine Account States
  • add passkey-based security evolution plan

On this page