Skip to content

feat(auth): sign epds_handle_mode through the callback hop - #243

Merged
aspiers merged 1 commit into
mainfrom
split148/handle-mode-plumbing
Aug 11, 2026
Merged

feat(auth): sign epds_handle_mode through the callback hop#243
aspiers merged 1 commit into
mainfrom
split148/handle-mode-plumbing

Conversation

@aspiers

@aspiers aspiers commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Split 3 of 4 from #148. Based on #241 — review that first; this diff shows only its own commit once #241 merges.

What it does

The chooser and consent screens need to know whether an account's handle was user-chosen or server-generated, so they can decide which identifier to show as primary (that's split 4).

epds_handle_mode was already being resolved correctly on the way in, and stored on the auth flow — but it was dropped on the auth-service → pds-core hop. By the time the approval step ran, pds-core had no idea what mode the flow was in and fell back to the default, so a flow that asked for a chosen handle could still show a generated random one.

This PR carries the already-resolved mode across that hop, signed, and forwards it to the final /oauth/authorize. Invalid values arriving on the callback are dropped rather than forwarded.

Resolution is unchanged

To be clear about scope: this PR does not change how the mode is resolved. That logic lives in resolveHandleMode() (packages/shared/src/handle.ts) and is untouched here — query parameter beats client metadata beats EPDS_DEFAULT_HANDLE_MODE, falling back to picker-with-random. What changes is only whether the resolved value survives the hop:

Stage Before After
Resolved and stored on the auth flow ✅ unchanged
Sent on /oauth/epds-callback → pds-core ❌ dropped ✅ sent
Covered by the HMAC signature n/a ✅ signed
Forwarded to the final /oauth/authorize ❌ lost ✅ forwarded when valid
Invalid value arriving on the callback n/a ✅ dropped, not forwarded
Approval / chooser screen falls back to default sees the real mode

⚠️ The signed payload changes — deploy pds-core first

epds_handle_mode is signed rather than appended after signing. The parameter decides what the approval screen shows, so leaving it unsigned would let the browser flip the presentation of a flow it does not own.

That means the HMAC input changes:

request_uri \n email \n approved \n new_account \n handle \n client_id \n epds_handle_mode \n ts

The empty-string sentinel convention is preserved, which makes the rollout one-directional, not symmetric:

Rollout state Result
Old auth-service → new pds-core Works. Old omits the field; new pds-core signs undefined ?? ''. Payloads match.
New auth-service → old pds-core Fails. New sends a real mode; old pds-core has no such field in its payload.

So deploy pds-core before or together with auth-service — never after. In that order there is no broken window. If the order is reversed, in-flight callbacks fail verification until pds-core catches up; the failure mode is a rejected callback and the user retrying sign-in, not a security hole, but it is user-visible.

Per writing-changesets, the HMAC boundary itself is contributor-facing and gets no changeset; the client-developer-facing behaviour does.

Unrelated extraction, flagged

The commit also extracts resolveCompleteIdentity() from the /auth/complete handler. Nothing to do with handle mode — the recovery-email lookup was already nested three levels deep in a function this change had to touch anyway. Called out here rather than shipped silently; happy to drop it if you'd rather keep this PR single-purpose.

Verification

typecheck, lint, format clean; 76 test files / 1137 tests pass. Tamper rejection and invalid-value dropping are both covered by tests.

Series

  1. refactor(pds-core): extract OAuth client-id resolution from CSS middleware #241 — extract OAuth client-id resolution
  2. fix(auth): gate email sign-in until its handlers are installed #242 — gate email sign-in until handlers are installed
  3. feat(auth): sign epds_handle_mode through the callback hop #243 ← this PR — sign epds_handle_mode through the callback hop
  4. fix(pds-core): identify generated-handle accounts by email #244 — identify generated-handle accounts by email

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings August 5, 2026 17:09
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
epds-demo Ready Ready Preview Aug 11, 2026 3:39pm

Request Review

@changeset-bot

changeset-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 534647d

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@aspiers, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e3dab6a0-344c-499e-94d1-e99f8b129476

📥 Commits

Reviewing files that changed from the base of the PR and between 11c23da and 534647d.

📒 Files selected for processing (9)
  • .changeset/handle-mode-through-approval.md
  • packages/auth-service/src/__tests__/callback-handle-mode.test.ts
  • packages/auth-service/src/routes/choose-handle.ts
  • packages/auth-service/src/routes/complete.ts
  • packages/pds-core/src/__tests__/epds-callback-authorize.test.ts
  • packages/pds-core/src/index.ts
  • packages/pds-core/src/lib/epds-callback-authorize.ts
  • packages/shared/src/__tests__/crypto.test.ts
  • packages/shared/src/crypto.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@railway-app

railway-app Bot commented Aug 5, 2026

Copy link
Copy Markdown

🚅 Deployed to the ePDS-pr-243 environment in ePDS

Service Status Web Updated (UTC)
@certified-app/auth-service ✅ Success (View Logs) Web Aug 6, 2026 at 9:58 am
@certified-app/pds-core ✅ Success (View Logs) Web Aug 6, 2026 at 9:17 am
@certified-app/demo ✅ Success (View Logs) Web Aug 5, 2026 at 5:14 pm
@certified-app/demo untrusted ✅ Success (View Logs) Web Aug 5, 2026 at 5:13 pm

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR ensures epds_handle_mode (handle assignment/display mode) survives the auth-service → pds-core callback hop by including it in the HMAC-signed callback payload, and then only forwarding valid values back into the upstream /oauth/authorize redirect. This supports consistent account labelling on chooser/consent screens, including PAR-backed flows.

Changes:

  • Extend the signed epds-callback payload to include epds_handle_mode, with empty-string sentinel handling aligned with existing optional fields.
  • Add pds-core callback redirect sanitization/forwarding for epds_handle_mode (forward valid values, drop invalid).
  • Add targeted unit/integration tests in shared, auth-service, and pds-core to pin signing, verification, and forwarding behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/shared/src/crypto.ts Adds epds_handle_mode to the signed callback payload and verification payload construction.
packages/shared/src/tests/crypto.test.ts Adds coverage for signing/verifying epds_handle_mode and rejecting tampering.
packages/pds-core/src/lib/epds-callback-authorize.ts New helper to validate and forward epds_handle_mode on the post-callback /oauth/authorize redirect.
packages/pds-core/src/index.ts Threads epds_handle_mode into callback verification and uses the new authorize-redirect builder.
packages/pds-core/src/tests/epds-callback-authorize.test.ts New unit tests ensuring valid epds_handle_mode is forwarded and invalid values are dropped.
packages/auth-service/src/routes/complete.ts Signs epds_handle_mode into the callback URL and refactors identity resolution into resolveCompleteIdentity().
packages/auth-service/src/routes/choose-handle.ts Includes epds_handle_mode in the signed callback params for chosen-handle flows.
packages/auth-service/src/tests/callback-handle-mode.test.ts New integration-ish tests asserting handle mode is preserved and signatures verify across key flows.
.changeset/handle-mode-through-approval.md Adds a client-developer-facing changeset documenting epds_handle_mode resolution/behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@aspiers
aspiers force-pushed the split148/handle-mode-plumbing branch from daa7cea to 84de1ac Compare August 5, 2026 17:25
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-243 August 5, 2026 17:25 Destroyed
@aspiers
aspiers force-pushed the split148/oauth-request-context branch from ee1ec80 to 707355b Compare August 6, 2026 09:16
@aspiers
aspiers force-pushed the split148/handle-mode-plumbing branch from 84de1ac to 14dd1b4 Compare August 6, 2026 09:16
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-243 August 6, 2026 09:16 Destroyed
Base automatically changed from split148/oauth-request-context to main August 6, 2026 09:56
@aspiers
aspiers force-pushed the split148/handle-mode-plumbing branch from 14dd1b4 to bf37444 Compare August 6, 2026 09:57
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-243 August 6, 2026 09:57 Destroyed
@coveralls-official

coveralls-official Bot commented Aug 6, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 31508182435

Coverage increased (+2.2%) to 60.533%

Details

  • Coverage increased (+2.2%) from the base build.
  • Patch coverage: 7 uncovered changes across 2 files (14 of 21 lines covered, 66.67%).
  • 2 coverage regressions across 1 file.

Uncovered Changes

File Changed Covered %
packages/pds-core/src/index.ts 5 0 0.0%
packages/auth-service/src/routes/complete.ts 9 7 77.78%
Total (3 files) 21 14 66.67%

Coverage Regressions

2 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
packages/pds-core/src/index.ts 2 0.0%

Coverage Stats

Coverage Status
Relevant Lines: 3119
Covered Lines: 1885
Line Coverage: 60.44%
Relevant Branches: 1951
Covered Branches: 1184
Branch Coverage: 60.69%
Branches in Coverage %: Yes
Coverage Strength: 9.89 hits per line

💛 - Coveralls

The chooser and consent screens need to know whether the account's
handle was user-chosen or server-generated, so they can decide which
identifier to show as primary. That mode was resolved on the way in
but lost on the auth-service -> pds-core hop, so the approval step
could fall back to showing a generated random handle.

Carry epds_handle_mode across the hop, and sign it rather than
appending it afterwards: the parameter decides what the approval
screen shows, so leaving it unsigned would let the browser flip the
presentation of a flow it does not own. Invalid values are dropped at
the boundary instead of being forwarded.

Adding a field to the signed payload changes the HMAC input, so
auth-service and pds-core must be deployed together. In-flight
callbacks signed by the old auth-service will fail verification on
the new pds-core, and the user retries the sign-in.

Also extracts resolveCompleteIdentity() from the /auth/complete
handler. Unrelated to handle mode, but the recovery-email lookup was
already nested three levels deep in a function this change had to
touch anyway.

Split out of #148.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aspiers
aspiers force-pushed the split148/handle-mode-plumbing branch from bf37444 to 534647d Compare August 11, 2026 15:38
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-243 August 11, 2026 15:39 Destroyed
@sonarqubecloud

Copy link
Copy Markdown

@aspiers
aspiers merged commit 7ade8ca into main Aug 11, 2026
24 checks passed
@aspiers
aspiers deleted the split148/handle-mode-plumbing branch August 11, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants