From 72db8edccf00d900effafc2f041d963511d82631 Mon Sep 17 00:00:00 2001 From: Jean-Marc Prieur Date: Mon, 6 Jul 2020 13:56:30 +0200 Subject: [PATCH 1/3] Updating to MSAL.NET 4.16 --- WebApp/Controllers/HomeController.cs | 14 ++-- WebApp/MailApp.csproj | 4 +- WebApp/Utils/MSALAppMemoryTokenCache.cs | 30 +++----- WebApp/Utils/MSALPerUserMemoryTokenCache.cs | 84 ++++++--------------- WebApp/Utils/MsalAppBuilder.cs | 11 +-- WebApp/Web.config | 4 +- WebApp/packages.config | 2 +- 7 files changed, 47 insertions(+), 102 deletions(-) diff --git a/WebApp/Controllers/HomeController.cs b/WebApp/Controllers/HomeController.cs index 71a4ee7..60cbe1a 100644 --- a/WebApp/Controllers/HomeController.cs +++ b/WebApp/Controllers/HomeController.cs @@ -48,13 +48,13 @@ public async Task SendMail() // Before we render the send email screen, we use the incremental consent to obtain and cache the access token with the correct scopes IConfidentialClientApplication app = MsalAppBuilder.BuildConfidentialClientApplication(); AuthenticationResult result = null; - var accounts = await app.GetAccountsAsync(); + var account = await app.GetAccountAsync(ClaimsPrincipal.Current.GetMsalAccountId()); string[] scopes = { "Mail.Send" }; try { // try to get an already cached token - result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync().ConfigureAwait(false); + result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync().ConfigureAwait(false); } catch (MsalUiRequiredException ex) { @@ -108,7 +108,7 @@ public async Task SendMail(string recipient, string subject, strin string message = String.Format(messagetemplate, subject, body, recipient); HttpClient client = new HttpClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "https://graph.microsoft.com/v1.0/me/microsoft.graph.sendMail") + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "https://graph.microsoft.com/v1.0/me/sendMail") { Content = new StringContent(message, Encoding.UTF8, "application/json") }; @@ -116,13 +116,13 @@ public async Task SendMail(string recipient, string subject, strin IConfidentialClientApplication app = MsalAppBuilder.BuildConfidentialClientApplication(); AuthenticationResult result = null; - var accounts = await app.GetAccountsAsync(); + var account = await app.GetAccountAsync(ClaimsPrincipal.Current.GetMsalAccountId()); string[] scopes = { "Mail.Send" }; try { // try to get an already cached token - result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync().ConfigureAwait(false); + result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync().ConfigureAwait(false); } catch (Exception ex) { @@ -158,13 +158,13 @@ public async Task ReadMail() { IConfidentialClientApplication app = MsalAppBuilder.BuildConfidentialClientApplication(); AuthenticationResult result = null; - var accounts = await app.GetAccountsAsync(); + var account = await app.GetAccountAsync(ClaimsPrincipal.Current.GetMsalAccountId()); string[] scopes = { "Mail.Read" }; try { // try to get token silently - result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync().ConfigureAwait(false); + result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync().ConfigureAwait(false); } catch (MsalUiRequiredException) { diff --git a/WebApp/MailApp.csproj b/WebApp/MailApp.csproj index 34e2685..704c71c 100644 --- a/WebApp/MailApp.csproj +++ b/WebApp/MailApp.csproj @@ -47,8 +47,8 @@ ..\packages\Antlr.3.5.0.2\lib\Antlr3.Runtime.dll - - ..\packages\Microsoft.Identity.Client.4.7.1\lib\net45\Microsoft.Identity.Client.dll + + ..\packages\Microsoft.Identity.Client.4.16.0\lib\net45\Microsoft.Identity.Client.dll ..\packages\Microsoft.IdentityModel.JsonWebTokens.5.4.0\lib\net461\Microsoft.IdentityModel.JsonWebTokens.dll diff --git a/WebApp/Utils/MSALAppMemoryTokenCache.cs b/WebApp/Utils/MSALAppMemoryTokenCache.cs index e417e17..2ecdc9f 100644 --- a/WebApp/Utils/MSALAppMemoryTokenCache.cs +++ b/WebApp/Utils/MSALAppMemoryTokenCache.cs @@ -35,11 +35,6 @@ namespace WebApp.Utils /// public class MSALAppMemoryTokenCache { - /// - /// The application cache key - /// - internal readonly string AppCacheId; - /// /// The backing MemoryCache instance /// @@ -55,11 +50,8 @@ public class MSALAppMemoryTokenCache /// Initializes a new instance of the class. /// /// The client's instance of the token cache. - /// The application's id (Client ID). - public MSALAppMemoryTokenCache(ITokenCache tokenCache, string clientId) + public MSALAppMemoryTokenCache(ITokenCache tokenCache) { - AppCacheId = clientId + "_AppTokenCache"; - tokenCache.SetBeforeAccess(AppTokenCacheBeforeAccessNotification); tokenCache.SetAfterAccess(AppTokenCacheAfterAccessNotification); tokenCache.SetBeforeWrite(AppTokenCacheBeforeWriteNotification); @@ -74,11 +66,6 @@ private void AppTokenCacheBeforeWriteNotification(TokenCacheNotificationArgs arg // Since we are using a MemoryCache ,whose methods are threads safe, we need not to do anything in this handler. } - public void Clear() - { - memoryCache.Remove(AppCacheId); - } - /// /// Triggered right before MSAL needs to access the cache. Reload the cache from the persistence store in case it changed since the last access. /// @@ -86,7 +73,7 @@ public void Clear() private void AppTokenCacheBeforeAccessNotification(TokenCacheNotificationArgs args) { // Load the token cache from memory - byte[] tokenCacheBytes = (byte[])this.memoryCache.Get(AppCacheId); + byte[] tokenCacheBytes = (byte[])this.memoryCache.Get(args.SuggestedCacheKey); args.TokenCache.DeserializeMsalV3(tokenCacheBytes, shouldClearExistingCache: true); } @@ -99,9 +86,16 @@ private void AppTokenCacheAfterAccessNotification(TokenCacheNotificationArgs arg // if the access operation resulted in a cache update if (args.HasStateChanged) { - // Reflect changes in the persistence store - this.memoryCache.Set(AppCacheId, args.TokenCache.SerializeMsalV3(), cacheDuration); - + if (args.HasTokens) + { + // No tokens => remove the entry in the cache + this.memoryCache.Remove(args.SuggestedCacheKey); + } + else + { + // Reflect changes in the persistence store + this.memoryCache.Set(args.SuggestedCacheKey, args.TokenCache.SerializeMsalV3(), cacheDuration); + } } } } diff --git a/WebApp/Utils/MSALPerUserMemoryTokenCache.cs b/WebApp/Utils/MSALPerUserMemoryTokenCache.cs index 4963008..ad1ba38 100644 --- a/WebApp/Utils/MSALPerUserMemoryTokenCache.cs +++ b/WebApp/Utils/MSALPerUserMemoryTokenCache.cs @@ -46,67 +46,31 @@ public class MSALPerUserMemoryTokenCache /// private readonly DateTimeOffset cacheDuration = DateTimeOffset.Now.AddHours(48); - /// - /// Once the user signes in, this will not be null and can be obtained via a call to Thread.CurrentPrincipal - /// - internal ClaimsPrincipal SignedInUser; - /// /// Initializes a new instance of the class. /// /// The client's instance of the token cache. public MSALPerUserMemoryTokenCache(ITokenCache tokenCache) { - Initialize(tokenCache, ClaimsPrincipal.Current); - } - - /// - /// Initializes a new instance of the class. - /// - /// The client's instance of the token cache. - /// The signed-in user for whom the cache needs to be established. - public MSALPerUserMemoryTokenCache(ITokenCache tokenCache, ClaimsPrincipal user) - { - Initialize(tokenCache, user); + Initialize(tokenCache); } /// Initializes the cache instance /// The ITokenCache passed through the constructor /// The signed-in user for whom the cache needs to be established.. - private void Initialize(ITokenCache tokenCache, ClaimsPrincipal user) + private void Initialize(ITokenCache tokenCache) { - SignedInUser = user; - tokenCache.SetBeforeAccess(UserTokenCacheBeforeAccessNotification); tokenCache.SetAfterAccess(UserTokenCacheAfterAccessNotification); tokenCache.SetBeforeWrite(UserTokenCacheBeforeWriteNotification); - - if (SignedInUser == null) - { - // No users signed in yet, so we return - return; - } - } - - /// - /// Explores the Claims of a signed-in user (if available) to populate the unique Id of this cache's instance. - /// - /// The signed in user's object.tenant Id , if available in the ClaimsPrincipal.Current instance - internal string GetMsalAccountId() - { - if (SignedInUser != null) - { - return SignedInUser.GetMsalAccountId(); - } - return null; } /// /// Clears the TokenCache's copy of this user's cache. /// - public void Clear() + public void Clear(string key) { - memoryCache.Remove(GetMsalAccountId()); + memoryCache.Remove(key); } /// @@ -115,18 +79,24 @@ public void Clear() /// Contains parameters used by the MSAL call accessing the cache. private void UserTokenCacheAfterAccessNotification(TokenCacheNotificationArgs args) { - SetSignedInUserFromNotificationArgs(args); - // if the access operation resulted in a cache update if (args.HasStateChanged) { - string cacheKey = GetMsalAccountId(); + string cacheKey = args.SuggestedCacheKey ?? args.Account?.HomeAccountId?.Identifier; - if (string.IsNullOrWhiteSpace(cacheKey)) - return; + if (args.HasTokens) + { - // Ideally, methods that load and persist should be thread safe.MemoryCache.Get() is thread safe. - this.memoryCache.Set(cacheKey, args.TokenCache.SerializeMsalV3(), cacheDuration); + if (string.IsNullOrWhiteSpace(cacheKey)) + return; + + // Ideally, methods that load and persist should be thread safe.MemoryCache.Get() is thread safe. + this.memoryCache.Set(cacheKey, args.TokenCache.SerializeMsalV3(), cacheDuration); + } + else + { + this.memoryCache.Remove(cacheKey); + } } } @@ -136,10 +106,11 @@ private void UserTokenCacheAfterAccessNotification(TokenCacheNotificationArgs ar /// Contains parameters used by the MSAL call accessing the cache. private void UserTokenCacheBeforeAccessNotification(TokenCacheNotificationArgs args) { - string cacheKey = GetMsalAccountId(); - - if (string.IsNullOrWhiteSpace(cacheKey)) + string cacheKey = args.SuggestedCacheKey ?? args.Account?.HomeAccountId?.Identifier; + if (string.IsNullOrEmpty(cacheKey)) + { return; + } byte[] tokenCacheBytes = (byte[])this.memoryCache.Get(cacheKey); args.TokenCache.DeserializeMsalV3(tokenCacheBytes, shouldClearExistingCache: true); @@ -153,18 +124,5 @@ private void UserTokenCacheBeforeWriteNotification(TokenCacheNotificationArgs ar { // Since we are using a MemoryCache ,whose methods are threads safe, we need not to do anything in this handler. } - - /// - /// To keep the cache, ClaimsPrincipal and Sql in sync, we ensure that the user's object Id we obtained by MSAL after - /// successful sign-in is set as the key for the cache. - /// - /// Contains parameters used by the MSAL call accessing the cache. - private void SetSignedInUserFromNotificationArgs(TokenCacheNotificationArgs args) - { - if (SignedInUser == null && args.Account != null) - { - SignedInUser = args.Account.ToClaimsPrincipal(); - } - } } } \ No newline at end of file diff --git a/WebApp/Utils/MsalAppBuilder.cs b/WebApp/Utils/MsalAppBuilder.cs index e80d2d1..2746474 100644 --- a/WebApp/Utils/MsalAppBuilder.cs +++ b/WebApp/Utils/MsalAppBuilder.cs @@ -32,11 +32,6 @@ namespace WebApp.Utils public static class MsalAppBuilder { public static IConfidentialClientApplication BuildConfidentialClientApplication() - { - return BuildConfidentialClientApplication(ClaimsPrincipal.Current); - } - - public static IConfidentialClientApplication BuildConfidentialClientApplication(ClaimsPrincipal currentUser) { IConfidentialClientApplication clientapp = ConfidentialClientApplicationBuilder.Create(AuthenticationConfig.ClientId) .WithClientSecret(AuthenticationConfig.ClientSecret) @@ -44,9 +39,8 @@ public static IConfidentialClientApplication BuildConfidentialClientApplication( .WithAuthority(new Uri(AuthenticationConfig.Authority)) .Build(); - // After the ConfidentialClientApplication is created, we overwrite its default UserTokenCache with our implementation - MSALPerUserMemoryTokenCache userTokenCache = new MSALPerUserMemoryTokenCache(clientapp.UserTokenCache, currentUser ?? ClaimsPrincipal.Current); - + // After the ConfidentialClientApplication is created, we overwrite its default UserTokenCache serialization with our implementation + MSALPerUserMemoryTokenCache userTokenCache = new MSALPerUserMemoryTokenCache(clientapp.UserTokenCache); return clientapp; } @@ -63,7 +57,6 @@ public static async Task ClearUserTokenCache() var userAccount = await clientapp.GetAccountAsync(ClaimsPrincipal.Current.GetMsalAccountId()); await clientapp.RemoveAsync(userAccount); - userTokenCache.Clear(); } } } \ No newline at end of file diff --git a/WebApp/Web.config b/WebApp/Web.config index a0da73d..6a0a49e 100644 --- a/WebApp/Web.config +++ b/WebApp/Web.config @@ -9,8 +9,8 @@ - - + + diff --git a/WebApp/packages.config b/WebApp/packages.config index cad237b..23935a4 100644 --- a/WebApp/packages.config +++ b/WebApp/packages.config @@ -7,7 +7,7 @@ - + From 1f726ee5465fdad167ea5ca5e3d512972643ea3c Mon Sep 17 00:00:00 2001 From: Jean-Marc Prieur Date: Mon, 6 Jul 2020 14:30:55 +0200 Subject: [PATCH 2/3] Update --- WebApp/App_Start/Startup.Auth.cs | 2 +- WebApp/Web.config | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WebApp/App_Start/Startup.Auth.cs b/WebApp/App_Start/Startup.Auth.cs index f99db39..bcd55c5 100644 --- a/WebApp/App_Start/Startup.Auth.cs +++ b/WebApp/App_Start/Startup.Auth.cs @@ -66,7 +66,7 @@ public void ConfigureAuth(IAppBuilder app) private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context) { // Upon successful sign in, get the access token & cache it using MSAL - IConfidentialClientApplication clientApp = MsalAppBuilder.BuildConfidentialClientApplication(new ClaimsPrincipal(context.AuthenticationTicket.Identity)); + IConfidentialClientApplication clientApp = MsalAppBuilder.BuildConfidentialClientApplication(); AuthenticationResult result = await clientApp.AcquireTokenByAuthorizationCode(new[] { "Mail.Read" }, context.Code).ExecuteAsync(); } diff --git a/WebApp/Web.config b/WebApp/Web.config index 6a0a49e..a0da73d 100644 --- a/WebApp/Web.config +++ b/WebApp/Web.config @@ -9,8 +9,8 @@ - - + + From 65e03f9a051ae654aa3044d40e8dbe590c5dbe04 Mon Sep 17 00:00:00 2001 From: Jean-Marc Prieur Date: Tue, 7 Jul 2020 12:19:08 +0200 Subject: [PATCH 3/3] Updating to benefit from the latest version of MSAL --- WebApp/Utils/MSALPerUserMemoryTokenCache.cs | 20 +++++--------------- WebApp/Utils/MsalAppBuilder.cs | 6 ++++-- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/WebApp/Utils/MSALPerUserMemoryTokenCache.cs b/WebApp/Utils/MSALPerUserMemoryTokenCache.cs index ad1ba38..9d21d8c 100644 --- a/WebApp/Utils/MSALPerUserMemoryTokenCache.cs +++ b/WebApp/Utils/MSALPerUserMemoryTokenCache.cs @@ -65,14 +65,6 @@ private void Initialize(ITokenCache tokenCache) tokenCache.SetBeforeWrite(UserTokenCacheBeforeWriteNotification); } - /// - /// Clears the TokenCache's copy of this user's cache. - /// - public void Clear(string key) - { - memoryCache.Remove(key); - } - /// /// Triggered right after MSAL accessed the cache. /// @@ -82,20 +74,18 @@ private void UserTokenCacheAfterAccessNotification(TokenCacheNotificationArgs ar // if the access operation resulted in a cache update if (args.HasStateChanged) { - string cacheKey = args.SuggestedCacheKey ?? args.Account?.HomeAccountId?.Identifier; - + string cacheKey = args.SuggestedCacheKey; if (args.HasTokens) { - if (string.IsNullOrWhiteSpace(cacheKey)) return; // Ideally, methods that load and persist should be thread safe.MemoryCache.Get() is thread safe. - this.memoryCache.Set(cacheKey, args.TokenCache.SerializeMsalV3(), cacheDuration); + memoryCache.Set(cacheKey, args.TokenCache.SerializeMsalV3(), cacheDuration); } else { - this.memoryCache.Remove(cacheKey); + memoryCache.Remove(cacheKey); } } } @@ -106,13 +96,13 @@ private void UserTokenCacheAfterAccessNotification(TokenCacheNotificationArgs ar /// Contains parameters used by the MSAL call accessing the cache. private void UserTokenCacheBeforeAccessNotification(TokenCacheNotificationArgs args) { - string cacheKey = args.SuggestedCacheKey ?? args.Account?.HomeAccountId?.Identifier; + string cacheKey = args.SuggestedCacheKey; if (string.IsNullOrEmpty(cacheKey)) { return; } - byte[] tokenCacheBytes = (byte[])this.memoryCache.Get(cacheKey); + byte[] tokenCacheBytes = (byte[])memoryCache.Get(cacheKey); args.TokenCache.DeserializeMsalV3(tokenCacheBytes, shouldClearExistingCache: true); } diff --git a/WebApp/Utils/MsalAppBuilder.cs b/WebApp/Utils/MsalAppBuilder.cs index 2746474..209bdbb 100644 --- a/WebApp/Utils/MsalAppBuilder.cs +++ b/WebApp/Utils/MsalAppBuilder.cs @@ -55,8 +55,10 @@ public static async Task ClearUserTokenCache() // We only clear the user's tokens. MSALPerUserMemoryTokenCache userTokenCache = new MSALPerUserMemoryTokenCache(clientapp.UserTokenCache); var userAccount = await clientapp.GetAccountAsync(ClaimsPrincipal.Current.GetMsalAccountId()); - - await clientapp.RemoveAsync(userAccount); + if (userAccount != null) + { + await clientapp.RemoveAsync(userAccount); + } } } } \ No newline at end of file