Skip to content

Handle MSAL token cache persistence failures gracefully (WSL support)#6079

Open
lewing wants to merge 7 commits into
dotnet:mainfrom
lewing:fix/darc-wsl-keyring-fallback
Open

Handle MSAL token cache persistence failures gracefully (WSL support)#6079
lewing wants to merge 7 commits into
dotnet:mainfrom
lewing:fix/darc-wsl-keyring-fallback

Conversation

@lewing
Copy link
Copy Markdown
Member

@lewing lewing commented Mar 10, 2026

Summary

darc login and all authenticated darc commands fail hard on WSL when the D-Bus secrets service (org.freedesktop.secrets / gnome-keyring) is unavailable. MSAL's VerifyPersistence() throws MsalCachePersistenceException before any authentication can occur, with no recovery.

Fixes #6060

Changes

CachedInteractiveBrowserCredential.cs

  • Catch MsalCachePersistenceException in GetToken/GetTokenAsync (not just in CacheAuthenticationRecord) and recreate credentials without TokenCachePersistenceOptions (in-memory only token cache)
  • After persistence fallback, if interactive auth also fails, throw CredentialUnavailableException so ChainedTokenCredential can try the next credential
  • Extract RecreateCredentialsWithoutPersistence() and IsMsalCachePersistenceException() as shared helpers (previously the recreation was inline and the exception check was a local function)

AppCredential.cs

  • Chain AzureCliCredential after the interactive credential in CreateUserCredential() via ChainedTokenCredential, so users with az login can authenticate without interactive flows when the keyring is unavailable

Behavior

Scenario Before After
WSL, no keyring, no az login Hard crash with MsalCachePersistenceException Falls back to in-memory cache, prompts device code
WSL, no keyring, az login active Hard crash Falls back to AzureCliCredential silently
Normal desktop (keyring available) Works No change — keyring used as before
User cancels interactive auth Error shown Error shown (NOT wrapped as CredentialUnavailable)

Security Notes

  • Dropping TokenCachePersistenceOptions means tokens are held in-memory only when the keyring is unavailable — this is more secure (no persistence), at the cost of re-authentication per session
  • AzureCliCredential reuses the user's existing az login session — no new credential storage introduced
  • The CredentialUnavailableException wrapping is scoped narrowly: only when the persistence fallback path also fails at interactive auth, not on all AuthenticationFailedException

When the D-Bus secrets service (gnome-keyring) is unavailable — common in
WSL environments — MSAL's VerifyPersistence() throws
MsalCachePersistenceException before any authentication can occur, causing
darc login and all authenticated commands to fail hard.

Changes:
- CachedInteractiveBrowserCredential: catch MsalCachePersistenceException
  in GetToken/GetTokenAsync and recreate credentials without
  TokenCachePersistenceOptions (in-memory only). Extract shared
  RecreateCredentialsWithoutPersistence() and IsMsalCachePersistenceException()
  helpers.
- AppCredential: chain AzureCliCredential after the interactive credential
  via ChainedTokenCredential so users with 'az login' can authenticate
  without interactive flows when the keyring is unavailable.

Fixes dotnet#6060

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 10, 2026 15:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Improves darc authentication reliability on WSL by handling MSAL token cache persistence failures (e.g., missing org.freedesktop.secrets) and introducing an Azure CLI-based fallback path.

Changes:

  • Catch MsalCachePersistenceException during token acquisition and fall back to non-persistent (in-memory) credentials.
  • Convert certain post-fallback interactive failures into CredentialUnavailableException so credential chaining can continue.
  • Add AzureCliCredential into the user credential chain to enable az login reuse.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/Maestro/Maestro.Common/AppCredentials/CachedInteractiveBrowserCredential.cs Adds persistence-failure handling around GetToken/GetTokenAsync, refactors helpers for recreating credentials and detecting persistence exceptions.
src/Maestro/Maestro.Common/AppCredentials/AppCredential.cs Wraps the interactive credential in a ChainedTokenCredential with AzureCliCredential as an additional option.

Comment thread src/Maestro/Maestro.Common/AppCredentials/AppCredential.cs Outdated
…lation

- RecreateCredentialsWithoutPersistence now preserves _options.AuthenticationRecord
  to avoid extra consent/device-code prompts when an auth record exists on disk
- Retry path exception filter now checks cancellationToken.IsCancellationRequested
  and inner OperationCanceledException to let user-initiated cancellations propagate
  instead of wrapping them as CredentialUnavailableException
- Add explanatory comment on ChainedTokenCredential ordering rationale

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread src/Maestro/Maestro.Common/AppCredentials/AppCredential.cs Outdated
premun
premun previously approved these changes Mar 11, 2026
@premun
Copy link
Copy Markdown
Member

premun commented Mar 11, 2026

LGTM

@lewing did you test this?

@lewing
Copy link
Copy Markdown
Member Author

lewing commented Mar 11, 2026

LGTM

@lewing did you test this?

partially, I can do a full test before merging though.

@premun
Copy link
Copy Markdown
Member

premun commented Mar 12, 2026

Please run one if you have the environment ready.
It's easy to run darc - you just dotnet build the csproj and then run it. Building it should just work out of the box even in WSL if you have .NET 10.

lewing and others added 4 commits May 21, 2026 17:32
DeviceCodeCredential's default callback writes the device-code prompt
to AzureEventSource, which is not routed to console output in CLI
hosts (like darc) or MCP server processes. Result: when MSAL
persistence is unavailable on WSL and the credential falls back to
device-code, the user sees "falling back to device code
authentication..." and then darc hangs silently — the device code and
URL are emitted to a stream nobody is watching.

Set DeviceCodeCallback on both DeviceCodeCredential constructions
(initial + post-persistence-fallback) to route the message through the
existing _logger, so the user sees "To sign in, use a web browser to
open https://microsoft.com/devicelogin and enter the code XXX...".

Tested by building darc locally on WSL without keyring; "darc login"
now prints the device code instead of hanging.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After RecreateCredentialsWithoutPersistence() rebuilds both credentials,
CacheAuthenticationRecord retries Authenticate(). The retry was
re-entering the InteractiveBrowserCredential path even though the first
attempt had already proved the browser flow was unavailable in this
environment (the _isDeviceCodeFallback flag was set, but only
GetTokenCore checked it — Authenticate did not).

On headless WSL the browser credential opens a Windows-side browser
but the OAuth redirect to http://localhost:XXXX cannot reach the
WSL-side listener, so the second InteractiveBrowserCredential.Authenticate
call hangs forever. The device code prompt never gets a chance to
print.

Honor _isDeviceCodeFallback at the top of Authenticate so the second
call goes straight to DeviceCodeCredential. With this fix, "darc login"
on WSL prints the device code + URL immediately on the retry instead of
hanging.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On WSL the interactive browser flow only succeeds when:
  1. A Windows-side launcher is available (wslu's `wslview` is the
     canonical bridge), and
  2. WSL2 localhost forwarding can route the OAuth redirect back into
     the WSL network namespace (default NAT mode handles this).

When wslview is present the flow works end-to-end and benefits from
Windows session SSO (which is what gets past Conditional Access).
When wslview is absent, xdg-open silently does nothing and
InteractiveBrowserCredential.Authenticate() blocks forever waiting for
a redirect that will never arrive — in that specific case we skip
straight to device code so the user at least sees a code instead of
an indefinite hang.

Env var overrides:
  DARC_FORCE_BROWSER_AUTH=1  always attempt browser flow
  DARC_USE_DEVICE_CODE=1     always skip browser flow

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lewing lewing force-pushed the fix/darc-wsl-keyring-fallback branch from 6c8a602 to 9088d98 Compare May 21, 2026 23:52
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.

darc login fails on WSL when gnome-keyring/D-Bus secrets service is unavailable

3 participants