Skip to content

Synchronize account and user token lifetimes - #2627

Merged
niemyjski merged 9 commits into
mainfrom
issue/user-session-generation
Oct 14, 2026
Merged

niemyjski merged 9 commits into
mainfrom
issue/user-session-generation

Conversation

@niemyjski

@niemyjski niemyjski commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Rotate authentication generations after password changes/resets and external-login removal/transfer, and reject user and OAuth tokens from an older generation. Authentication reads remain cached; existing users and tokens with matching null generations remain supported.

Scope: session lifecycle only. User repository concurrency, serializer changes and transfer compensation are in draft #2631.

Validation: the published revision builds with zero warnings/errors; all 71 focused tests pass. Complete CI is green, including API/OpenAPI, all six browser shards, client checks and Docker builds.

Remaining limitations: stale whole-user writes are addressed separately in #2631, and late cache publication is tracked in Foundatio.Repositories #323. This PR improves token revocation but does not guarantee immediate revocation through stale cached snapshots.

Verification and implementation details

Coverage includes legacy null generations, stale user/OAuth token rejection, password-change cleanup failures and external-login transfers using validated fixtures. The remaining production diff is 86 added and 14 removed lines across eight files; the rest is tests and the API snapshot. Local integration startup was previously blocked by Aspire's EnsureKubernetesAsync; hosted CI provides endpoint and OpenAPI verification. Password-change/reset and provider-transfer browser journeys have not been manually dogfooded on this revision.

@niemyjski
niemyjski force-pushed the issue/user-session-generation branch from 7bfd49d to 1cf38ce 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/user-session-generation branch 3 times, most recently from cd9081e to e31f3c9 Compare October 13, 2026 02:58
@niemyjski
niemyjski removed this pull request from stack #2642 October 13, 2026 03:05
@niemyjski
niemyjski force-pushed the issue/user-session-generation branch from e31f3c9 to 605b43d Compare October 13, 2026 03:05
@niemyjski
niemyjski changed the base branch from issue/versioned-password-records to main October 13, 2026 03:05
@niemyjski
niemyjski added this pull request to stack #2659 October 13, 2026 03:05
@niemyjski
niemyjski marked this pull request as ready for review October 13, 2026 03:08
@niemyjski niemyjski self-assigned this Oct 13, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T03:05:46.669584Z a178e7b New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 605b43dbf4

ℹ️ 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 Outdated
@niemyjski
niemyjski force-pushed the issue/user-session-generation branch from 605b43d to 0f3cc3a Compare October 13, 2026 03: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: 0f3cc3a1b1

ℹ️ 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: 8d643dffa5

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

/// This value does not describe the password hashing format.
/// </summary>
public ISet<string> OrganizationIds { get; init; } = new HashSet<string>();
public string? AuthenticationVersion { get; set; }

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 Prevent stale user saves from restoring revoked generations

When token cleanup fails—the case this generation is intended to protect—an already-authenticated request can retain the pre-rotation User and later overwrite this property through an unrelated whole-document save. For example, OrganizationHandler.AddModelAsync captures the request user before awaiting organization creation and then calls SaveAsync(user); if a password change completes between those operations, that save restores the old/null generation and makes the surviving old tokens valid again. Persist the generation separately, use partial updates for unrelated user mutations, or enforce optimistic concurrency so stale user documents cannot undo revocation.

AGENTS.md reference: AGENTS.md:L79-L82

Useful? React with 👍 / 👎.

@niemyjski
niemyjski removed this pull request from stack #2659 October 13, 2026 12:26
@niemyjski
niemyjski added this pull request to stack #2660 October 13, 2026 12:27

@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: aec2c07fb6

ℹ️ 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 on lines +139 to +140
if (!user.HasValidAuthenticationVersion(tokenRecord.AuthenticationVersion))
return AuthenticateResult.Fail("Token is not valid");

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 Prevent stale cache writes from restoring old generations

When token cleanup fails and an authentication request began an uncached user read before a password change, that request can publish the old user snapshot to the cache after SaveAsync has rotated the generation and invalidated the cache. This check then accepts the surviving old token on subsequent requests until the stale cache entry expires or is invalidated again, defeating the revocation fallback; validate the generation against a current read or make cache publication/invalidation ordering safe.

Useful? React with 👍 / 👎.

@niemyjski
niemyjski force-pushed the issue/user-session-generation branch 2 times, most recently from 956f021 to 9593b05 Compare October 14, 2026 02:38

@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: 956f021aab

ℹ️ 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 on lines +644 to +647
var destination = await userRepository.GetByIdAsync(destinationUserId);
if (destination is not null && destination.OAuthAccounts.Any(existing => String.Equals(existing.Provider, account.Provider, StringComparison.Ordinal)
&& String.Equals(existing.ProviderUserId, account.ProviderUserId, StringComparison.Ordinal)))
return;

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 Check every owner before restoring an external login

When another signed-in account links the same provider identity after it is removed from the previous owner but before the destination save fails, this check examines only the intended destination. The rollback can therefore restore the identity to currentOwner even though the third account now owns the same provider/provider-user-id pair, leaving duplicates that GetUserByOAuthProviderAsync resolves via an arbitrary FirstOrDefault; verify that no user currently owns the identity before restoring it.

AGENTS.md reference: AGENTS.md:L81-L81

Useful? React with 👍 / 👎.


try
{
await userRepository.PatchAsync(user.Id, new ActionPatch<User>(current => current.OrganizationIds.Add(organization.Id)), o => o.Cache());

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 Reject a false membership patch before returning created

If the authenticated user is deleted after the organization is added but before this patch executes, PatchAsync can return false rather than throw. The handler then skips compensation, updates only the stale request object, publishes UserMembershipChanged, and returns the newly created organization even though no persisted user belongs to it; treat a false result as failure and remove the organization just as the exception path does.

AGENTS.md reference: AGENTS.md:L81-L81

Useful? React with 👍 / 👎.

@niemyjski
niemyjski force-pushed the issue/user-session-generation branch from 9593b05 to 2ef6152 Compare October 14, 2026 02:38

@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: 2ef6152178

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

{
if (!user.OrganizationIds.Contains(organization.Id))
{
await userRepository.PatchAsync(user.Id, new ActionPatch<User>(current => current.OrganizationIds.Add(organization.Id)), o => o.Cache());

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 Handle a missing user before publishing membership

If the user is deleted after GetByEmailAddressAsync but before this patch, PatchAsync returns false; the handler nevertheless mutates the stale object, publishes UserMembershipChanged, sends the organization-added email, and returns success even though no persisted user joined. Check the result and treat a missing destination as failure or create an invite instead.

AGENTS.md reference: AGENTS.md:L81-L81

Useful? React with 👍 / 👎.

Comment thread src/Exceptionless.Core/Models/User.cs Outdated
namespace Exceptionless.Core.Models;

public record User : IIdentity, IHaveDates, IValidatableObject
public record User : IIdentity, IHaveDates, IVersioned, IValidatableObject

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 Declare conflicts for all versioned user-save endpoints

Making User versioned causes concurrent whole-user saves to surface as HTTP 409, but the OpenAPI changes cover only auth routes. For example, the organization membership tests now expect a conflict from POST /organizations/{id}/users/{email}, while OrganizationEndpoints.cs and the snapshot still omit that response; avatar, email-address, and verification saves have the same undocumented outcome. Add 409 metadata and regenerate the snapshot for every newly affected endpoint.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

@niemyjski
niemyjski force-pushed the issue/user-session-generation branch from 4222af7 to a178e7b Compare October 14, 2026 03:03
@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Exceptionless.AppHost 23% 23% 128 ❌
Exceptionless.Core 77% 69% 10881 ✔
Exceptionless.Insulation 51% 43% 370 ➖
Exceptionless.Web 86% 71% 9189 ✔
Summary 80% (28141 / 35117) 69% (13912 / 20148) 20568 ✔

@niemyjski
niemyjski merged commit 8bc518b into main Oct 14, 2026
22 checks passed
@niemyjski
niemyjski deleted the issue/user-session-generation branch October 14, 2026 03:16
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