-
Notifications
You must be signed in to change notification settings - Fork 17
/
MSALClientHelper.cs
272 lines (231 loc) · 11.7 KB
/
MSALClientHelper.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;
using Microsoft.IdentityModel.Abstractions;
using System.Diagnostics;
#if WINDOWS
using Microsoft.Identity.Client.Desktop;
#endif
namespace SignInMaui.MSALClient
{
/// <summary>
/// Contains methods that initialize and use the MSAL SDK
/// </summary>
public class MSALClientHelper
{
/// <summary>
/// As for the Tenant, you can use a name as obtained from the azure portal, e.g. kko365.onmicrosoft.com"
/// </summary>
public AzureAdConfig AzureAdConfig;
/// <summary>
/// Gets the authentication result (if available) from MSAL's various operations.
/// </summary>
/// <value>
/// The authentication result.
/// </value>
public AuthenticationResult AuthResult { get; private set; }
/// <summary>
/// Gets the MSAL public client application instance.
/// </summary>
/// <value>
/// The public client application.
/// </value>
public IPublicClientApplication PublicClientApplication { get; private set; }
/// <summary>
/// This will determine if the Interactive Authentication should be Embedded or System view
/// </summary>
public bool UseEmbedded { get; set; } = false;
/// <summary>
/// The PublicClientApplication builder used internally
/// </summary>
private PublicClientApplicationBuilder PublicClientApplicationBuilder;
// Token Caching setup - Mac
public static readonly string KeyChainServiceName = "Contoso.MyProduct";
public static readonly string KeyChainAccountName = "MSALCache";
public static readonly KeyValuePair<string, string> LinuxKeyRingAttr2 = new KeyValuePair<string, string>("ProductGroup", "Contoso");
private static string PCANotInitializedExceptionMessage = "The PublicClientApplication needs to be initialized before calling this method. Use InitializePublicClientAppAsync() to initialize.";
/// <summary>
/// Initializes a new instance of the <see cref="MSALClientHelper"/> class.
/// </summary>
public MSALClientHelper(AzureAdConfig azureAdConfig)
{
AzureAdConfig = azureAdConfig;
this.InitializePublicClientApplicationBuilder();
}
/// <summary>
/// Initializes the MSAL's PublicClientApplication builder from config.
/// </summary>
/// <autogeneratedoc />
private void InitializePublicClientApplicationBuilder()
{
this.PublicClientApplicationBuilder = PublicClientApplicationBuilder.Create(AzureAdConfig.ClientId)
.WithExperimentalFeatures() // this is for upcoming logger
.WithAuthority(string.Format(AzureAdConfig.Authority, AzureAdConfig.TenantId))
.WithLogging(new IdentityLogger(EventLogLevel.Warning), enablePiiLogging: false) // This is the currently recommended way to log MSAL message. For more info refer to https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/logging. Set Identity Logging level to Warning which is a middle ground
.WithIosKeychainSecurityGroup("com.microsoft.adalcache");
}
/// <summary>
/// Initializes the public client application of MSAL.NET with the required information to correctly authenticate the user.
/// </summary>
/// <returns></returns>
public async Task<IAccount> InitializePublicClientAppAsync()
{
// Initialize the MSAL library by building a public client application
this.PublicClientApplication = this.PublicClientApplicationBuilder
.WithRedirectUri($"msal{PublicClientSingleton.Instance.MSALClientHelper.AzureAdConfig.ClientId}://auth")
#if WINDOWS
.WithWindowsEmbeddedBrowserSupport()
#endif
.Build();
await AttachTokenCache();
return await FetchSignedInUserFromCache().ConfigureAwait(false);
}
/// <summary>
/// Attaches the token cache to the Public Client app.
/// </summary>
/// <returns>IAccount list of already signed-in users (if available)</returns>
private async Task<IEnumerable<IAccount>> AttachTokenCache()
{
if (DeviceInfo.Current.Platform != DevicePlatform.WinUI)
{
return null;
}
// Cache configuration and hook-up to public application. Refer to https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/wiki/Cross-platform-Token-Cache#configuring-the-token-cache
var storageProperties = new StorageCreationPropertiesBuilder(AzureAdConfig.CacheFileName, AzureAdConfig.CacheDir)
.Build();
var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties);
msalcachehelper.RegisterCache(PublicClientApplication.UserTokenCache);
// If the cache file is being reused, we'd find some already-signed-in accounts
return await PublicClientApplication.GetAccountsAsync().ConfigureAwait(false);
}
/// <summary>
/// Signs in the user and obtains an Access token for a provided set of scopes
/// </summary>
/// <param name="scopes"></param>
/// <returns> Access Token</returns>
public async Task<string> SignInUserAndAcquireAccessToken(string[] scopes)
{
Exception<NullReferenceException>.ThrowOn(() => this.PublicClientApplication == null, PCANotInitializedExceptionMessage);
var existingUser = await FetchSignedInUserFromCache().ConfigureAwait(false);
try
{
// 1. Try to sign-in the previously signed-in account
if (existingUser != null)
{
this.AuthResult = await this.PublicClientApplication
.AcquireTokenSilent(scopes, existingUser)
.ExecuteAsync()
.ConfigureAwait(false);
}
else
{
this.AuthResult = await SignInUserInteractivelyAsync(scopes);
}
}
catch (MsalUiRequiredException ex)
{
// A MsalUiRequiredException happened on AcquireTokenSilentAsync. This indicates you need to call AcquireTokenInteractive to acquire a token interactively
Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
this.AuthResult = await this.PublicClientApplication
.AcquireTokenInteractive(scopes)
.ExecuteAsync()
.ConfigureAwait(false);
}
catch (MsalException msalEx)
{
Debug.WriteLine($"Error Acquiring Token interactively:{Environment.NewLine}{msalEx}");
}
return this.AuthResult.AccessToken;
}
/// <summary>
/// Signs the in user and acquire access token for a provided set of scopes.
/// </summary>
/// <param name="scopes">The scopes.</param>
/// <param name="extraclaims">The extra claims, usually from CAE. We basically handle CAE by sending the user back to Azure AD for
/// additional processing and requesting a new access token for Graph</param>
/// <returns></returns>
public async Task<String> SignInUserAndAcquireAccessToken(string[] scopes, string extraclaims)
{
Exception<NullReferenceException>.ThrowOn(() => this.PublicClientApplication == null, PCANotInitializedExceptionMessage);
try
{
// Send the user to Azure AD for re-authentication as a silent acquisition wont resolve any CAE scenarios like an extra claims request
this.AuthResult = await this.PublicClientApplication.AcquireTokenInteractive(scopes)
.WithClaims(extraclaims)
.ExecuteAsync()
.ConfigureAwait(false);
}
catch (MsalException msalEx)
{
Debug.WriteLine($"Error Acquiring Token:{Environment.NewLine}{msalEx}");
}
return this.AuthResult.AccessToken;
}
/// <summary>
/// Shows a pattern to sign-in a user interactively in applications that are input constrained and would need to fall-back on device code flow.
/// </summary>
/// <param name="scopes">The scopes.</param>
/// <param name="existingAccount">The existing account.</param>
/// <returns></returns>
public async Task<AuthenticationResult> SignInUserInteractivelyAsync(string[] scopes, IAccount existingAccount = null)
{
Exception<NullReferenceException>.ThrowOn(() => this.PublicClientApplication == null, PCANotInitializedExceptionMessage);
if (this.PublicClientApplication == null)
throw new NullReferenceException();
if (this.PublicClientApplication.IsUserInteractive())
{
return await this.PublicClientApplication.AcquireTokenInteractive(scopes)
.WithParentActivityOrWindow(PlatformConfig.Instance.ParentWindow)
.ExecuteAsync()
.ConfigureAwait(false);
}
// If the operating system does not have UI (e.g. SSH into Linux), you can fallback to device code, however this
// flow will not satisfy the "device is managed" CA policy.
return await this.PublicClientApplication.AcquireTokenWithDeviceCode(scopes, (dcr) =>
{
Console.WriteLine(dcr.Message);
return Task.CompletedTask;
}).ExecuteAsync().ConfigureAwait(false);
}
/// <summary>
/// Removes the first signed-in user's record from token cache
/// </summary>
public async Task SignOutUserAsync()
{
var existingUser = await FetchSignedInUserFromCache().ConfigureAwait(false);
await this.SignOutUserAsync(existingUser).ConfigureAwait(false);
}
/// <summary>
/// Removes a given user's record from token cache
/// </summary>
/// <param name="user">The user.</param>
public async Task SignOutUserAsync(IAccount user)
{
if (this.PublicClientApplication == null) return;
await this.PublicClientApplication.RemoveAsync(user).ConfigureAwait(false);
}
/// <summary>
/// Fetches the signed in user from MSAL's token cache (if available).
/// </summary>
/// <returns></returns>
public async Task<IAccount> FetchSignedInUserFromCache()
{
Exception<NullReferenceException>.ThrowOn(() => this.PublicClientApplication == null, PCANotInitializedExceptionMessage);
// get accounts from cache
IEnumerable<IAccount> accounts = await this.PublicClientApplication.GetAccountsAsync();
// Error corner case: we should always have 0 or 1 accounts, not expecting > 1
// This is just an example of how to resolve this ambiguity, which can arise if more apps share a token cache.
// Note that some apps prefer to use a random account from the cache.
if (accounts.Count() > 1)
{
foreach (var acc in accounts)
{
await this.PublicClientApplication.RemoveAsync(acc);
}
return null;
}
return accounts.SingleOrDefault();
}
}
}