[DT-4012] Handle the full consent /api/user/me contract and B2C authorization-response errors - #3881
Merged
Merged
Conversation
…client Consent now answers /api/user/me with distinct statuses (DT-3997 #3021, DT-4011 #3032): 401 rejected token, 404 authenticated but unregistered, 409 Sam sub-provider conflict with an actionable message. - server/src/auth/me.ts: 401 destroys the session (no more profileSeen disambiguation), 404 keeps the "authenticated, no user" answer, and 409 forwards the upstream message as { error: 'provider_conflict', message } after destroying the session. - server/src/types/session.ts: drop the now-dead profileSeen flag (and its once-per-session store write). - src/libs/auth/session.ts: the probe treats 409 as an authoritative signed-out answer and shows the conflict message instead of failing sign-in generically. - src/libs/auth/postSignIn.ts: a 409 from getMe signs the user out with the message instead of attempting a registration that cannot succeed; the legacy "azureb2c authentication error" substring check stays for older consent builds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aw 500 JSON B2C can answer the authorization request with an error instead of a code — the known case is a Microsoft identity outside the accepted client ids (e.g. a personal Live account), plus the user's own cancel (access_denied). authorizationCodeGrant then throws AuthorizationResponseError, and the thrown 500 answered the browser's top-level /auth/callback navigation with raw JSON, stranding the user. - server/src/auth/callback.ts: catch AuthorizationResponseError; a cancel redirects home silently, anything else redirects to /?signInError=provider. Other failures (state mismatch, bad token) still throw. - src/App.tsx: show a fixed error toast for the signInError marker and strip it from the URL. The marker is a key, never reflected text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Coverage Report for DUOS Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
… (S3776) Sonar flagged getMe at 18 against the allowed 15 after the 401/404/409 mapping landed. The refresh-before-forward block moves to refreshedIfExpiring() and the 409 body parse to providerConflictMessage() — same behavior, getMe now reads as one status-to-answer mapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Updates DUOS UI + BFF sign-in/session handling to match Consent’s expanded /api/user/me contract (401/404/409) and to gracefully handle Azure B2C authorization-response errors during /auth/callback, surfacing actionable user guidance instead of generic failures.
Changes:
- Map Consent/BFF
409 provider_conflictto a signed-out session with a user-facing actionable toast, and prevent impossible registration attempts. - Treat upstream
401as a terminal rejected-session response (destroy session) while preserving the “authenticated but unregistered” flow for404. - Catch
openid-clientAuthorizationResponseErrorin/auth/callbackand redirect back to the SPA with a stablesignInErrormarker that the SPA turns into a toast and then strips.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/libs/auth/session.spec.ts | Adds probe-level coverage for 409 provider-conflict behavior and caching semantics. |
| test/libs/auth/postSignIn.spec.ts | Adds coverage to ensure 409 from getMe signs out and does not attempt registration; keeps legacy substring path. |
| test/components/App.spec.tsx | Adds coverage for SPA toast + URL marker stripping when landing with ?signInError=provider. |
| src/libs/auth/session.ts | Implements 409 handling in the BFF session probe and shows an actionable toast once per attempt. |
| src/libs/auth/postSignIn.ts | Treats 409 from getMe as terminal conflict: show message + sign out rather than register. |
| src/App.tsx | Adds a boot-time handler for signInError marker to show a fixed toast and strip the marker from the URL. |
| server/test/me.test.ts | Updates server-side contract tests for 401 destroy behavior and new 409 forwarding + fallback. |
| server/test/authCrypto.test.ts | Adds coverage for callback redirects when B2C returns authorization-response errors (server_error/access_denied). |
| server/src/types/session.ts | Removes profileSeen from the session type (no longer needed with the new contract). |
| server/src/auth/me.ts | Reworks /auth/me mapping for 401/404/409 and removes profileSeen-based behavior. |
| server/src/auth/callback.ts | Catches AuthorizationResponseError and redirects to SPA rather than returning raw JSON errors. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… level - me.ts: the !res.ok comment claimed "non-4xx", but the branch also catches contract-undefined 4xx (400/403/429) — reworded to match the behavior, which stays deliberately transient. - callback.ts: log the user's own B2C cancel (access_denied) at info so it stays out of warn-based alerting; real provider errors stay warn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rushtong
marked this pull request as ready for review
August 25, 2026 16:34
rushtong
requested review from
fboulnois and
kevinmarete
and removed request for
a team
August 25, 2026 16:34
This comment was marked as outdated.
This comment was marked as outdated.
rushtong
marked this pull request as draft
August 25, 2026 17:12
…toast The server_error on /auth/callback is not a rejected account type: the non-prod B2C tenant's federation client secret to the upstream Microsoft provider expired (AADB2C90289 / invalid_client), which fails every Microsoft sign-in in those environments. The old toast told users their account type was unsupported — wrong advice for a provider-side fault. It now says the provider reported an error, try again, contact Terra support; the server log keeps the B2C error and description. Comments and the test's error_description now name the real cause. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
rushtong
marked this pull request as ready for review
August 25, 2026 17:34
kevinmarete
approved these changes
Aug 25, 2026
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.



Addresses
DT-4012
Security risk: low — sign-in error paths change how a refusal is presented; no change to who is admitted.
Summary
Consent now answers
GET /api/user/mewith distinct statuses (DT-3997 #3021, DT-4011 #3032): 401 rejected token, 404 authenticated but unregistered, 409 Sam sub-provider conflict with an actionable message. The BFF and client now map that contract instead of the old conflated handling:server/src/auth/me.ts: 401 destroys the session (theprofileSeendisambiguator is dead machinery and is removed, along with its once-per-session store write); 404 keeps the "authenticated, no user" answer that routes new users into registration; 409 forwards the upstream message as{ error: 'provider_conflict', message }after destroying the session — the account lives under the other provider, so the session cannot become usable.src/libs/auth/session.ts: the probe treats 409 as an authoritative signed-out answer and shows the message (sign in with the other provider, plus the support link) instead of failing sign-in generically.src/libs/auth/postSignIn.ts: a 409 from getMe signs the user out with the message instead of attempting a registration that cannot succeed. The legacy"azureb2c authentication error"substring check stays for older consent builds.A second edge case is outside consent entirely: B2C can answer the authorization request itself with an error instead of a code — the user's own cancel, or a B2C-side failure. The observed case is
AADB2C90289/invalid_client: the non-prod B2C tenant's federation client secret to the upstream Microsoft provider has expired, which fails every Microsoft sign-in in those environments (secret rotation is tracked separately).authorizationCodeGrantthrowsAuthorizationResponseError, and the resulting 500 answered the browser's top-level/auth/callbacknavigation with raw JSON, stranding the user:server/src/auth/callback.ts: catchAuthorizationResponseError; a cancel (access_denied) redirects home silently, anything else logs the B2C error and redirects to/?signInError=provider. State-mismatch and token-validation failures still throw.src/App.tsx: a fixed toast for thesignInErrormarker, then the marker is stripped from the URL. The marker is a key, never reflected text, so the toast cannot echo attacker-supplied content.Deployment note: the 401 tightening assumes every environment runs a consent build with both fixes above. An old consent behind this BFF would destroy a brand-new user's session on the first probe and block registration.
Testing
pnpm run lint,pnpm run type-check,pnpm test(368 files, 4312 tests), and the server suite (327 tests) pass. New coverage: 401 destroys the session (including a failing session store); 409 forwards the upstream message, falls back on an unusable body, and overwrites the probe's held signed-in answer; the bootstrap signs out on a 409 without a registration attempt; the callback's authorization-error redirects run against the realopenid-clientgrant (fake B2C), alongside the unchanged security rejections; the App toast renders once and the marker is stripped.The Microsoft sign-in repro was observed manually and produced the exact error this PR handles.
404 User Not Found -> New User Registration Case
409 Sam AzureB2C Error Case
500 Auth Provider Error
Have you read Terra's Contributing Guide lately? If not, do that first.
🤖 Generated with Claude Code