Skip to content

fix: Clear persisted MSAL token cache on Disconnect-MgGraph#3649

Open
gavinbarron wants to merge 1 commit into
mainfrom
gavinbarron/fix-disconnect-clear-token-cache
Open

fix: Clear persisted MSAL token cache on Disconnect-MgGraph#3649
gavinbarron wants to merge 1 commit into
mainfrom
gavinbarron/fix-disconnect-clear-token-cache

Conversation

@gavinbarron

Copy link
Copy Markdown
Member

Summary

Fixes #3648

When Disconnect-MgGraph is called with ContextScope == CurrentUser, the persisted MSAL token cache on disk was not being cleared. This PR adds cache clearing using MsalCacheHelper.Clear() from the MSAL Extensions library.

Changes

  • Added explicit Microsoft.Identity.Client.Extensions.Msal v4.78.0 package reference to Authentication.Core.csproj
  • Added TokenCacheUtilities.cs — builds StorageCreationProperties matching Azure.Identity's internal cache layout (Windows DPAPI, macOS Keychain, Linux KeyRing) and clears both .cae and .nocae cache variants
  • Modified AuthenticationHelpers.LogoutAsync() — calls TokenCacheUtilities.ClearPersistedTokenCacheAsync() when ContextScope == CurrentUser, with non-fatal error handling (disconnect still succeeds if cache clear fails)
  • Added unit tests for the new behavior
  • Updated Disconnect-MgGraph documentation

Design Decisions

  • Non-fatal error handling: Cache clearing failure does not prevent disconnect from succeeding — it's wrapped in try/catch
  • Both CAE variants cleared: Azure.Identity creates separate .cae and .nocae cache files; both are cleared
  • Matches Azure.Identity internals: StorageCreationProperties are configured to match the exact storage layout Azure.Identity uses (directory, keychain service, keyring schema/attributes)

Testing

  • 3 new unit tests added (all pass on net8.0 and net472)
  • All existing 77 tests continue to pass on net8.0

When ContextScope is CurrentUser, Disconnect-MgGraph now clears the
disk-persisted MSAL token cache (both .cae and .nocae variants) using
MsalCacheHelper.Clear() from Microsoft.Identity.Client.Extensions.Msal.

Previously only the in-memory cache and auth record file were cleared,
leaving cached tokens on disk that could be reused without
re-authentication.

Fixes #3648

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@gavinbarron gavinbarron requested a review from a team as a code owner June 22, 2026 23:26

@jsquire jsquire 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.

This looks correct to me, though I don't have a deep understanding in the area. I've asked @g2vinay and @JonathanCrd to also review. I'd consider them to be the authoritative voices from the Azure.Identity perspective.

@ramsessanchez

Copy link
Copy Markdown
Contributor

Approving the changes, however, in line with @jsquire 's comments, we should await feedback from the folks he mentioned. In order to reduce the change of needing a patch release later on, lets await their sign-off and before merging and releasing.

@gavinbarron

Copy link
Copy Markdown
Member Author

Agreed

{
try
{
await TokenCacheUtilities.ClearPersistedTokenCacheAsync(Constants.CacheName).ConfigureAwait(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

on Windows, WAM is on by default for interactive sign-in, and WAM keeps the account + refresh token in its own cache. So cleaning this file won't actually sign the user out of WAM.

For the broker we would need to ask WAM to forget the account. Is the Windows Broker in scope for this fix?

Copilot suggested something like:

if (ShouldUseWam(authContext))
{
    var pca = PublicClientApplicationBuilder
        .Create(authContext.ClientId)
        .WithAuthority(GetAuthorityUrl(authContext))
        .WithBroker(new BrokerOptions(BrokerOptions.OperatingSystems.Windows))
        .Build();
    foreach (var account in await pca.GetAccountsAsync().ConfigureAwait(false))
        await pca.RemoveAsync(account).ConfigureAwait(false);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@JonathanCrd I think we probably need to clear the WAM token cache if possible to ensure a more complete fix.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Additionally, would using this approach avoid the need to have the TokenCacheUtilities class?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't think so, the broker only covers Windows, and there are non-broker auth flows on windows too.

@gavinbarron gavinbarron Jun 24, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@JonathanCrd Sorry, I wasn't very clear here I was more meaning the general approach of using a PCA instance to clear the cached tokens not that specific implementation. Maybe something like this to provide broad cover and eliminate the additional code that knows about the caching specifics

      var builder = PublicClientApplicationBuilder
              .Create(authContext.ClientId)
              .WithAuthority(GetAuthorityUrl(authContext));
      if (ShouldUseWam(authContext))
      {
          builder = builder
              .WithBroker(new BrokerOptions(BrokerOptions.OperatingSystems.Windows));
      }
      var pca = builder.Build();
      foreach (var account in await pca.GetAccountsAsync().ConfigureAwait(false))
          await pca.RemoveAsync(account).ConfigureAwait(false);

@g2vinay g2vinay Jun 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

TokenCacheUtilities -> ClearPersistedTokenCacheAsync still needs to be retained as it deletes the file based caches. PCA code above is needed on top to clear the WAM caches.

Just heads up
WAM is shared broker storage across Windows Azure tools. Removing accounts would also sign out:
Visual Studio
Azure CLI
Azure PowerShell
VS Code Azure extensions
Any app using Azure.Identity with broker enabled

This can impact UX for other logged in flows.

Comment on lines +62 to +63
#pragma warning disable CS0618 // MsalCacheHelper.Clear is obsolete but is the correct approach for full cache wipe on disconnect
cacheHelper.Clear();

@JonathanCrd JonathanCrd Jun 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why is Clear() the intentional approach here? This would wipe out the entire MSAL Cache, removing all accounts, not only the ones in the current context.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

My thinking here is that it would be roughly equivalent to the approach of iterating over the results from .GetAccounts() to remove each or them, how does this differ given that the cache files are managed per machine user?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I see. My comment was more about removing only the account being disconnected rather than all accounts, mainly from a UX perspective. That said, if there aren't any issues with clearing all accounts, then this approach works for me.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The CLI does not offer an account parameter, so any user account selection is done during the browser flow, which requires Disconnect-MgGraph to trigger the account selection again, so this feel like the right option. I suppose that there could be the case where a user has closed a terminal and is running Connect-MgGraph again triggering account selection.
But either way I'm comfortable with fully clearing out any cached tokens,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Msal cache scope is per window user, so .clear() will only scope to disconnecting user, I believe.

@JonathanCrd JonathanCrd 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.

Overall it looks good but I left a couple of comments about certain scenarios that might be missing and some questions too.

await TokenCacheUtilities.ClearPersistedTokenCacheAsync(Constants.CacheName).ConfigureAwait(false);
}
catch (Exception)
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Any actual failure here would be silent, could you add a log here so it's diagnosable?

@g2vinay

g2vinay commented Jun 24, 2026

Copy link
Copy Markdown

Looks fine to me, left my comments on the ongoing discussions.

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.

Disconnect-MgGraph does not clear the persisted MSAL token cache

5 participants