feat: prompt step-up consent on the VERIFIED reason, mid-flow - #104
Merged
Conversation
The flagged follow-up from #103: the step-up consent prompt fired before the RP start fetch, so the human decided on origin/rpDid alone and the signed reason — the thing #103 made verifiable — was never shown. The spec's rule is 'consumers MUST verify the proof BEFORE surfacing the reason'; we verified but never surfaced. Reorder the flow so consent sits between verification and signing: - core: new performStepUpVta owns the enforced order — start -> verify (signed approve-request; proof + enrolled-executor signer + issuer == page rpDid) -> consent callback -> sign approve-response -> finish. A refused approve-request returns before the callback, so no prompt is ever raised for unverifiable content; a decline sends nothing (the RP's challenge lapses on its TTL). Unit-tested against a mock RP: prompt content comes from inside the signature (a tampered unsigned copy is never shown), declined sends nothing, missing document and issuer/rpDid mismatch both refuse without prompting. - extension background: handleStepUpVta no longer pre-prompts; it forwards to the offscreen, threading the browser-attested origin. The new mid-flow RUNTIME_STEP_UP_CONSENT (offscreen -> background) raises the prompt through the same gatedConsent gate as before — the 'remember this site' origin-trust short-circuit keeps its pre-#103 semantics — with the verified reason length-capped (500 chars) and control/bidi-character-stripped before it reaches the popup. - confirm popup: step-up framing ('Step-up approval request') plus a reason card that renders the RP's verified reason as plain text (React text nodes, no markup), visually attributed to the verified RP DID card with an explicit 'their claim' note. A document with no reason falls back to the previous origin/rpDid-only prompt. Security invariants from #103 unchanged: no prompt on missing/invalid document, issuer/rpDid binding intact, response signing only after explicit user approval. Part of the step-up programme (affinidi/affinidi-webvh-service#147). Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
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.
What
The flagged follow-up from #103 ("Merge notes"): the step-up consent prompt fired before the RP
startfetch, so the human decided on origin/rpDid alone — the signedreason, the very thing #103 made verifiable, was never shown. The spec's rule is "consumers MUST verify the proof BEFORE surfacing the reason"; we verified, but never surfaced.This PR reorders the flow so the consent prompt sits between verification and signing, and renders the verified reason.
Flow, before → after
startfetch → verify → sign →finishstartfetch → verify (proof + enrolled-executor signer + issuer == page rpDid)RUNTIME_STEP_UP_CONSENT: consent prompt showing the verified reason, attributed to the verified RPfinishperformStepUpVta(packages/corerp-login/step-up.ts) encodes start → verify → consent → sign → finish with an injected consent callback, so the ordering invariant is unit-testable. The offscreen contributes only the holder identity, the enrolled-executor set, and the bridge to a human.RUNTIME_STEP_UP_CONSENT(offscreen → background), same shape as the existingRUNTIME_TASK_CONSENTleg. Background raises the samegatedConsentgate as before — the "remember this site" origin-trust short-circuit keeps its pre-existing semantics; the reorder changes when the prompt fires and what it shows, not who sees one.startandfinish. A decline (or closing the window) sends nothing: no denied approve-response, the RP's challenge lapses on its own TTL, matching prior declined behaviour.Rendering the reason safely
\u0000–\u001F,\u200E/F,\u202A–E,\u2066–69) and caps at 500 chars with ellipsis (render-side cap as belt-and-braces).reasonin the signed payload → the card is absent and the prompt is the previous origin/rpDid-only text.#103 invariants preserved
document— refusal happens before the consent callback can run (tested).performStepUpVta; mismatch refuses without prompting — tested).Tests
Extended
packages/core/tests/rp-login.step-up.mjswith a mock-RP harness forperformStepUpVta(5 new tests): prompt content comes from inside the signature even when the unsigned copy is tampered; a reason-less document prompts without a reason member; declined sends nothing (only/startever hit); missing document refuses without prompting; enrolled-but-wrong issuer refuses without prompting.npm run lint(tsc -b, all workspaces),npm test(181 pass),npm run build— all green.Refs #103. Part of the step-up programme: affinidi/affinidi-webvh-service#147.