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/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..9d21d8c 100644 --- a/WebApp/Utils/MSALPerUserMemoryTokenCache.cs +++ b/WebApp/Utils/MSALPerUserMemoryTokenCache.cs @@ -46,67 +46,23 @@ 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() - { - memoryCache.Remove(GetMsalAccountId()); } /// @@ -115,18 +71,22 @@ 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(); - - 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); + 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. + memoryCache.Set(cacheKey, args.TokenCache.SerializeMsalV3(), cacheDuration); + } + else + { + memoryCache.Remove(cacheKey); + } } } @@ -136,12 +96,13 @@ 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; + if (string.IsNullOrEmpty(cacheKey)) + { return; + } - byte[] tokenCacheBytes = (byte[])this.memoryCache.Get(cacheKey); + byte[] tokenCacheBytes = (byte[])memoryCache.Get(cacheKey); args.TokenCache.DeserializeMsalV3(tokenCacheBytes, shouldClearExistingCache: true); } @@ -153,18 +114,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..209bdbb 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; } @@ -61,9 +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); - userTokenCache.Clear(); + if (userAccount != null) + { + await clientapp.RemoveAsync(userAccount); + } } } } \ No newline at end of file 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 @@ - +