Skip to content

feat(experience,phrases): add pinned password and primary code step-up screens - #9595

Draft
wangsijie wants to merge 10 commits into
wangsijie-log-14120-add-the-step-up-spa-route-tree-with-method-selection-guardfrom
wangsijie-log-14115-implement-pinned-password-and-primary-email-phone-step-up
Draft

feat(experience,phrases): add pinned password and primary code step-up screens#9595
wangsijie wants to merge 10 commits into
wangsijie-log-14120-add-the-step-up-spa-route-tree-with-method-selection-guardfrom
wangsijie-log-14115-implement-pinned-password-and-primary-email-phone-step-up

Conversation

@wangsijie

@wangsijie wangsijie commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #9589 (the /step-up route tree), which is stacked on #9582 (submitStepUp). Implements LOG-14115: the pinned-user first-factor pages of the step-up route tree, the two screens #9589 left navigating to a 404.

API

apis/experience/step-up.ts gains the two verification calls, next to the sendStepUpVerificationCode() it already had:

  • verifyStepUpPassword(password)POST /verification/password { password }. No identifier, raw or masked, rides along: Core verifies against the pinned subject's credential.
  • verifyStepUpVerificationCode({ type, code, verificationId })POST /verification/verification-code/verify { identifier: { type }, code, verificationId }. As when sending, only the type travels; Core reads the value it challenged from the verification record.

Both complete through the existing identifyAndSubmitInteraction({ verificationId }), so the identification phase's public contract is unchanged and the existing conflict guard rejects a record that resolves to another account.

Screens

  • /step-up/password renders a password form that is the password field of the sign-in form and nothing else: no identifier to show or switch, and no forgot-password link, since the mode rejects the event switch it would start. There is no captcha either — nothing on this page creates an interaction.
  • /step-up/verification-code/:type renders StepUpCodeVerification, modeled on MfaCodeVerification (the existing value-less code container) rather than on containers/VerificationCode, whose props, flow-keyed hooks and switch links are all built around a raw identifier. The header shows only the masked identifier from the server context, and the resend goes through the same pinned variant, renewing the stored verification ID so a refresh verifies against the code that was actually sent last.
  • Both pages read the authenticationContext the guard loaded and render the invalid-session page when Core no longer offers that method, so a stale link or a method removed mid-flow is not a form to fill in.
image image image

Route shape

The code page takes the identifier type as a path segment, /step-up/verification-code/:type, rather than the bare /step-up/verification-code of the design's route table. With both primary code methods offered, nothing else on the page distinguishes them once location.state is off the table, and the design's own "no location.state dependency" rule is what makes the path the right place for it: a refresh lands on the same method. getStepUpVerificationCodeRoute(type) builds it, and the method list navigates through it.

Error handling

Each screen composes three sets, in this order:

  • Its own inline errors — a wrong password, a wrong or expired code — shown on the form rather than toasted, as on the sign-in pages.
  • useSubmitInteractionErrorHandler(SignIn), so a sign-in with requested ACR continues from the same pages into MFA verification, a mandatory profile, or MFA enrollment. These pages are where session.step_up.require_verification lands, through the landing that feat(experience,phrases): add the step-up route tree with method selection and guard #9589 navigates it to.
  • useStepUpErrorHandler, which wins on a conflict: a session that is gone, a subject mismatch, a route the mode forbids, and a submission that does not yet satisfy the selected class (back to the method list, which refetches the authoritative context — the path a step-up to mfa takes after this first factor).

Sentinel lockout errors are untouched and keep their existing toast, so step-up and sign-in surface a lockout identically.

Adds step_up.enter_password_description and step_up.enter_verification_code_description in all 21 Experience locales.

Testing

Unit tests

  • apis/experience/step-up.test.ts: the exact payload of each call (pinned strictly: no identifier value on either), that the record the verify call returns is the one that identifies the subject, and that a rejected password or code neither identifies nor submits.
  • pages/StepUp/Password/index.test.tsx: the form renders only when Core still offers the password, the absent forgot-password and switching affordances, the submitted payload, the empty-password guard, the inline invalid-credentials message, and the composed handler set.
  • pages/StepUp/VerificationCode/index.test.tsx: each identifier type with the verification ID the send stored and the masked identifier from the server context, an identifier type that takes no code, and the invalid-session page when the context is gone, the method is no longer offered, or no code was sent for that identifier.
  • containers/StepUpCodeVerification/index.test.tsx: verify and resend by identifier type only, verifying against the renewed record after a resend, the incomplete-code guard, and both failure paths through the step-up handlers.
  • containers/StepUpMethodList/index.test.tsx: updated for the typed code route.

The whole Experience suite passes (639 tests), together with tsc, ESLint and Stylelint for the package and the two phrases packages.

Not exercised: the flow in a browser against a running Core.

Checklist

  • .changeset (dev feature; M8 adds the changeset)
  • unit tests
  • integration tests (LOG-14119)

🤖 Generated with Claude Code

https://claude.ai/code/session_01FFx5WPS3gkEZc5jWEbX7J1


Generated by Claude Code

wangsijie and others added 10 commits September 12, 2026 07:02
- Dispatch `POST /experience/submit` on the stored step-up mode: a pure step-up
  completes through `submitStepUp()`, an allow-list path that asserts the achieved
  context satisfies `selectedAcr`, writes only the methods the interaction
  established, and runs no sign-in guard or side effect (`lastSignInAt`, tenant MFA
  policy, hooks, identity and token-set sync, JIT provisioning are all skipped).
- Add `Interaction.SignIn.StepUp.Submit` to the log-key grammar, recording the
  requested and achieved classes and the proved factor families, and document the
  operation through a dev-feature OpenAPI supplement.
- Add unit coverage for the completion path, the persistence scope and the audit
  payload, plus pure step-up integration coverage driven through a real
  Authorization Code Flow on a developer-created application.
- `manualConsent()` follows the resumed authorization directly; probing the
  interaction URL first consumed the authorization code, so the follow-up request
  had nothing left to complete.
- Give the TOTP user `skipMfaOnSignIn`, so the password sign-in that establishes
  the session is not challenged for MFA; the step-up still verifies the factor
  because the requested ACR is what asks for it.
- Add `MockClient.resumeAuthorization()`: a resumed authorization either asks for
  consent once more or goes straight back to the callback, and both are completed
  from a single request. Re-requesting a URL that already handed back a callback
  consumes the authorization a second time, which is what broke the previous fix.
- Enroll a separate TOTP user per test: a TOTP code is single-use within its time
  step, so tests sharing one secret failed the second verification.
- Restore the user-controlled MFA settings after the policy test, so the tests
  that follow still have TOTP enabled.
…heck

The submit route appends the interaction snapshot to its audit entry, exactly as
it does for a sign-in, so the entry names the verification records the interaction
holds. Those are identifiers, not credentials; the credentials check stays.
`persistEstablishedMethods()` writes the identifier or factor the interaction
established without the revalidation `submit()` performs, so a duplicate primary
email / phone would reach the `(tenant_id, primary_email)` unique constraint as a
raw error instead of the 422 the API promises, and a factor disabled in between
would be bound anyway.

Both guards only run when there is something to write, which the step-up route
allow-list keeps unreachable until the establishment and enrollment routes open.
The ACR assertion runs before the subject is read, so a submission that counted no
verification is a 403, not the 404 the JSDoc promised. Keep the 404 documented for
the shape that does reach it — a counted `bind` proof, which identifies nobody and
which the allow-list blocks until establishment opens — and pin that shape with a
test instead of deleting the guard.
…ction and guard

Add the `/step-up` route tree to the Experience SPA for pure step-up: a
server-driven context provider that creates the interaction only when
storage holds nothing for it and otherwise reads `authenticationContext`
from `GET /experience/interaction`, so a refresh and a return to the method
list recover the same state; the `StepUpGuard` that lands on the
invalid-session page when the interaction is gone or carries no context;
the landing page that auto-forwards a single method with route replacement,
renders the `StepUpMethodList` chooser for several, and reads the
subject-proof connectors and establishable methods when none is available;
and the step-up error handlers, including the
`session.step_up.require_verification` entry consumed by a sign-in with
requested ACR.

Add the `step_up` copy in every Experience locale and the
`require_verification` error phrase in every locale.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DtT2PtWPXt81GyYW1tvdY
…nd error handling

Add component, hook, and API tests for the step-up slice: initialization
and refresh recovery through the guard, the invalid-session and redirect
cases, every method family with its masked subtitle and selection target,
the landing dispatch (auto-forward, chooser, subject proof, establishable
methods, nothing available), the subject-proof connector buttons, and the
step-up error handler mappings including `session.step_up.require_verification`.

Resolve the subject-proof connectors by the type Core states instead of by
id alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DtT2PtWPXt81GyYW1tvdY
…on-gone error codes

Ignore the result of a superseded step-up load, so a landing arrival that
overtakes a slower one never applies a stale context or stays loading.
Share one list of session-gone error codes between the guard and the
step-up error handlers, so an expired OIDC interaction lands on the
invalid-session page from both, and render that page from the guard exactly
as the `unknown-session` route does. Reuse the MFA factor button styles for
the step-up method button instead of copying them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DtT2PtWPXt81GyYW1tvdY
…p screens

Add the `/step-up/password` and `/step-up/verification-code/:type` pages, the
pinned-user first factors of the step-up route tree.

- `verifyStepUpPassword()` sends the password alone; `verifyStepUpVerificationCode()`
  sends the identifier type alone. Neither carries a raw identifier, and both complete
  through the existing `identifyAndSubmitInteraction({ verificationId })`.
- The password form is the password field of the sign-in form and nothing else: no
  identifier switching, and no forgot-password link, since the mode rejects the event
  switch it would start.
- The code page displays only the masked identifier from the server context, and its
  resend goes through the same pinned variant, renewing the stored verification ID.
- Both pages read the authentication context the guard loaded, so a refresh recovers the
  same state. The identifier type rides in the path for the same reason.
- Error handling composes the step-up handlers with the sign-in submission handlers, so a
  sign-in with requested ACR continues from the same pages. Sentinel lockout errors keep
  their existing handling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFx5WPS3gkEZc5jWEbX7J1
@github-actions github-actions Bot added size/xl and removed size/xl labels Sep 13, 2026
@wangsijie
wangsijie force-pushed the wangsijie-log-14120-add-the-step-up-spa-route-tree-with-method-selection-guard branch from af48111 to 285b9a3 Compare September 14, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

1 participant