Skip to content

Validate external account state and identity - #2630

Open
niemyjski wants to merge 27 commits into
mainfrom
issue/external-account-state
Open

niemyjski wants to merge 27 commits into
mainfrom
issue/external-account-state

Conversation

@niemyjski

@niemyjski niemyjski commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Refresh an existing OAuth account's username and supplied profile data on linking and login, retaining one canonical entry for duplicate identities. Prevent concurrent sign-ins and transfers from creating duplicate owners or losing links. Native user version checks reject stale saves, including metadata refresh racing a password change.

Latest validation (89d85141): backend test-project build passed with zero warnings/errors; 74 focused backend tests, 10 focused frontend auth tests, and frontend validation passed. The stale-email and OAuth 409 regressions failed before their fixes. CI at this head passed all four backend shards, frontend validation/tests, and image builds. Five browser shards passed; shard 1 reached a saved-view 404 and passed its automatic retry, so the flaky-test gate correctly failed the run. The trace shows a successful 201 create, an immediate 200 list omitting the new view, and a later list containing it. Saved-view creation/listing and their repository paths are unchanged from main and do not write user authentication state. This existing saved-view consistency race is outside this authentication change; the failed jobs are being rerun once. No test gate, timeout, or retry policy was changed.

API behavior: stale user writes can return 409. Unlink accepts canonical provider names only (422 otherwise); blank subject IDs retain their existing 400 response and body contract.

Deployment note: pre-version cached users expire normally; writes using those old snapshots may fail until their cache TTL expires.

Microsoft email ownership: this PR retains the existing email-matching and email-verification policy. Graph profile email is not a mailbox-ownership assertion; draft #2687, stacked on this branch, documents the concern and separately proposes Microsoft checks while preserving signup, established-identity sign-in, and authenticated linking. That Microsoft-only implementation does not depend on OAuth2 #163.

Verification and implementation details
  • Reconciled merged Replace Windows Live authentication with Microsoft and remove Facebook login #2403 with the canonical Microsoft provider and unlink validation, retained legacy-provider cleanup and last-usable-login protection (including duplicate identities), and preserved the generic 403 endpoint description. The frontend displays the new 409 failures; password-reset cancellation stays on the page when it fails.

  • A fresh email-owner read must still match the provider address before attachment. A handler regression reproduced a successful login after the email changed between lookup and admission; the request now returns 401 without changing the account or issuing a token.

  • Earlier validation on d99fc68df: 323 affected backend tests, frontend validation, and two browser runs without retries passed locally; full CI passed.

  • Existing account refresh preserves the same instance, merges supplied data, retains unspecified keys, and removes legacy duplicates so stale provider email metadata cannot survive in another copy. Authenticated relinking and anonymous OAuth login both refresh metadata while preserving the user's application email/name. Unit and real HTTP regressions cover both paths.

  • Plain IVersioned and its repository/serializer tests moved here from Protect user updates and clean up orphaned private views #2631 because OAuth refresh needs protection before that dependent PR lands. A native repository persistence hook performs a real password-change HTTP request before OAuth's save; both login modes return 409, preserve the new credentials, and retain a working replacement token. Elasticsearch supplies versions when legacy records are read; no migration, resave, cache repair, explicit interface wrapper, or user-specific serializer modifier is required. Existing cached records expire normally. See Foundatio versioning.

  • The original one-parameter cached email helper remains unchanged. OAuth admission uses a native uncached query: a canceled cached lookup can finish later and publish a miss hiding an existing owner. The real stale-negative-cache regression requires unchanged user count and the stored owner's token. Late cache publication is tracked in Foundatio.Repositories #323.

  • Identity lookup returns all exact ordinal provider/subject matches, with native pagination and ordinary cancellation checks. The handler rejects ambiguous ownership under identity/email/user locks; the repository never throws an authentication exception. Exact pairing is necessary because Elasticsearch flattens ordinary object arrays. Lock acquisition and admission reads have a 15-second cancellation deadline; admitted persistence and token issuance complete inside the locks. Integration tests use native repositories and locks; unavailable-lock injection remains in unit tests.

  • Standard request validation rejects invalid providers. Blank subjects are handled directly by the handler; the custom unlink response filter is removed. OpenAPI snapshots cover 409 responses on affected user writes. Forgot-password consumes version conflicts internally, returns the unknown-email success response, and sends no uncommitted reset token. Repository single/bulk deletes do not apply version preconditions; their existing API contract is retained.

  • CI already runs npm ci. A failing AppHost topology regression proved that redundant installation was still automatic; CI mode now uses the supported installer-disable option, while development retains installation and both modes wait for the API. A separate browser failure clicked a loading skeleton. The actual scenario reproduced this with a delayed stack-list response, passed with a selectable-row locator, then passed twice with normal responses and retries disabled. Temporary fault injection was removed. Timeouts, retries, flaky-test gates, required checks, and shard discovery are unchanged.

@niemyjski
niemyjski force-pushed the issue/external-account-state branch from ee3d4ec to f174a57 Compare October 2, 2026 04:10
@niemyjski
niemyjski added this pull request to stack #2642 October 5, 2026 02:11
@niemyjski
niemyjski force-pushed the issue/external-account-state branch from f174a57 to 92ee222 Compare October 13, 2026 02:39
@niemyjski
niemyjski force-pushed the issue/external-account-state branch from 92ee222 to c33052c Compare October 13, 2026 02:41
@niemyjski
niemyjski force-pushed the issue/external-account-state branch from c33052c to de6dba7 Compare October 13, 2026 02:58
@niemyjski
niemyjski force-pushed the issue/external-account-state branch from de6dba7 to ac8d008 Compare October 13, 2026 03:05
@niemyjski
niemyjski removed this pull request from stack #2642 October 13, 2026 03:05
@niemyjski
niemyjski added this pull request to stack #2659 October 13, 2026 03:05
@niemyjski
niemyjski force-pushed the issue/external-account-state branch from ac8d008 to 9b46d71 Compare October 13, 2026 03:27
@niemyjski
niemyjski removed this pull request from stack #2659 October 13, 2026 12:26
@niemyjski
niemyjski force-pushed the issue/external-account-state branch from 9b46d71 to eb0fab9 Compare October 13, 2026 12:26
@niemyjski
niemyjski added this pull request to stack #2660 October 13, 2026 12:27
@niemyjski
niemyjski force-pushed the issue/external-account-state branch from eb0fab9 to 6a6aa78 Compare October 13, 2026 12:40
@niemyjski
niemyjski force-pushed the issue/external-account-state branch from 6a6aa78 to c1a99bd Compare October 13, 2026 13:33
@niemyjski
niemyjski force-pushed the issue/external-account-state branch 2 times, most recently from 16e9543 to 1d13fe9 Compare October 14, 2026 02:46
@niemyjski
niemyjski force-pushed the issue/external-account-state branch from 1d13fe9 to 5258916 Compare October 14, 2026 02:50
@niemyjski
niemyjski force-pushed the issue/external-account-state branch 2 times, most recently from 38a41c7 to 77141f7 Compare October 14, 2026 03:16
@niemyjski
niemyjski force-pushed the issue/external-account-state branch from a7b95bf to ea8d962 Compare October 22, 2026 03:02

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea8d962b26

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Exceptionless.Web/Api/Handlers/AuthHandler.cs

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 869c91d029

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Exceptionless.Core/Extensions/UserExtensions.cs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d99fc68df6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Exceptionless.Core/Models/User.cs

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 89d85141a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Exceptionless.Web/Api/Handlers/AuthHandler.cs

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 217e1f87b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return;

bool isFirstUser = await userRepository.CountAsync() == 0;
bool isFirstUser = await userRepository.CountAsync().WaitAsync(cancellationToken) == 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Delay caching the first-user check until persistence

When the first account is created through OAuth and the request or 15-second admission deadline is canceled immediately after this count completes, AddGlobalAdminRoleIfFirstUserAsync has already set _isFirstUserChecked but CompleteExternalAccountAdmission aborts before the user is persisted. A subsequent successful signup in the same process then skips the check and creates the installation's first user without GlobalAdmin; after that, restarting cannot repair it because the count is nonzero. Publish or reset this process-wide flag only after the first user has been stored successfully.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Exceptionless.AppHost 70% 65% 128 ➖
Exceptionless.Core 78% 70% 10998 ✔
Exceptionless.Insulation 51% 43% 370 ➖
Exceptionless.Web 86% 72% 9483 ✔
Summary 81% (28935 / 35667) 70% (14456 / 20572) 20979 ✔

This branch has not been deployed

No deployments
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.

1 participant