From d36ef914266ffd4d79e103bffc4a32dabdd9cea3 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 25 Mar 2021 13:50:14 -0700 Subject: [PATCH] Adding WindowsProvider project --- ...CommunityToolkit.Uwp.Authentication.csproj | 38 +++ .../WindowsProvider.cs | 276 ++++++++++++++++++ Windows-Toolkit-Graph-Controls.sln | 42 +++ 3 files changed, 356 insertions(+) create mode 100644 CommunityToolkit.Uwp.Authentication/CommunityToolkit.Uwp.Authentication.csproj create mode 100644 CommunityToolkit.Uwp.Authentication/WindowsProvider.cs diff --git a/CommunityToolkit.Uwp.Authentication/CommunityToolkit.Uwp.Authentication.csproj b/CommunityToolkit.Uwp.Authentication/CommunityToolkit.Uwp.Authentication.csproj new file mode 100644 index 0000000..79270cb --- /dev/null +++ b/CommunityToolkit.Uwp.Authentication/CommunityToolkit.Uwp.Authentication.csproj @@ -0,0 +1,38 @@ + + + + uap10.0.17763 + Windows Community Toolkit Graph Uwp Authentication Provider + CommunityToolkit.Uwp.Authentication + + This library provides an authentication provider based on the native Windows dialogues. It is part of the Windows Community Toolkit. + + Classes: + - WindowsProvider: + + UWP Toolkit Windows Microsoft Graph AadLogin Authentication Login + false + true + 8.0 + Debug;Release;CI + AnyCPU;ARM;ARM64;x64;x86 + + + + + + + + $(DefineConstants);WINRT + + + + + + + + + + + + diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs new file mode 100644 index 0000000..a3b4a63 --- /dev/null +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -0,0 +1,276 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using CommunityToolkit.Net.Authentication; +using Windows.Security.Authentication.Web; +using Windows.Security.Authentication.Web.Core; +using Windows.UI.ApplicationSettings; + +namespace Microsoft.Toolkit.Graph.Providers.Uwp +{ + /// + /// A provider for leveraging Windows system authentication. + /// + public class WindowsProvider : BaseProvider + { + private struct AuthenticatedUser + { + public Windows.Security.Credentials.PasswordCredential TokenCredential { get; private set; } + + public string GetUserName() + { + return TokenCredential?.UserName; + } + + public string GetToken() + { + return TokenCredential?.Password; + } + + public AuthenticatedUser(Windows.Security.Credentials.PasswordCredential tokenCredential) + { + TokenCredential = tokenCredential; + } + } + + private const string TokenCredentialResourceName = "WindowsProviderToken"; + private const string WebAccountProviderId = "https://login.microsoft.com"; + private static readonly string[] DefaultScopes = new string[] { "user.read" }; + private static readonly string GraphResourceProperty = "https://graph.microsoft.com"; + + /// + /// Gets the redirect uri value based on the current app callback uri. + /// + public static string RedirectUri => string.Format("ms-appx-web://Microsoft.AAD.BrokerPlugIn/{0}", WebAuthenticationBroker.GetCurrentApplicationCallbackUri().Host.ToUpper()); + + private AccountsSettingsPane _currentPane; + private AuthenticatedUser? _currentUser; + private string[] _scopes; + private string _clientId; + + /// + /// Initializes a new instance of the class. + /// + /// The clientId for the app registration. + /// The security scopes used to access specific workloads. + public WindowsProvider(string clientId, string[] scopes = null) + { + _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + _currentPane = null; + _currentUser = null; + _scopes = scopes ?? DefaultScopes; + + State = ProviderState.SignedOut; + + _ = TrySilentSignInAsync(); + } + + /// + /// Attempts to sign in the logged in user automatically. + /// + /// Success boolean. + public async Task TrySilentSignInAsync() + { + if (State == ProviderState.SignedIn) + { + return false; + } + + State = ProviderState.Loading; + + var tokenCredential = GetCredentialFromLocker(); + if (tokenCredential == null) + { + // There is no credential stored in the locker. + State = ProviderState.SignedOut; + return false; + } + + // Populate the password (aka token). + tokenCredential.RetrievePassword(); + + // Log the user in by storing the credential in memory. + _currentUser = new AuthenticatedUser(tokenCredential); + + try + { + var testRequest = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1/me"); + await AuthenticateRequestAsync(testRequest); + await new HttpClient().SendAsync(testRequest); + + // Update the state to be signed in. + State = ProviderState.SignedIn; + return true; + } + catch + { + // Update the state to be signed in. + State = ProviderState.SignedOut; + return false; + } + } + + /// + public override Task LoginAsync() + { + if (State == ProviderState.SignedIn) + { + return Task.CompletedTask; + } + + State = ProviderState.Loading; + + if (_currentPane != null) + { + _currentPane.AccountCommandsRequested -= BuildPaneAsync; + } + + _currentPane = AccountsSettingsPane.GetForCurrentView(); + _currentPane.AccountCommandsRequested += BuildPaneAsync; + + AccountsSettingsPane.Show(); + return Task.CompletedTask; + } + + /// + public override Task LogoutAsync() + { + if (State == ProviderState.SignedOut) + { + return Task.CompletedTask; + } + + State = ProviderState.Loading; + + if (_currentPane != null) + { + _currentPane.AccountCommandsRequested -= BuildPaneAsync; + _currentPane = null; + } + + if (_currentUser != null) + { + // Remove the user info from the PaasswordVault + var vault = new Windows.Security.Credentials.PasswordVault(); + vault.Remove(_currentUser?.TokenCredential); + + _currentUser = null; + } + + State = ProviderState.SignedOut; + return Task.CompletedTask; + } + + /// + public override Task AuthenticateRequestAsync(HttpRequestMessage request) + { + // Append the token to the authorization header of any outgoing Graph requests. + var token = _currentUser?.GetToken(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + return Task.CompletedTask; + } + + /// + /// https://docs.microsoft.com/en-us/windows/uwp/security/web-account-manager#build-the-account-settings-pane. + /// + private async void BuildPaneAsync(AccountsSettingsPane sender, AccountsSettingsPaneCommandsRequestedEventArgs args) + { + var deferral = args.GetDeferral(); + + try + { + // Providing nothing shows all accounts, providing authority shows only aad + var msaProvider = await WebAuthenticationCoreManager.FindAccountProviderAsync(WebAccountProviderId); + + if (msaProvider == null) + { + State = ProviderState.SignedOut; + return; + } + + var command = new WebAccountProviderCommand(msaProvider, GetTokenAsync); + args.WebAccountProviderCommands.Add(command); + } + catch + { + State = ProviderState.SignedOut; + } + finally + { + deferral.Complete(); + } + } + + private async void GetTokenAsync(WebAccountProviderCommand command) + { + // Build the token request + WebTokenRequest request = new WebTokenRequest(command.WebAccountProvider, string.Join(',', _scopes), _clientId); + request.Properties.Add("resource", GraphResourceProperty); + + // Get the results + WebTokenRequestResult result = await WebAuthenticationCoreManager.RequestTokenAsync(request); + + // Handle user cancellation + if (result.ResponseStatus == WebTokenRequestStatus.UserCancel) + { + State = ProviderState.SignedOut; + return; + } + + // Handle any errors + if (result.ResponseStatus != WebTokenRequestStatus.Success) + { + Debug.WriteLine(result.ResponseError.ErrorMessage); + State = ProviderState.SignedOut; + return; + } + + // Extract values from the results + var token = result.ResponseData[0].Token; + var account = result.ResponseData[0].WebAccount; + + // The UserName value may be null, but the Id is always present. + var userName = account.Id; + + // Save the user info to the PaasswordVault + var vault = new Windows.Security.Credentials.PasswordVault(); + var tokenCredential = new Windows.Security.Credentials.PasswordCredential(TokenCredentialResourceName, userName, token); + vault.Add(tokenCredential); + + // Set the current user object + _currentUser = new AuthenticatedUser(tokenCredential); + + // Update the state to be signed in. + State = ProviderState.SignedIn; + } + + private Windows.Security.Credentials.PasswordCredential GetCredentialFromLocker() + { + Windows.Security.Credentials.PasswordCredential credential = null; + + try + { + var vault = new Windows.Security.Credentials.PasswordVault(); + var credentialList = vault.FindAllByResource(TokenCredentialResourceName); + if (credentialList.Count > 0) + { + // We delete the credential upon logout, so only one user can be stored in the vault at a time. + credential = credentialList.First(); + } + } + catch + { + // FindAllByResource will throw an exception if the resource isn't found. + } + + return credential; + } + } +} diff --git a/Windows-Toolkit-Graph-Controls.sln b/Windows-Toolkit-Graph-Controls.sln index 65d2f35..2d0cab8 100644 --- a/Windows-Toolkit-Graph-Controls.sln +++ b/Windows-Toolkit-Graph-Controls.sln @@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Net.Authen EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Net.Authentication.Msal", "CommunityToolkit.Net.Authentication.Msal\CommunityToolkit.Net.Authentication.Msal.csproj", "{CA4042D2-33A2-450B-8B9D-C286B9F3F3F4}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Uwp.Authentication", "CommunityToolkit.Uwp.Authentication\CommunityToolkit.Uwp.Authentication.csproj", "{2E4A708A-DF53-4863-B797-E14CDC6B90FA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CI|Any CPU = CI|Any CPU @@ -252,6 +254,46 @@ Global {CA4042D2-33A2-450B-8B9D-C286B9F3F3F4}.Release|x64.Build.0 = Release|x64 {CA4042D2-33A2-450B-8B9D-C286B9F3F3F4}.Release|x86.ActiveCfg = Release|x86 {CA4042D2-33A2-450B-8B9D-C286B9F3F3F4}.Release|x86.Build.0 = Release|x86 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.CI|Any CPU.ActiveCfg = CI|Any CPU + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.CI|Any CPU.Build.0 = CI|Any CPU + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.CI|ARM.ActiveCfg = CI|ARM + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.CI|ARM.Build.0 = CI|ARM + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.CI|ARM64.ActiveCfg = CI|ARM64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.CI|ARM64.Build.0 = CI|ARM64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.CI|x64.ActiveCfg = CI|x64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.CI|x64.Build.0 = CI|x64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.CI|x86.ActiveCfg = CI|x86 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.CI|x86.Build.0 = CI|x86 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Debug|ARM.ActiveCfg = Debug|ARM + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Debug|ARM.Build.0 = Debug|ARM + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Debug|ARM64.Build.0 = Debug|ARM64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Debug|x64.ActiveCfg = Debug|x64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Debug|x64.Build.0 = Debug|x64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Debug|x86.ActiveCfg = Debug|x86 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Debug|x86.Build.0 = Debug|x86 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Native|Any CPU.ActiveCfg = Debug|Any CPU + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Native|Any CPU.Build.0 = Debug|Any CPU + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Native|ARM.ActiveCfg = Debug|ARM + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Native|ARM.Build.0 = Debug|ARM + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Native|ARM64.ActiveCfg = Debug|ARM64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Native|ARM64.Build.0 = Debug|ARM64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Native|x64.ActiveCfg = Debug|x64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Native|x64.Build.0 = Debug|x64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Native|x86.ActiveCfg = Debug|x86 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Native|x86.Build.0 = Debug|x86 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Release|Any CPU.Build.0 = Release|Any CPU + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Release|ARM.ActiveCfg = Release|ARM + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Release|ARM.Build.0 = Release|ARM + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Release|ARM64.ActiveCfg = Release|ARM64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Release|ARM64.Build.0 = Release|ARM64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Release|x64.ActiveCfg = Release|x64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Release|x64.Build.0 = Release|x64 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Release|x86.ActiveCfg = Release|x86 + {2E4A708A-DF53-4863-B797-E14CDC6B90FA}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE