feat(experience,phrases): add pinned password and primary code step-up screens - #9595
Draft
Conversation
- 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
wangsijie
force-pushed
the
wangsijie-log-14120-add-the-step-up-spa-route-tree-with-method-selection-guard
branch
from
September 14, 2026 09:00
af48111 to
285b9a3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on #9589 (the
/step-uproute 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.tsgains the two verification calls, next to thesendStepUpVerificationCode()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/passwordrenders 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/:typerendersStepUpCodeVerification, modeled onMfaCodeVerification(the existing value-less code container) rather than oncontainers/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.authenticationContextthe 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.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-codeof the design's route table. With both primary code methods offered, nothing else on the page distinguishes them oncelocation.stateis off the table, and the design's own "nolocation.statedependency" 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:
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 wheresession.step_up.require_verificationlands, 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 tomfatakes 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_descriptionandstep_up.enter_verification_code_descriptionin 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)🤖 Generated with Claude Code
https://claude.ai/code/session_01FFx5WPS3gkEZc5jWEbX7J1
Generated by Claude Code