Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<LangVersion>9.0</LangVersion>
<TargetFrameworks>netstandard2.0;net6.0;net472</TargetFrameworks>
<RootNamespace>Microsoft.Graph.PowerShell.Authentication.Core</RootNamespace>
<Version>2.35.1</Version>
<Version>2.38.0</Version>
<!-- Suppress .NET Target Framework Moniker (TFM) Support Build Warnings -->
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
</PropertyGroup>
Expand All @@ -20,6 +20,8 @@
<PackageReference Include="Azure.Identity.Broker" Version="1.4.0" />
<!-- Explicitly reference Microsoft.Identity.Client to ensure form_post support -->
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
<!-- Explicitly reference MSAL Extensions to clear persisted token cache on disconnect -->
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.3" />
<PackageReference Include="Microsoft.Graph.Core" Version="4.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,18 @@ public static async Task<IAuthContext> LogoutAsync()
{
var authContext = GraphSession.Instance.AuthContext;
GraphSession.Instance.InMemoryTokenCache?.ClearCache();
if (authContext?.ContextScope == ContextScope.CurrentUser)
{
try
{
await TokenCacheUtilities.ClearPersistedTokenCacheAsync(Constants.CacheName).ConfigureAwait(false);
Comment thread
gavinbarron marked this conversation as resolved.
Outdated
}
catch (Exception)
{
Comment thread
gavinbarron marked this conversation as resolved.
// 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Utilities for managing the MSAL token cache persisted to disk by Azure.Identity.
/// </summary>
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<string, string> DefaultCacheKeyringAttribute1 =
new KeyValuePair<string, string>("MsalClientID", "Microsoft.Developer.IdentityService");
private static readonly KeyValuePair<string, string> DefaultCacheKeyringAttribute2 =
new KeyValuePair<string, string>("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");

/// <summary>
/// Clears the persisted MSAL token cache files created by Azure.Identity
/// for the given cache name. Clears both CAE-enabled and CAE-disabled variants.
/// </summary>
/// <param name="cacheName">The cache name (e.g., "mg.msal.cache").</param>
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();
Comment thread
gavinbarron marked this conversation as resolved.
#pragma warning restore CS0618
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
2 changes: 1 addition & 1 deletion src/Authentication/descriptions/Disconnect-MgGraph.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
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.
19 changes: 17 additions & 2 deletions src/Authentication/docs/Disconnect-MgGraph.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ Once you're signed in, you'll remain signed in until you invoke Disconnect-MgGra
## SYNTAX

```
Disconnect-MgGraph [<CommonParameters>]
Disconnect-MgGraph [-ProgressAction <ActionPreference>] [<CommonParameters>]
```

## 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

Expand All @@ -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).

Expand Down
Loading