Skip to content

entra: serialize interactive sign-in across GCM processes - #2471

Open
cveld wants to merge 1 commit into
git-ecosystem:mainfrom
cveld:poc/serialize-entra-interactive-auth
Open

cveld wants to merge 1 commit into
git-ecosystem:mainfrom
cveld:poc/serialize-entra-interactive-auth

Conversation

@cveld

@cveld cveld commented Oct 13, 2026

Copy link
Copy Markdown

Problem

When a cached token expires, every GCM process that needs it (one per Git worktree, one per parallel fetch, an IDE autofetch, ...) falls back to interactive authentication on its own. They share one MSAL token cache, so one sign-in is enough, but after it completes the other processes never look at the cache again. They keep prompting or waiting. In one environment a single token expiry left 189 stray git fetch / git-credential-manager get processes behind.

Discussed in #1555 (see my two comments there) and related to #2288.

Change

In EntraAuthentication.GetTokenForUserAsync, after silent authentication fails and interaction is allowed:

  1. Take a lock file entra-signin-<hash>.lock in the GCM data directory (hash of client ID, authority and cache type).
  2. Once the lock is held, resolve the account again and retry silent authentication, so a token stored by another process is used without a second prompt.
  3. Otherwise run the interactive/broker flow as before, holding the lock until it finishes.

Waiting honours cancellation and gives up after 5 minutes. If the lock cannot be taken, authentication continues exactly as before. A file handle is used rather than a named mutex (thread affinity across await); the OS releases it if the process exits.

Verification

  • 7 unit tests for the lock (EntraAuthenticationInteractiveLockTests).
  • Full dotnet test run on Windows: everything passes except 6 Trace2MessageTests.BuildTimeSpan cases that fail on a Dutch locale (decimal comma) on main as well; they pass with DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1.
  • Four parallel get processes for an account missing from the cache (GCM_MSAUTH_FLOW=devicecode): main starts 4 device-code flows, this branch starts 1 and the other 3 wait.

Not verified

  • A real sign-in completing and the waiters picking the token up from the cache.
  • Broker and browser flows (only device code was exercised), macOS and Linux.
  • The wait timeout and what waiters should do if the first user cancels are my own choices and open for discussion.

Opened as a draft: the maintainers have said serialization is planned for 3.0, so this is meant as a concrete proposal to react to.

🤖 Generated with Claude Code

When a cached token expires, every GCM process that needs it (for
example one per Git worktree, or one per parallel fetch) falls back to
interactive authentication on its own. They share one MSAL token cache,
so only one sign-in is needed, but after it completes the other
processes never look at the cache again and keep prompting or waiting.

Take a per-client, per-authority lock file in the GCM data directory
before starting an interactive or broker flow. Once the lock is held,
resolve the account again and retry silent authentication, so a token
stored by another process is used without a second prompt. Waiting
honours cancellation and gives up after five minutes. If the lock
cannot be taken, authentication continues as before.

Verified with four parallel `get` requests against an expired account:
before, four device-code flows started; now one starts and three wait.

Assisted-by: Claude Sonnet 5.5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cveld
cveld marked this pull request as ready for review October 13, 2026 11:46
@cveld
cveld requested a review from a team as a code owner October 13, 2026 11:46
@becm

becm commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

As demonstrated in initial post of #1555, this is an issue for all providers during parallel requests.

Some auth paths would need to keep a lock until the completion ot the store phase (2nd process invocation).
Or the updated token may not yet be available in time for parallel requests
(Hack to keep the update within a single GCM invocation: Ditch Git convention and update during get).

@cveld

cveld commented Nov 3, 2026

Copy link
Copy Markdown
Author

Thanks for looking, and agreed that the underlying problem is not specific to Entra. This PR is intentionally the narrow variant: it only covers flows that go through EntraAuthentication (Azure Repos and other Entra-based providers), with the lock held inside a single get invocation.

On the store phase: for a known account the token is already in the shared MSAL token cache when the lock is released, so a waiting process retries silent authentication against that cache and does not depend on Git's later store call. What store does for Azure Repos here is only record the org -> user binding, not the token. My measurement for this case was four parallel get requests with an expired token: four device-code flows on main, one flow plus three waiters on this branch.

You are right about the case without a binding, though. Because the binding is only recorded in store, a first sign-in for an organization has no account, and the silent retry after taking the lock is skipped (GetTokenForUserSilentAsync returns early without an account). I tested this (four parallel get requests, no binding, device-code flow, sandboxed Git config): all four processes prompted, one after another. So the lock helps for an expired token with a known account, but not for a first sign-in. I will state this limitation in the PR description.

Closing that gap needs something that spans get -> store: a lease with an owner and a timeout, and handling for the case where Git never calls store (so other processes do not hang). It probably should not be Entra-only either. I would rather not grow this PR for that. If it is useful, I can write up a proposal in #1555 so we can agree on a direction first.

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.

2 participants