Skip to content

Commit a6ce9a4

Browse files
authored
Merge pull request #216 from microsoftgraph/po/multiPlatformTokenCache
Multi-Platform Token Cache
2 parents 9a45911 + 88329b7 commit a6ce9a4

File tree

15 files changed

+1037
-74
lines changed

15 files changed

+1037
-74
lines changed

.azure-pipelines/validate-pr-auth-module.yml

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,18 @@ pr:
88
include:
99
- dev
1010
- master
11+
- milestone/*
1112
paths:
1213
include:
1314
- src/Authentication/*
1415
trigger: none
1516

1617
jobs:
17-
- job: MSGraphPSSDKValidation
18-
displayName: MS Graph PS SDK Auth Validation
18+
- job: MSGraphPSSDKValidation_Windows
19+
displayName: MS Graph PS SDK Auth Validation - Windows
1920
timeoutInMinutes: 300
2021
pool:
21-
name: Microsoft Graph
22-
demands: 'Agent.Name -equals Local-Agent'
23-
22+
vmImage: 'windows-latest'
2423
steps:
2524
- task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2
2625
displayName: 'Run CredScan'
@@ -41,6 +40,50 @@ jobs:
4140
projects: '$(System.DefaultWorkingDirectory)/src/Authentication/Authentication.Test/*.csproj'
4241
testRunTitle: 'Run Enabled Tests'
4342

43+
- task: YodLabs.O365PostMessage.O365PostMessageBuild.O365PostMessageBuild@0
44+
displayName: 'Graph Client Tooling pipeline fail notification'
45+
inputs:
46+
addressType: serviceEndpoint
47+
serviceEndpointName: 'microsoftgraph pipeline status'
48+
title: '$(Build.DefinitionName) failure notification'
49+
text: 'This pipeline has failed. View the build details for further information. This is a blocking failure. '
50+
condition: and(failed(), ne(variables['Build.Reason'], 'Manual'))
51+
enabled: true
52+
53+
- job: MSGraphPSSDKValidation_Linux
54+
displayName: MS Graph PS SDK Auth Validation - Linux
55+
pool:
56+
vmImage: 'ubuntu-latest'
57+
steps:
58+
- task: DotNetCoreCLI@2
59+
displayName: 'Run Enabled Tests'
60+
inputs:
61+
command: 'test'
62+
projects: '$(System.DefaultWorkingDirectory)/src/Authentication/Authentication.Test/*.csproj'
63+
testRunTitle: 'Run Enabled Tests'
64+
65+
- task: YodLabs.O365PostMessage.O365PostMessageBuild.O365PostMessageBuild@0
66+
displayName: 'Graph Client Tooling pipeline fail notification'
67+
inputs:
68+
addressType: serviceEndpoint
69+
serviceEndpointName: 'microsoftgraph pipeline status'
70+
title: '$(Build.DefinitionName) failure notification'
71+
text: 'This pipeline has failed. View the build details for further information. This is a blocking failure. '
72+
condition: and(failed(), ne(variables['Build.Reason'], 'Manual'))
73+
enabled: true
74+
75+
- job: MSGraphPSSDKValidation_MacOS
76+
displayName: MS Graph PS SDK Auth Validation - MacOS
77+
pool:
78+
vmImage: 'macOS-latest'
79+
steps:
80+
- task: DotNetCoreCLI@2
81+
displayName: 'Run Enabled Tests'
82+
inputs:
83+
command: 'test'
84+
projects: '$(System.DefaultWorkingDirectory)/src/Authentication/Authentication.Test/*.csproj'
85+
testRunTitle: 'Run Enabled Tests'
86+
4487
- task: YodLabs.O365PostMessage.O365PostMessageBuild.O365PostMessageBuild@0
4588
displayName: 'Graph Client Tooling pipeline fail notification'
4689
inputs:

.azure-pipelines/validate-pr-beta-modules.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pr:
88
include:
99
- master
1010
- dev
11+
- milestone/*
1112
paths:
1213
include:
1314
- src/Beta/*
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
namespace Microsoft.Graph.Authentication.Test.TokenCache
2+
{
3+
using Microsoft.Graph.PowerShell.Authentication.TokenCache;
4+
using System;
5+
using System.Text;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Xunit;
9+
10+
public class TokenCacheStorageTests: IDisposable
11+
{
12+
private const string TestAppId1 = "test_app_id_1";
13+
14+
[Fact]
15+
public void ShouldStoreNewTokenToPlatformCache()
16+
{
17+
// Arrange
18+
string strContent = "random data for app.";
19+
byte[] bufferToStore = Encoding.UTF8.GetBytes(strContent);
20+
21+
// Act
22+
TokenCacheStorage.SetToken(TestAppId1, bufferToStore);
23+
24+
// Assert
25+
byte[] storedBuffer = TokenCacheStorage.GetToken(TestAppId1);
26+
Assert.Equal(bufferToStore.Length, storedBuffer.Length);
27+
Assert.Equal(strContent, Encoding.UTF8.GetString(storedBuffer));
28+
29+
// Cleanup
30+
CleanTokenCache(TestAppId1);
31+
}
32+
33+
[Fact]
34+
public void ShouldStoreMultipleAppTokensInPlatformCache()
35+
{
36+
// Arrange
37+
string app1StrContent = "random data for app 1.";
38+
byte[] app1BufferToStore = Encoding.UTF8.GetBytes(app1StrContent);
39+
40+
string TestAppId2 = "test_app_id_2";
41+
string app2StrContent = "random data for app 2 plus more data.";
42+
byte[] app2BufferToStore = Encoding.UTF8.GetBytes(app2StrContent);
43+
44+
// Act
45+
TokenCacheStorage.SetToken(TestAppId1, app1BufferToStore);
46+
TokenCacheStorage.SetToken(TestAppId2, app2BufferToStore);
47+
48+
// Assert
49+
byte[] app1StoredBuffer = TokenCacheStorage.GetToken(TestAppId1);
50+
Assert.Equal(app1BufferToStore.Length, app1StoredBuffer.Length);
51+
Assert.Equal(app1StrContent, Encoding.UTF8.GetString(app1StoredBuffer));
52+
53+
byte[] app2StoredBuffer = TokenCacheStorage.GetToken(TestAppId2);
54+
Assert.Equal(app2BufferToStore.Length, app2StoredBuffer.Length);
55+
Assert.Equal(app2StrContent, Encoding.UTF8.GetString(app2StoredBuffer));
56+
57+
// Cleanup
58+
CleanTokenCache(TestAppId1);
59+
CleanTokenCache(TestAppId2);
60+
}
61+
62+
63+
[Fact]
64+
public void ShouldUpdateTokenInPlatformCache()
65+
{
66+
// Arrange
67+
string originalStrContent = "random data for app.";
68+
byte[] originalBuffer = Encoding.UTF8.GetBytes(originalStrContent);
69+
TokenCacheStorage.SetToken(TestAppId1, originalBuffer);
70+
71+
// Act
72+
string strContentToUpdate = "updated random data for app.";
73+
byte[] updateBuffer = Encoding.UTF8.GetBytes(strContentToUpdate);
74+
TokenCacheStorage.SetToken(TestAppId1, updateBuffer);
75+
76+
// Assert
77+
byte[] storedBuffer = TokenCacheStorage.GetToken(TestAppId1);
78+
Assert.NotEqual(originalBuffer.Length, storedBuffer.Length);
79+
Assert.Equal(updateBuffer.Length, storedBuffer.Length);
80+
Assert.Equal(strContentToUpdate, Encoding.UTF8.GetString(storedBuffer));
81+
82+
// Cleanup
83+
CleanTokenCache(TestAppId1);
84+
}
85+
86+
[Fact]
87+
public void ShouldReturnNoContentWhenPlatformCacheIsEmpty()
88+
{
89+
// Arrange
90+
CleanTokenCache(TestAppId1);
91+
92+
// Act
93+
byte[] storedBuffer = TokenCacheStorage.GetToken(TestAppId1);
94+
95+
// Assert
96+
Assert.Empty(storedBuffer);
97+
}
98+
99+
[Fact]
100+
public void ShouldDeleteCache()
101+
{
102+
// Arrange
103+
string originalStrContent = "random data for app.";
104+
byte[] originalBuffer = Encoding.UTF8.GetBytes(originalStrContent);
105+
TokenCacheStorage.SetToken(TestAppId1, originalBuffer);
106+
107+
// Act
108+
TokenCacheStorage.DeleteToken(TestAppId1);
109+
110+
// Assert
111+
byte[] storedBuffer = TokenCacheStorage.GetToken(TestAppId1);
112+
Assert.Empty(storedBuffer);
113+
}
114+
115+
116+
[Fact]
117+
public void ShouldMakeParallelCallsToTokenCache()
118+
{
119+
// Arrange
120+
int executions = 50;
121+
int count = 0;
122+
bool failed = false;
123+
124+
// Act
125+
Parallel.For(0, executions, (index) => {
126+
byte[] contentBuffer = Encoding.UTF8.GetBytes(index.ToString());
127+
TokenCacheStorage.SetToken($"{index}", contentBuffer);
128+
129+
byte[] storedBuffer = TokenCacheStorage.GetToken(index.ToString());
130+
if (index.ToString() != Encoding.UTF8.GetString(storedBuffer))
131+
{
132+
failed = true;
133+
}
134+
135+
CleanTokenCache(index.ToString());
136+
Interlocked.Increment(ref count);
137+
});
138+
139+
// Assert
140+
Assert.Equal(executions, count);
141+
Assert.False(failed, "Unexpected content found.");
142+
}
143+
144+
public void Dispose()
145+
{
146+
CleanTokenCache(TestAppId1);
147+
}
148+
149+
private void CleanTokenCache(string appId)
150+
{
151+
TokenCacheStorage.DeleteToken(appId);
152+
}
153+
}
154+
}

src/Authentication/Authentication/Constants.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ public static class Constants
1313
internal const string UserParameterSet = "UserParameterSet";
1414
internal const string AppParameterSet = "AppParameterSet";
1515
internal const int MaxDeviceCodeTimeOut = 120; // 2 mins timeout.
16-
internal const string UserCacheFileName = "userTokenCache.bin3";
17-
internal const string AppCacheFileName = "appTokenCache.bin3";
1816
internal static readonly string TokenCacheDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".graph");
17+
internal const string TokenCacheServiceName = "com.microsoft.graph.powershell.sdkcache";
1918
}
2019
}

src/Authentication/Authentication/ErrorConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ internal static class Message
2020
internal const string InvalidJWT = "Invalid JWT access token.";
2121
internal const string MissingAuthContext = "Authentication needed, call Connect-Graph.";
2222
internal const string InstanceExists = "An instance of {0} already exists. Call {1} to overwrite it.";
23+
internal const string NullOrEmptyParameter = "Parameter '{0}' cannot be null or empty.";
24+
internal const string MacKeyChainFailed = "{0} failed with result code {1}.";
2325
}
2426
}
2527
}

src/Authentication/Authentication/Helpers/AuthenticationHelpers.cs

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal static IAuthenticationProvider GetAuthProvider(IAuthContext authConfig)
2424
.WithTenantId(authConfig.TenantId)
2525
.Build();
2626

27-
ConfigureTokenCache(publicClientApp.UserTokenCache, Constants.UserCacheFileName);
27+
ConfigureTokenCache(publicClientApp.UserTokenCache, authConfig.ClientId);
2828
return new DeviceCodeProvider(publicClientApp, authConfig.Scopes, async (result) => {
2929
await Console.Out.WriteLineAsync(result.Message);
3030
});
@@ -37,7 +37,7 @@ internal static IAuthenticationProvider GetAuthProvider(IAuthContext authConfig)
3737
.WithCertificate(string.IsNullOrEmpty(authConfig.CertificateThumbprint) ? GetCertificateByName(authConfig.CertificateName) : GetCertificateByThumbprint(authConfig.CertificateThumbprint))
3838
.Build();
3939

40-
ConfigureTokenCache(confidentialClientApp.AppTokenCache, Constants.AppCacheFileName);
40+
ConfigureTokenCache(confidentialClientApp.AppTokenCache, authConfig.ClientId);
4141
return new ClientCredentialProvider(confidentialClientApp);
4242
}
4343
}
@@ -46,37 +46,24 @@ internal static void Logout(IAuthContext authConfig)
4646
{
4747
lock (FileLock)
4848
{
49-
if (authConfig.AuthType == AuthenticationType.Delegated)
50-
File.Delete(Path.Combine(Constants.TokenCacheDirectory, Constants.UserCacheFileName));
51-
else
52-
File.Delete(Path.Combine(Constants.TokenCacheDirectory, Constants.AppCacheFileName));
49+
TokenCacheStorage.DeleteToken(authConfig.ClientId);
5350
}
5451
}
5552

56-
private static void ConfigureTokenCache(ITokenCache tokenCache, string tokenCacheFile)
53+
private static void ConfigureTokenCache(ITokenCache tokenCache, string appId)
5754
{
58-
if (!Directory.Exists(Constants.TokenCacheDirectory))
59-
Directory.CreateDirectory(Constants.TokenCacheDirectory);
60-
61-
string tokenCacheFilePath = Path.Combine(Constants.TokenCacheDirectory, tokenCacheFile);
62-
6355
tokenCache.SetBeforeAccess((TokenCacheNotificationArgs args) => {
6456
lock (FileLock)
6557
{
66-
args.TokenCache.DeserializeMsalV3(File.Exists(tokenCacheFilePath)
67-
? TokenCryptographer.DecryptToken(File.ReadAllBytes(tokenCacheFilePath))
68-
: null,
69-
shouldClearExistingCache: true);
58+
args.TokenCache.DeserializeMsalV3(TokenCacheStorage.GetToken(appId), shouldClearExistingCache: true);
7059
}
7160
});
7261

7362
tokenCache.SetAfterAccess((TokenCacheNotificationArgs args) => {
7463
lock (FileLock)
7564
{
7665
if (args.HasStateChanged)
77-
{
78-
File.WriteAllBytes(tokenCacheFilePath, TokenCryptographer.EncryptToken(args.TokenCache.SerializeMsalV3()));
79-
}
66+
TokenCacheStorage.SetToken(appId, args.TokenCache.SerializeMsalV3());
8067
}
8168
});
8269
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
namespace Microsoft.Graph.PowerShell.Authentication.Helpers
6+
{
7+
using System.Runtime.InteropServices;
8+
9+
internal static class OperatingSystem
10+
{
11+
/// <summary>
12+
/// Detects if the platform we are running on is Windows.
13+
/// </summary>
14+
public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
15+
16+
/// <summary>
17+
/// Detects if the platform we are running on is MacOS.
18+
/// </summary>
19+
public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
20+
21+
/// <summary>
22+
/// Detects if the platform we are running on is Linux.
23+
/// </summary>
24+
public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
25+
}
26+
}

src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
RootModule = './Microsoft.Graph.Authentication.psm1'
1313

1414
# Version number of this module.
15-
ModuleVersion = '0.5.1'
15+
ModuleVersion = '0.7.0'
1616

1717
# Supported PSEditions
1818
CompatiblePSEditions = 'Core', 'Desktop'

0 commit comments

Comments
 (0)