Skip to content

fix(auth): preserve organization invitations through login - #2528

Open
niemyjski wants to merge 4 commits into
mainfrom
feature/invitation-auth-flow
Open

fix(auth): preserve organization invitations through login#2528
niemyjski wants to merge 4 commits into
mainfrom
feature/invitation-auth-flow

Conversation

@niemyjski

@niemyjski niemyjski commented Aug 25, 2026

Copy link
Copy Markdown
Member

Summary

  • forwards invitation tokens through password and OAuth login/signup flows
  • permits an invited OAuth user to create an account when general account creation is disabled
  • keeps the selected invited organization stable after acceptance
  • derives the organization users and billing pages' organization id from the route param so deep links cannot render a stale organization's data
  • adds backend, frontend unit, and end-to-end coverage for invitation acceptance, including rejection of invalid invite tokens when account creation is disabled

The invitation email already links to /signup?token=... on main. This PR fixes the independent acceptance gap discovered while auditing the email modernization work; it contains no email-rendering changes.

Verification

  • dotnet test tests/Exceptionless.Tests/Exceptionless.Tests.csproj -- --filter-class Exceptionless.Tests.Api.Endpoints.AuthEndpointTests (49 passed)
  • npm run test:unit -- src/lib/features/auth/api.test.ts (2 passed)
  • targeted Prettier and ESLint checks on all changed frontend files
  • npm run check (0 errors, 0 warnings)
  • git diff --check

Post-Deploy Monitoring & Validation

Verify both an existing user and a new OAuth user can accept an organization invitation from the tokenized signup link, including when public account creation is disabled. Confirm the invite is consumed and the invited organization is selected after authentication.

@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: 3baf39f1f9

ℹ️ 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".

onSubmit: LoginSchema,
onSubmitAsync: async ({ value }) => {
const response = await login(value.email, value.password);
const response = await login(value.email, value.password, value.invite_token);

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 Select the invited organization after login

When an existing user has another valid organization persisted in organization.current, this call consumes the invitation but the success path still navigates to the default /; the app layout preserves any currently valid selection, so the invited organization is not selected. The same problem affects invitation-based OAuth signup, which redirects to the add-project page and can consequently default a new project to the user's previous organization. Return or resolve the invited organization and select it before continuing.

AGENTS.md reference: AGENTS.md:L72-L75

Useful? React with 👍 / 👎.

import { toast } from 'svelte-sonner';

const organizationId = organization.current!;
const organizationId = page.params.organizationId || organization.current!;

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 Make the route organization ID reactive

When client-side navigation changes only [organizationId] while this users page remains mounted, this plain const retains the old parameter. The parent layout and heading switch to the new organization, but the users query, invite mutation, and table options continue targeting the previous organization, so an administrator can view or modify the wrong membership list. Derive the ID reactively from page.params instead.

AGENTS.md reference: src/Exceptionless.Web/ClientApp/AGENTS.md:L21-L23

Useful? React with 👍 / 👎.

@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: 14799e817d

ℹ️ 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".

const response = await client.postJSON<TokenResult>(`auth/${options.provider}`, {
clientId: options.clientId,
code: data.code,
inviteToken: options.inviteToken,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep invitation OAuth separate from account linking

When a user opens an invitation while already signed in, the shared fetch client sends the existing access token with this OAuth request. AuthHandler.FromExternalLoginAsync therefore enters its authenticated account-linking branch (lines 566-585) instead of logging in the OAuth user; if the selected OAuth identity belongs to another Exceptionless account, it is removed from that account and attached to the current one, after which the forwarded invitation is also granted to the current account. This can lock the intended account out and consume its invitation, so invitation login must omit the current authentication or otherwise distinguish login from account linking.

Useful? React with 👍 / 👎.

Deep-linking an organization users URL while a different organization was previously selected captured the stale persisted id at component init, querying the wrong organization's users under the current route. Derive the id from the route param and thread it through table options as a getter so columns rebuild with the live value.
Billing and invoice queries read the persisted selected-organization store instead of the route, so cold deep-links fetched the wrong organization's billing data before the layout effect synced the store.
…valid invites

Renames the new e2e test to match the sentence-case suite convention and adds backend coverage asserting an invalid invite token is forbidden when account creation is disabled and no user is created.
@niemyjski
niemyjski force-pushed the feature/invitation-auth-flow branch from 14799e8 to 18072df Compare August 26, 2026 02:28

@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: 18072df35a

ℹ️ 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".


const defaultRedirect = resolve('/');
const redirectUrl = getSafeRedirectUrl(page.url.searchParams.get('redirect'), defaultRedirect);
const inviteToken = page.url.searchParams.get('token');

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 Keep the invitation token reactive

When client-side navigation or a popstate transition changes only the login route's token query parameter, this plain constant retains the previous token. The signup links and form therefore continue submitting that stale token, which can consume the wrong organization invitation; bind the query parameter through the shared reactive query-parameter utility and keep the form value synchronized.

AGENTS.md reference: src/Exceptionless.Web/ClientApp/AGENTS.md:L27-L27

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Exceptionless.Insulation 37% 35% 286
Exceptionless.Core 75% 67% 10267
Exceptionless.Web 85% 69% 7972
Exceptionless.AppHost 38% 41% 147
Summary 78% (25348 / 32357) 67% (11933 / 17696) 18672

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