diff --git a/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj b/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj index 54f37452ac4..a1a4944e102 100644 --- a/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj +++ b/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj @@ -4,7 +4,7 @@ 9.0 netstandard2.0;net6.0;net472 Microsoft.Graph.PowerShell.Authentication.Core - 2.35.1 + 2.38.0 true @@ -20,6 +20,8 @@ + + diff --git a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs index e4448663d72..7d581c0205e 100644 --- a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs +++ b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs @@ -430,6 +430,18 @@ public static async Task LogoutAsync() { var authContext = GraphSession.Instance.AuthContext; GraphSession.Instance.InMemoryTokenCache?.ClearCache(); + if (authContext?.ContextScope == ContextScope.CurrentUser) + { + try + { + await TokenCacheUtilities.ClearPersistedTokenCacheAsync(Constants.CacheName).ConfigureAwait(false); + } + catch (Exception) + { + // Non-fatal: persisted cache clearing may fail on some platforms. + // The auth record and in-memory state are still cleared below. + } + } GraphSession.Instance.AuthContext = null; GraphSession.Instance.GraphHttpClient = null; await DeleteAuthRecordAsync().ConfigureAwait(false); diff --git a/src/Authentication/Authentication.Core/Utilities/TokenCacheUtilities.cs b/src/Authentication/Authentication.Core/Utilities/TokenCacheUtilities.cs new file mode 100644 index 00000000000..8f6fcc3f023 --- /dev/null +++ b/src/Authentication/Authentication.Core/Utilities/TokenCacheUtilities.cs @@ -0,0 +1,67 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using Microsoft.Identity.Client.Extensions.Msal; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.Graph.PowerShell.Authentication.Core.Utilities +{ + /// + /// Utilities for managing the MSAL token cache persisted to disk by Azure.Identity. + /// + internal static class TokenCacheUtilities + { + // Azure.Identity internal constants for cache storage configuration. + // See: Azure/azure-sdk-for-net - sdk/core/Azure.Core/src/Identity/Constants.cs + private const string DefaultCacheKeychainService = "Microsoft.Developer.IdentityService"; + private const string DefaultCacheKeyringSchema = "msal.cache"; + private const string DefaultCacheKeyringCollection = "default"; + private static readonly KeyValuePair DefaultCacheKeyringAttribute1 = + new KeyValuePair("MsalClientID", "Microsoft.Developer.IdentityService"); + private static readonly KeyValuePair DefaultCacheKeyringAttribute2 = + new KeyValuePair("Microsoft.Developer.IdentityService", "1.0.0.0"); + + // Azure.Identity appends CAE suffixes to the cache name internally. + private const string CaeEnabledSuffix = ".cae"; + private const string CaeDisabledSuffix = ".nocae"; + + private static readonly string DefaultCacheDirectory = + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + ".IdentityService"); + + /// + /// Clears the persisted MSAL token cache files created by Azure.Identity + /// for the given cache name. Clears both CAE-enabled and CAE-disabled variants. + /// + /// The cache name (e.g., "mg.msal.cache"). + public static async Task ClearPersistedTokenCacheAsync(string cacheName) + { + // Azure.Identity creates separate caches for CAE-enabled and CAE-disabled tokens. + await ClearCacheAsync(cacheName + CaeEnabledSuffix).ConfigureAwait(false); + await ClearCacheAsync(cacheName + CaeDisabledSuffix).ConfigureAwait(false); + } + + private static async Task ClearCacheAsync(string cacheFileName) + { + var storageProperties = new StorageCreationPropertiesBuilder(cacheFileName, DefaultCacheDirectory) + .WithMacKeyChain(DefaultCacheKeychainService, cacheFileName) + .WithLinuxKeyring( + DefaultCacheKeyringSchema, + DefaultCacheKeyringCollection, + cacheFileName, + DefaultCacheKeyringAttribute1, + DefaultCacheKeyringAttribute2) + .Build(); + + var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties).ConfigureAwait(false); +#pragma warning disable CS0618 // MsalCacheHelper.Clear is obsolete but is the correct approach for full cache wipe on disconnect + cacheHelper.Clear(); +#pragma warning restore CS0618 + } + } +} diff --git a/src/Authentication/Authentication.Test/TokenCache/TokenCacheUtilitiesTests.cs b/src/Authentication/Authentication.Test/TokenCache/TokenCacheUtilitiesTests.cs new file mode 100644 index 00000000000..a1cb20bc303 --- /dev/null +++ b/src/Authentication/Authentication.Test/TokenCache/TokenCacheUtilitiesTests.cs @@ -0,0 +1,80 @@ +using Microsoft.Graph.PowerShell.Authentication; +using Microsoft.Graph.PowerShell.Authentication.Core.TokenCache; +using Microsoft.Graph.PowerShell.Authentication.Core.Utilities; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Graph.Authentication.Test.TokenCache +{ + public class TokenCacheUtilitiesTests : IDisposable + { + public TokenCacheUtilitiesTests() + { + GraphSession.Initialize(() => new GraphSession()); + GraphSession.Instance.InMemoryTokenCache = new InMemoryTokenCache(); + } + + public void Dispose() + { + GraphSession.Reset(); + } + + [Fact] + public async Task LogoutAsyncShouldClearInMemoryCacheForProcessScope() + { + // Arrange + GraphSession.Instance.InMemoryTokenCache = new InMemoryTokenCache( + Encoding.UTF8.GetBytes("mockTokenData")); + GraphSession.Instance.AuthContext = new AuthContext + { + AuthType = AuthenticationType.UserProvidedAccessToken, + ContextScope = ContextScope.Process + }; + + // Act + var result = await AuthenticationHelpers.LogoutAsync(); + + // Assert + Assert.NotNull(result); + Assert.Equal(AuthenticationType.UserProvidedAccessToken, result.AuthType); + Assert.Null(GraphSession.Instance.AuthContext); + Assert.Null(GraphSession.Instance.GraphHttpClient); + Assert.Empty(GraphSession.Instance.InMemoryTokenCache.ReadTokenData()); + } + + [Fact] + public async Task LogoutAsyncShouldNotThrowWhenAuthContextIsNull() + { + // Arrange + GraphSession.Instance.AuthContext = null; + + // Act - should not throw even though there's no auth context + var result = await AuthenticationHelpers.LogoutAsync(); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task LogoutAsyncShouldAttemptCacheClearForCurrentUserScope() + { + // Arrange + GraphSession.Instance.AuthContext = new AuthContext + { + AuthType = AuthenticationType.UserProvidedAccessToken, + ContextScope = ContextScope.CurrentUser + }; + + // Act - should not throw even if no persisted cache exists on disk + var result = await AuthenticationHelpers.LogoutAsync(); + + // Assert + Assert.NotNull(result); + Assert.Equal(ContextScope.CurrentUser, result.ContextScope); + Assert.Null(GraphSession.Instance.AuthContext); + } + } +} diff --git a/src/Authentication/descriptions/Disconnect-MgGraph.md b/src/Authentication/descriptions/Disconnect-MgGraph.md index bf3a8afface..5964655e8fe 100644 --- a/src/Authentication/descriptions/Disconnect-MgGraph.md +++ b/src/Authentication/descriptions/Disconnect-MgGraph.md @@ -1 +1 @@ -Use Disconnect-MgGraph to sign out. \ No newline at end of file +Use Disconnect-MgGraph to sign out. This clears the persisted MSAL token cache from disk when using CurrentUser context scope, as well as removing the in-memory token cache and authentication record. \ No newline at end of file diff --git a/src/Authentication/docs/Disconnect-MgGraph.md b/src/Authentication/docs/Disconnect-MgGraph.md index 0414a17ed25..81acd8cb713 100644 --- a/src/Authentication/docs/Disconnect-MgGraph.md +++ b/src/Authentication/docs/Disconnect-MgGraph.md @@ -13,11 +13,11 @@ Once you're signed in, you'll remain signed in until you invoke Disconnect-MgGra ## SYNTAX ``` -Disconnect-MgGraph [] +Disconnect-MgGraph [-ProgressAction ] [] ``` ## DESCRIPTION -Use Disconnect-MgGraph to sign out. +Use Disconnect-MgGraph to sign out. This clears the persisted MSAL token cache from disk when using CurrentUser context scope, as well as removing the in-memory token cache and authentication record. ## EXAMPLES @@ -30,6 +30,21 @@ Use Disconnect-MgGraph to sign out. ## PARAMETERS +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).