Fix OAuth account enrichment with tooltip explanation#148
Fix OAuth account enrichment with tooltip explanation#148
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: a7022f3 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 |
📝 WalkthroughWalkthroughPropagates and validates epds_handle_mode through auth flows and callbacks, injects an enriched client script that presents email-first identifiers and accessible handle tooltips (hiding handles in random mode), refactors client-id/request_uri resolution, and adds/updates tests and E2E steps to cover consent, chooser, and account enrichment plus login readiness. ChangesEmail-First Account Presentation & Handle-Mode Enrichment
Sequence DiagramsequenceDiagram
participant Browser
participant AuthService as Auth Service
participant DB as Database
participant PDS as PDS Core / OAuth Provider
participant DOM as Browser DOM (Enrichment Script)
participant User
Browser->>AuthService: POST /auth/complete (with flow cookie)
AuthService->>DB: get auth_flow (handle_mode)
DB-->>AuthService: return handle_mode
AuthService->>AuthService: resolve identity (email / DID)
AuthService->>PDS: redirect → /oauth/epds-callback?epds_handle_mode=...
PDS->>PDS: buildEpdsCallbackAuthorizeUrl(..., handleMode)
PDS-->>Browser: render /oauth/authorize HTML (meta + enrichment script)
Browser->>DOM: execute enrichment script
DOM->>DOM: read meta handle-mode, normalize accounts, enrich UI
alt handleMode == random
DOM->>DOM: hide handles, add aria-describedby + tooltip
else
DOM->>DOM: show handle and email together
end
User->>DOM: hover/focus → tooltip reveals handle/explanation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
🚅 Deployed to the pr-5c336d-148 environment in ePDS
|
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/auth-service/src/routes/complete.ts (1)
164-169:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftSign
epds_handle_modeas part of the callback contract.
epds_handle_modeis being appended aftersignCallback(), so the browser can alter it before/oauth/epds-callbackreaches pds-core. Because the callback value is later used to drive the chooser/consent presentation, this makes the privacy/display mode user-tamperable. Please either include it in the signed payload end-to-end or stop trusting the callback query for this field.As per coding guidelines,
packages/{auth-service,pds-core}/**/*.{ts,tsx,js,mjs}: All epds-callback redirects must be HMAC-SHA256 signed using signCallback()/verifyCallback() from@certified-app/shared.Also applies to: 208-213
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/auth-service/src/routes/complete.ts` around lines 164 - 169, The fix: include epds_handle_mode in the signed callback instead of appending it afterward — add epds_handle_mode: flow.handleMode into the callbackParams object before calling signCallback(callbackParams, ctx.config.epdsCallbackSecret) so the generated ts/sig cover that field, and keep using URLSearchParams(params) afterwards; ensure any other occurrences (the similar block around the later 208-213) follow the same pattern so all epds-callback redirects are HMAC-SHA256 signed via signCallback()/verifyCallback().packages/auth-service/src/routes/choose-handle.ts (1)
366-371:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftAvoid adding
epds_handle_modeoutside the signed callback payload.This POST path has the same integrity gap as
/auth/complete:epds_handle_modeis mutated onto the redirect URL aftersignCallback(). That leaves the mode browser-controlled on the hop to pds-core, so the final consent/chooser UI can be flipped without invalidating the callback signature.As per coding guidelines,
packages/{auth-service,pds-core}/**/*.{ts,tsx,js,mjs}: All epds-callback redirects must be HMAC-SHA256 signed using signCallback()/verifyCallback() from@certified-app/shared.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/auth-service/src/routes/choose-handle.ts` around lines 366 - 371, The code currently appends epds_handle_mode to params after calling signCallback, leaving it unsigned; instead, if flow.handleMode is present add it into the callbackParams object before calling signCallback (e.g. set callbackParams.epds_handle_mode = flow.handleMode), then call signCallback(callbackParams, ctx.config.epdsCallbackSecret) and build URLSearchParams from the resulting signed callback (remove the later params.set('epds_handle_mode', ...) call); this ensures the handle mode is included in the HMAC produced by signCallback/verifyCallback.
🧹 Nitpick comments (2)
.changeset/preserve-handle-mode-callback.md (1)
5-5: ⚡ Quick winRewrite the summary line in plainer end-user language.
app-requested handle display modereads like implementation terminology. Since this first line is the headline release note for End users, I’d rephrase it in terms of what people actually see during sign-up/approval.As per coding guidelines,
.changeset/*.md: Changeset summary line is the first non-frontmatter line, read by every listed audience; if End users is an audience, write in plain language without OTP/DID/PAR/OAuth jargon or implementation concepts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.changeset/preserve-handle-mode-callback.md at line 5, Rewrite the first non-frontmatter line (currently: "Sign-up screens now keep the app-requested handle display mode through the final approval step.") into plain end-user language that describes what the user sees during sign-up/approval without implementation jargon; e.g. state that the app will remember and show the preferred handle/display option throughout the sign-up and final approval screens, using simple terms like "remember" or "keep showing" instead of "app-requested handle display mode" or other technical phrases.e2e/step-definitions/session-reuse-bugs.steps.ts (1)
439-506: ⚡ Quick winAssert the exact hidden handle, not just a non-empty suffix.
This still passes if
aria-describedbypoints at a generic placeholder or the wrong row's description. Since the callback already hashandleLabel, capture its text and compare the description body to that exact hidden handle.Suggested tightening
type HiddenHandleDescriptionRow = { describedBy: string | null descriptions: { id: string isHiddenHandleDescription: boolean text: string }[] emailTitle: string | null + hiddenHandleText: string rowIndex: number } ... return { describedBy, descriptions, emailTitle: emailLabel.getAttribute('title'), + hiddenHandleText: handleLabel.textContent?.trim() ?? '', rowIndex, } ... - expect( - descriptionText.slice(prefixIndex + prefix.length).trim().length, - ).toBeGreaterThan(0) + expect(descriptionText.slice(prefixIndex + prefix.length).trim()).toBe( + row.hiddenHandleText, + )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@e2e/step-definitions/session-reuse-bugs.steps.ts` around lines 439 - 506, The test currently only asserts that the hidden-handle description contains a non-empty suffix; instead capture the actual handle text from the DOM (use handleLabel.textContent.trim() inside the evaluateAll mapping and include it on the returned HiddenHandleDescriptionRow), then in the outer assertions compare description.text exactly (or ensure it contains) that captured handleText (not just non-empty trimmed suffix). Update references in the mapping for handleLabel and the returned object (e.g., add handleText) and replace the loose suffix checks on descriptionText with a precise comparison against that handleText.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@e2e/step-definitions/consent.steps.ts`:
- Around line 162-172: The test reads aria-describedby into describedBy and
constructs a locator with page.locator(`#${describedBy?.trim()}`), but
aria-describedby can contain multiple space-separated IDs; split describedBy on
whitespace (or use describedBy?.split(/\s+/)[0]) and guard for null/empty before
building the selector so you only target the first ID; update the code around
describedBy, tooltipControl and the tooltip locator to use the first token (and
bail or assert if none) when calling page.locator and subsequent expects.
In `@packages/pds-core/src/chooser-enrichment.ts`:
- Around line 895-910: The catch block that silences failures when resolving
request_uri/metadata should log the failure at debug level: thread the pds-core
logger into this middleware (or import the existing logger) and in the catch for
the resolveOAuthClientIdFromQuery/resolveMeta sequence emit logger.debug with a
concise message that includes the failing inputs (e.g. the query/request_uri and
resolved clientId if available) and the caught error/stack; keep behavior
unchanged otherwise so metaMode still falls back to query/env. Ensure the log
uses logger.debug(...) and references resolveOAuthClientIdFromQuery,
resolveClientIdFromRequestUri and resolveMeta to locate the code to modify.
---
Outside diff comments:
In `@packages/auth-service/src/routes/choose-handle.ts`:
- Around line 366-371: The code currently appends epds_handle_mode to params
after calling signCallback, leaving it unsigned; instead, if flow.handleMode is
present add it into the callbackParams object before calling signCallback (e.g.
set callbackParams.epds_handle_mode = flow.handleMode), then call
signCallback(callbackParams, ctx.config.epdsCallbackSecret) and build
URLSearchParams from the resulting signed callback (remove the later
params.set('epds_handle_mode', ...) call); this ensures the handle mode is
included in the HMAC produced by signCallback/verifyCallback.
In `@packages/auth-service/src/routes/complete.ts`:
- Around line 164-169: The fix: include epds_handle_mode in the signed callback
instead of appending it afterward — add epds_handle_mode: flow.handleMode into
the callbackParams object before calling signCallback(callbackParams,
ctx.config.epdsCallbackSecret) so the generated ts/sig cover that field, and
keep using URLSearchParams(params) afterwards; ensure any other occurrences (the
similar block around the later 208-213) follow the same pattern so all
epds-callback redirects are HMAC-SHA256 signed via
signCallback()/verifyCallback().
---
Nitpick comments:
In @.changeset/preserve-handle-mode-callback.md:
- Line 5: Rewrite the first non-frontmatter line (currently: "Sign-up screens
now keep the app-requested handle display mode through the final approval
step.") into plain end-user language that describes what the user sees during
sign-up/approval without implementation jargon; e.g. state that the app will
remember and show the preferred handle/display option throughout the sign-up and
final approval screens, using simple terms like "remember" or "keep showing"
instead of "app-requested handle display mode" or other technical phrases.
In `@e2e/step-definitions/session-reuse-bugs.steps.ts`:
- Around line 439-506: The test currently only asserts that the hidden-handle
description contains a non-empty suffix; instead capture the actual handle text
from the DOM (use handleLabel.textContent.trim() inside the evaluateAll mapping
and include it on the returned HiddenHandleDescriptionRow), then in the outer
assertions compare description.text exactly (or ensure it contains) that
captured handleText (not just non-empty trimmed suffix). Update references in
the mapping for handleLabel and the returned object (e.g., add handleText) and
replace the loose suffix checks on descriptionText with a precise comparison
against that handleText.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3cebbd6d-6d03-4125-9514-8c20d849467e
📒 Files selected for processing (21)
.beads/issues.jsonl.changeset/preserve-handle-mode-callback.md.changeset/random-handle-consent-metadata.md.changeset/scoped-account-enrichment.mde2e/step-definitions/consent.steps.tse2e/step-definitions/session-reuse-bugs.steps.tsfeatures/consent-screen.featurefeatures/session-reuse-bugs.featurepackages/auth-service/src/__tests__/callback-handle-mode.test.tspackages/auth-service/src/routes/choose-handle.tspackages/auth-service/src/routes/complete.tspackages/pds-core/src/__tests__/chooser-enrichment.test.tspackages/pds-core/src/__tests__/epds-callback-authorize.test.tspackages/pds-core/src/__tests__/preview-chooser.test.tspackages/pds-core/src/__tests__/preview-consent.test.tspackages/pds-core/src/chooser-enrichment.tspackages/pds-core/src/index.tspackages/pds-core/src/lib/client-css-injection.tspackages/pds-core/src/lib/epds-callback-authorize.tspackages/pds-core/src/lib/oauth-request-context.tspackages/pds-core/src/lib/preview-consent.ts
|
(reply generated by gpt-5.5) Review follow-up status:
|
|
(reply generated by gpt-5.5) Follow-up: added |
…ndles-in-random-mode-explain-via-tooltip
|
@coderabbitai please review |
|
The first PR description image (17:29:13) shows an OAuth authorization consent screen for The second PR description image (17:31:05) shows a similar OAuth consent screen in random-handle mode. The primary account identifier is now the email
✅ Actions performedReview triggered.
|
…ndles-in-random-mode-explain-via-tooltip
|
@coderabbitai please review |
|
The first PR description image (17:29:13) shows an OAuth authorization consent screen where the AT Protocol handle The second PR description image (17:31:05) shows the same consent screen in random-handle mode — the primary identifier is now the email
✅ Actions performedReview triggered.
|
| sendOtpForm.dataset.epdsLoginReady = 'true'; | ||
| sendOtpBtn.disabled = false; |
There was a problem hiding this comment.
@Kzoeps These seem to trigger unconditionally on page load, which would make this disable/enable mechanism ineffective.
| : undefined | ||
| } | ||
|
|
||
| export function buildEpdsCallbackAuthorizeUrl(opts: { |
There was a problem hiding this comment.
This seems a little odd. Surely the code already has had to construct callback URLs for a long time, so why is a new function needed now?
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/shared/src/crypto.ts (1)
28-34:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCompare UTF-8 byte lengths before calling
crypto.timingSafeEqual.The
timingSafeEqual()helper guards on JavaScript string.length, which counts UTF-16 code units, not UTF-8 bytes. When passed non-ASCII strings with the same.lengthbut different byte lengths (e.g., '£' vs 'a' both.length === 1),Buffer.from(a)andBuffer.from(b)produce buffers of different byte lengths, causingcrypto.timingSafeEqual()to throwRangeErrorinstead of returningfalse. This is a timing side-channel: an attacker observes exception vs. return to infer whether inputs have multibyte characters.Suggested fix
export function timingSafeEqual(a: string, b: string): boolean { - if (a.length !== b.length) { - const dummy = Buffer.alloc(a.length) + const aBytes = Buffer.byteLength(a, 'utf8') + const bBytes = Buffer.byteLength(b, 'utf8') + if (aBytes !== bBytes) { + const dummy = Buffer.alloc(Math.max(aBytes, bBytes)) crypto.timingSafeEqual(dummy, dummy) return false } - return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)) + return crypto.timingSafeEqual( + Buffer.from(a, 'utf8'), + Buffer.from(b, 'utf8'), + ) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/crypto.ts` around lines 28 - 34, The timingSafeEqual function currently compares JavaScript string .length (UTF-16 code units) which can differ from UTF-8 byte length and can cause crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)) to throw; update timingSafeEqual to compare UTF-8 byte lengths using Buffer.byteLength(a, 'utf8') and Buffer.byteLength(b, 'utf8'), and if the byte lengths differ call crypto.timingSafeEqual on a dummy buffer of the larger byte length (e.g., Buffer.alloc(maxLen)) to preserve constant-time behavior and then return false; only convert to Buffer.from(..., 'utf8') and call crypto.timingSafeEqual when the UTF-8 byte lengths are equal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/auth-service/src/routes/login-page.ts`:
- Line 672: The <noscript> warning is currently nested inside the `#step-email`
container (which gets the "hidden" class when initialStep === 'otp') so no-JS
users landing on the OTP step won't see it; move the <noscript><div
class="flash-msg error">…</div></noscript> element out of the `#step-email` block
so it is rendered alongside the page-level content (outside the email step
wrapper) to ensure it is always visible regardless of initialStep/hidden state,
keeping the same markup and classes so styling remains unchanged.
---
Outside diff comments:
In `@packages/shared/src/crypto.ts`:
- Around line 28-34: The timingSafeEqual function currently compares JavaScript
string .length (UTF-16 code units) which can differ from UTF-8 byte length and
can cause crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)) to throw;
update timingSafeEqual to compare UTF-8 byte lengths using Buffer.byteLength(a,
'utf8') and Buffer.byteLength(b, 'utf8'), and if the byte lengths differ call
crypto.timingSafeEqual on a dummy buffer of the larger byte length (e.g.,
Buffer.alloc(maxLen)) to preserve constant-time behavior and then return false;
only convert to Buffer.from(..., 'utf8') and call crypto.timingSafeEqual when
the UTF-8 byte lengths are equal.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 00bc0a21-910e-4e27-b253-28ddd84d98f7
📒 Files selected for processing (16)
.changeset/sign-in-account-presentation.mdCHANGELOG.mde2e/step-definitions/consent.steps.tsfeatures/consent-screen.featurepackage.jsonpackages/auth-service/src/__tests__/build-epds-callback-url.test.tspackages/auth-service/src/__tests__/callback-handle-mode.test.tspackages/auth-service/src/__tests__/login-page.test.tspackages/auth-service/src/routes/choose-handle.tspackages/auth-service/src/routes/complete.tspackages/auth-service/src/routes/login-page.tspackages/pds-core/src/__tests__/client-css-injection.test.tspackages/pds-core/src/index.tspackages/pds-core/src/lib/client-css-injection.tspackages/shared/src/__tests__/crypto.test.tspackages/shared/src/crypto.ts
✅ Files skipped from review due to trivial changes (4)
- package.json
- CHANGELOG.md
- .changeset/sign-in-account-presentation.md
- features/consent-screen.feature
🚧 Files skipped from review as they are similar to previous changes (6)
- packages/pds-core/src/lib/client-css-injection.ts
- packages/auth-service/src/tests/callback-handle-mode.test.ts
- packages/auth-service/src/tests/login-page.test.ts
- packages/auth-service/src/routes/complete.ts
- packages/pds-core/src/index.ts
- e2e/step-definitions/consent.steps.ts
…ndles-in-random-mode-explain-via-tooltip
d77caed to
60d4ed4
Compare
|



Summary
This PR restores account identity enrichment across the OAuth and account-management surfaces for ePDS deployments that use generated/random handles.
The user-facing goal is simple: when a handle is system-generated and not meaningful to the user, ePDS should identify the account primarily by email while still keeping the public AT Protocol handle available where it is useful for context, accessibility, or app interoperability.
closes #143
What changed
Restores scoped account identity enrichment on:
/account/account/:didMakes random-handle flows email-first:
Tightens the DOM enrichment logic so only known account identity elements are rewritten.
This avoids accidentally mutating legal copy, technical details, connected-app rows, device rows, footer text, or arbitrary prose that happens to contain handle-like text.
Makes
epds_handle_moderesolution consistent between auth-service and pds-core:request_uri/oauth/epds-callbackShares OAuth request-context resolution between chooser enrichment and client CSS injection so PAR-backed authorize pages can still resolve the client id correctly.
Updates random-handle e2e coverage to assert the accessible hidden-handle behavior instead of the previous stale
titletooltip behavior.Summary by CodeRabbit
New Features
Bug Fixes
Accessibility
Tests
UI Changes
when handle mode is picker or picker with random
when handle mode is random