From d36ef914266ffd4d79e103bffc4a32dabdd9cea3 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 25 Mar 2021 13:50:14 -0700 Subject: [PATCH 01/24] 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 From b2882507a3edec81956f823d1c24591c8e1c666c Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 25 Mar 2021 14:40:29 -0700 Subject: [PATCH 02/24] Added WindowsProvider to sampleTest --- SampleTest/App.xaml.cs | 11 +++++++++-- SampleTest/SampleTest.csproj | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/SampleTest/App.xaml.cs b/SampleTest/App.xaml.cs index 17faf49..e6497e2 100644 --- a/SampleTest/App.xaml.cs +++ b/SampleTest/App.xaml.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Net.Authentication; +using Microsoft.Toolkit.Graph.Providers.Uwp; using System; using System.Threading.Tasks; using Windows.ApplicationModel; @@ -36,7 +37,8 @@ public App() private enum ProviderType { Mock, - Msal + Msal, + Windows } /// @@ -60,10 +62,15 @@ private void InitializeGlobalProvider() ProviderManager.Instance.GlobalProvider = new MockProvider(signedIn: true); break; - //Msal provider + // Msal provider case ProviderType.Msal: ProviderManager.Instance.GlobalProvider = new MsalProvider(clientId, scopes); break; + + // Windows provider + case ProviderType.Windows: + ProviderManager.Instance.GlobalProvider = new WindowsProvider(clientId, scopes); + break; } } diff --git a/SampleTest/SampleTest.csproj b/SampleTest/SampleTest.csproj index d604df6..45172ce 100644 --- a/SampleTest/SampleTest.csproj +++ b/SampleTest/SampleTest.csproj @@ -168,6 +168,10 @@ {b2246169-0cd8-473c-aff6-172310e2c3f6} CommunityToolkit.Net.Graph + + {2E4A708A-DF53-4863-B797-E14CDC6B90FA} + CommunityToolkit.Uwp.Authentication + {42252ee8-7e68-428f-972b-6d2dd3aa12cc} CommunityToolkit.Uwp.Graph.Controls From d7a804a8ce9d7f9c6c75953b039d57ebced27095 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 1 Apr 2021 15:20:28 -0700 Subject: [PATCH 03/24] Refactor, currently works with MSA --- .../WindowsProvider.cs | 385 ++++++++++-------- 1 file changed, 209 insertions(+), 176 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index a3b4a63..1a523dd 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -1,276 +1,309 @@ -// 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; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using CommunityToolkit.Net.Authentication; +using Windows.Networking.Connectivity; using Windows.Security.Authentication.Web; using Windows.Security.Authentication.Web.Core; +using Windows.Security.Credentials; +using Windows.Storage; using Windows.UI.ApplicationSettings; -namespace Microsoft.Toolkit.Graph.Providers.Uwp +namespace CommunityToolkit.Uwp.Authentication { /// - /// 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 const string AzureADAuthority = "organizations"; + private const string MicrosoftAccountProviderId = "https://login.windows.net"; + private const string GraphResourceProperty = "https://graph.microsoft.com"; + private const string WebAccountProviderId = "https://login.microsoft.com"; + private const string SettingsKeyWamAccountId = "WamAccountId"; + private const string SettingsKeyWamProviderId = "WamProviderId"; + + private static readonly string[] DefaultScopes = + { + "User.Read", + }; + + private ApplicationDataContainer _appSettings; private string _clientId; + private string[] _scopes; + private WebAccount _webAccount; + private WebAccountProvider _webAccountProvider; /// /// 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) + /// + /// + public WindowsProvider (string clientId, string[] scopes = null) { - _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - _currentPane = null; - _currentUser = null; + _appSettings = ApplicationData.Current.LocalSettings; + _clientId = clientId; _scopes = scopes ?? DefaultScopes; + _webAccount = null; State = ProviderState.SignedOut; + } - _ = TrySilentSignInAsync(); + /// + public override async Task AuthenticateRequestAsync(HttpRequestMessage request) + { + string token = await GetTokenAsync(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } - /// - /// Attempts to sign in the logged in user automatically. - /// - /// Success boolean. - public async Task TrySilentSignInAsync() + /// + public override async Task LoginAsync() { - if (State == ProviderState.SignedIn) + if (_webAccount != null || State != ProviderState.SignedOut) { - return false; + await LogoutAsync(); } - State = ProviderState.Loading; + // The state will get updated as part of the auth flow. + var token = await GetTokenAsync(); - var tokenCredential = GetCredentialFromLocker(); - if (tokenCredential == null) + if (token == null) { - // There is no credential stored in the locker. - State = ProviderState.SignedOut; - return false; + await LogoutAsync(); } + } - // Populate the password (aka token). - tokenCredential.RetrievePassword(); - - // Log the user in by storing the credential in memory. - _currentUser = new AuthenticatedUser(tokenCredential); + /// + public override async Task LogoutAsync() + { + _appSettings.Values.Remove(SettingsKeyWamAccountId); + _appSettings.Values.Remove(SettingsKeyWamProviderId); - try + if (_webAccount != null) { - var testRequest = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1/me"); - await AuthenticateRequestAsync(testRequest); - await new HttpClient().SendAsync(testRequest); + try + { + await _webAccount.SignOutAsync(); + } + catch + { + // Failed to remove an account. + } - // 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; + _webAccount = null; } + + State = ProviderState.SignedOut; } - /// - public override Task LoginAsync() + private async Task GetTokenAsync(bool silentOnly = false) { - if (State == ProviderState.SignedIn) + var internetConnectionProfile = NetworkInformation.GetInternetConnectionProfile(); + if (internetConnectionProfile == null) { - return Task.CompletedTask; + // We are not online, no token for you. + // TODO: Is there anything special to do when we go offline? + return null; } - State = ProviderState.Loading; - - if (_currentPane != null) + try { - _currentPane.AccountCommandsRequested -= BuildPaneAsync; - } + if (State == ProviderState.SignedOut) + { + State = ProviderState.Loading; + } - _currentPane = AccountsSettingsPane.GetForCurrentView(); - _currentPane.AccountCommandsRequested += BuildPaneAsync; + // Attempt to authenticate silently. + var authResult = await AuthenticateSilentAsync(); - AccountsSettingsPane.Show(); - return Task.CompletedTask; - } + if (authResult?.ResponseStatus != WebTokenRequestStatus.Success && !silentOnly) + { + // Attempt to authenticate interactively. + authResult = await AuthenticateInteractiveAsync(); + } - /// - public override Task LogoutAsync() - { - if (State == ProviderState.SignedOut) + if (authResult?.ResponseStatus == WebTokenRequestStatus.Success) + { + var account = _webAccount; + var newAccount = authResult.ResponseData[0].WebAccount; + + if (account == null || account.Id != newAccount.Id) + { + // Account was switched, update the active account. + await SetAccountAsync(newAccount); + } + + var authToken = authResult.ResponseData[0].Token; + return authToken; + } + else if (authResult?.ResponseError != null) + { + throw new Exception(authResult.ResponseError.ErrorCode + ": " + authResult.ResponseError.ErrorMessage); + } + } + catch (Exception e) { - return Task.CompletedTask; } - State = ProviderState.Loading; + return null; + } - if (_currentPane != null) + private async Task SetAccountAsync(WebAccount account) + { + if (account == null) { - _currentPane.AccountCommandsRequested -= BuildPaneAsync; - _currentPane = null; + if (_webAccount != null) + { + await LogoutAsync(); + } + else + { + State = ProviderState.SignedOut; + } + + return; } - if (_currentUser != null) + if (account.Id == _webAccount?.Id) { - // Remove the user info from the PaasswordVault - var vault = new Windows.Security.Credentials.PasswordVault(); - vault.Remove(_currentUser?.TokenCredential); - - _currentUser = null; + // no change + return; } - State = ProviderState.SignedOut; - return Task.CompletedTask; + // Save off the account ids. + _webAccount = account; + _appSettings.Values[SettingsKeyWamAccountId] = account.Id; + _appSettings.Values[SettingsKeyWamProviderId] = account.WebAccountProvider.Id; + + State = ProviderState.SignedIn; } - /// - public override Task AuthenticateRequestAsync(HttpRequestMessage request) + private async Task AuthenticateSilentAsync() { - // 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; + var account = _webAccount; + if (account != null) + { + // Prepare a request to get a token. + var webTokenRequest = GetWebTokenRequest(account.WebAccountProvider); + + try + { + WebTokenRequestResult authResult = await WebAuthenticationCoreManager.GetTokenSilentlyAsync(webTokenRequest, account); + return authResult; + } + catch (HttpRequestException) + { + throw; /* probably offline, no point continuing to interactive auth */ + } + } + + return null; } - /// - /// 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) + private async Task AuthenticateInteractiveAsync() { - var deferral = args.GetDeferral(); + var pane = AccountsSettingsPane.GetForCurrentView(); + pane.AccountCommandsRequested += OnAccountCommandsRequested; try { - // Providing nothing shows all accounts, providing authority shows only aad - var msaProvider = await WebAuthenticationCoreManager.FindAccountProviderAsync(WebAccountProviderId); + WebTokenRequestResult authResult = null; - if (msaProvider == null) + var account = _webAccount; + if (account == null) { - State = ProviderState.SignedOut; - return; + await AccountsSettingsPane.ShowAddAccountAsync(); + + // _webAccountProvider will be set once the user has selected an account. + if (_webAccountProvider != null) + { + var webTokenRequest = GetWebTokenRequest(_webAccountProvider); + + // The webAccountProvider may need to come from the commands event instead. + + // If we reached here, then WebAccountProviderCommandInvoked + // was called and a new _webTokenRequest was generated based + // on the user's selection in the dialog. + authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest); + } + } + else + { + var webTokenRequest = GetWebTokenRequest(account.WebAccountProvider); + authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest, account); } - var command = new WebAccountProviderCommand(msaProvider, GetTokenAsync); - args.WebAccountProviderCommands.Add(command); + return authResult; } - catch + catch (HttpRequestException) { - State = ProviderState.SignedOut; + throw; /* probably offline, no point continuing to interactive auth */ } finally { - deferral.Complete(); + pane.AccountCommandsRequested -= OnAccountCommandsRequested; } } - private async void GetTokenAsync(WebAccountProviderCommand command) + private async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSettingsPaneCommandsRequestedEventArgs e) { - // 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) + void WebAccountProviderCommandInvoked(WebAccountProviderCommand command) { - State = ProviderState.SignedOut; - return; + _webAccountProvider = command.WebAccountProvider; } - // Handle any errors - if (result.ResponseStatus != WebTokenRequestStatus.Success) + var deferral = e.GetDeferral(); + + try { - Debug.WriteLine(result.ResponseError.ErrorMessage); - State = ProviderState.SignedOut; - return; - } + WebAccountProvider webAccountProvider = await GetWebAccountProvider(); - // Extract values from the results - var token = result.ResponseData[0].Token; - var account = result.ResponseData[0].WebAccount; + var providerCommand = new WebAccountProviderCommand(webAccountProvider, WebAccountProviderCommandInvoked); + e.WebAccountProviderCommands.Add(providerCommand); - // The UserName value may be null, but the Id is always present. - var userName = account.Id; + // e.HeaderText = _resourceLoader.GetString("WAMTitle"); - // 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); + // We only show the privacy link if the debugger is not attached because it throws internally as part of + // parsing the string (CSettingsCommandFactory::CreateSettingsCommand first tries to parse the string as a guid + // and uses exceptions in determining it is not a guid :( ). + // if (!Debugger.IsAttached) + // e.Commands.Add(new SettingsCommand("privacypolicy", "Privacy policy", PrivacyPolicyInvoked)); + } + catch + { + await LogoutAsync(); + } + finally + { + deferral.Complete(); + } + } - // Set the current user object - _currentUser = new AuthenticatedUser(tokenCredential); + /// + /// Create a token request. Executing the request will prompt the authentication flow to begin. + /// + private WebTokenRequest GetWebTokenRequest(WebAccountProvider provider) + { + var webTokenRequest = new WebTokenRequest(provider, string.Join(',', _scopes), _clientId); + webTokenRequest.Properties.Add("resource", GraphResourceProperty); - // Update the state to be signed in. - State = ProviderState.SignedIn; + return webTokenRequest; } - private Windows.Security.Credentials.PasswordCredential GetCredentialFromLocker() + private async Task GetWebAccountProvider() { - 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. - } + // Find the provider for general MSA login. + // Org accounts will work out of the box, but MSA's won't work unless the app is associated with the store. + return await WebAuthenticationCoreManager.FindAccountProviderAsync(WebAccountProviderId); - return credential; + // Find the provider for org account login. + // return await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftAccountProviderId, AzureADAuthority); } } } From a4801f77b5347e65942be754bd7f4cfac2430f4a Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 1 Apr 2021 16:03:27 -0700 Subject: [PATCH 04/24] Tweaks to method order --- .../WindowsProvider.cs | 134 +++++++++--------- 1 file changed, 66 insertions(+), 68 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 1a523dd..1fa919f 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -13,7 +13,7 @@ namespace CommunityToolkit.Uwp.Authentication { /// - /// + /// An authentication provider based on the native AccountSettingsPane in Windows. /// public class WindowsProvider : BaseProvider { @@ -22,10 +22,10 @@ public class WindowsProvider : BaseProvider /// public static string RedirectUri => string.Format("ms-appx-web://Microsoft.AAD.BrokerPlugIn/{0}", WebAuthenticationBroker.GetCurrentApplicationCallbackUri().Host.ToUpper()); - private const string AzureADAuthority = "organizations"; - private const string MicrosoftAccountProviderId = "https://login.windows.net"; private const string GraphResourceProperty = "https://graph.microsoft.com"; - private const string WebAccountProviderId = "https://login.microsoft.com"; + private const string MicrosoftProviderId = "https://login.microsoft.com"; + private const string AzureActiveDirectoryAuthority = "organizations"; + private const string MicrosoftAccountAuthority = "consumers"; private const string SettingsKeyWamAccountId = "WamAccountId"; private const string SettingsKeyWamProviderId = "WamProviderId"; @@ -38,7 +38,6 @@ public class WindowsProvider : BaseProvider private string _clientId; private string[] _scopes; private WebAccount _webAccount; - private WebAccountProvider _webAccountProvider; /// /// Initializes a new instance of the class. @@ -149,6 +148,7 @@ private async Task GetTokenAsync(bool silentOnly = false) } catch (Exception e) { + System.Diagnostics.Debug.WriteLine(e.Message); } return null; @@ -186,57 +186,41 @@ private async Task SetAccountAsync(WebAccount account) private async Task AuthenticateSilentAsync() { - var account = _webAccount; - if (account != null) + try { - // Prepare a request to get a token. - var webTokenRequest = GetWebTokenRequest(account.WebAccountProvider); + WebTokenRequestResult authResult = null; - try - { - WebTokenRequestResult authResult = await WebAuthenticationCoreManager.GetTokenSilentlyAsync(webTokenRequest, account); - return authResult; - } - catch (HttpRequestException) + var account = _webAccount; + if (account != null) { - throw; /* probably offline, no point continuing to interactive auth */ + // Prepare a request to get a token. + var webTokenRequest = GetWebTokenRequest(account.WebAccountProvider); + authResult = await WebAuthenticationCoreManager.GetTokenSilentlyAsync(webTokenRequest, account); } - } - return null; + return authResult; + } + catch (HttpRequestException) + { + throw; /* probably offline, no point continuing to interactive auth */ + } } private async Task AuthenticateInteractiveAsync() { - var pane = AccountsSettingsPane.GetForCurrentView(); - pane.AccountCommandsRequested += OnAccountCommandsRequested; - try { WebTokenRequestResult authResult = null; var account = _webAccount; - if (account == null) + if (account != null) { - await AccountsSettingsPane.ShowAddAccountAsync(); - - // _webAccountProvider will be set once the user has selected an account. - if (_webAccountProvider != null) - { - var webTokenRequest = GetWebTokenRequest(_webAccountProvider); - - // The webAccountProvider may need to come from the commands event instead. - - // If we reached here, then WebAccountProviderCommandInvoked - // was called and a new _webTokenRequest was generated based - // on the user's selection in the dialog. - authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest); - } + var webTokenRequest = GetWebTokenRequest(account.WebAccountProvider); + authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest, account); } else { - var webTokenRequest = GetWebTokenRequest(account.WebAccountProvider); - authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest, account); + authResult = await ShowAddAccountAndGetResultAsync(); } return authResult; @@ -245,49 +229,59 @@ private async Task AuthenticateInteractiveAsync() { throw; /* probably offline, no point continuing to interactive auth */ } - finally - { - pane.AccountCommandsRequested -= OnAccountCommandsRequested; - } } - private async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSettingsPaneCommandsRequestedEventArgs e) + private async Task ShowAddAccountAndGetResultAsync() { - void WebAccountProviderCommandInvoked(WebAccountProviderCommand command) + var addAccountTaskCompletionSource = new TaskCompletionSource(); + + async void WebAccountProviderCommandInvoked(WebAccountProviderCommand command) { - _webAccountProvider = command.WebAccountProvider; - } + var webTokenRequest = GetWebTokenRequest(command.WebAccountProvider); - var deferral = e.GetDeferral(); + var authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest); + addAccountTaskCompletionSource.SetResult(authResult); + } - try + async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSettingsPaneCommandsRequestedEventArgs e) { - WebAccountProvider webAccountProvider = await GetWebAccountProvider(); + var deferral = e.GetDeferral(); - var providerCommand = new WebAccountProviderCommand(webAccountProvider, WebAccountProviderCommandInvoked); - e.WebAccountProviderCommands.Add(providerCommand); + try + { + WebAccountProvider webAccountProvider = await GetWebAccountProvider(); - // e.HeaderText = _resourceLoader.GetString("WAMTitle"); + var providerCommand = new WebAccountProviderCommand(webAccountProvider, WebAccountProviderCommandInvoked); + e.WebAccountProviderCommands.Add(providerCommand); - // We only show the privacy link if the debugger is not attached because it throws internally as part of - // parsing the string (CSettingsCommandFactory::CreateSettingsCommand first tries to parse the string as a guid - // and uses exceptions in determining it is not a guid :( ). - // if (!Debugger.IsAttached) - // e.Commands.Add(new SettingsCommand("privacypolicy", "Privacy policy", PrivacyPolicyInvoked)); + // TODO: Expose configuration so developers can have some control over the popup. + } + catch + { + await LogoutAsync(); + } + finally + { + deferral.Complete(); + } } - catch + + var pane = AccountsSettingsPane.GetForCurrentView(); + pane.AccountCommandsRequested += OnAccountCommandsRequested; + + try { - await LogoutAsync(); + await AccountsSettingsPane.ShowAddAccountAsync(); + + var authResult = await addAccountTaskCompletionSource.Task; + return authResult; } finally { - deferral.Complete(); + pane.AccountCommandsRequested -= OnAccountCommandsRequested; } } - /// - /// Create a token request. Executing the request will prompt the authentication flow to begin. - /// private WebTokenRequest GetWebTokenRequest(WebAccountProvider provider) { var webTokenRequest = new WebTokenRequest(provider, string.Join(',', _scopes), _clientId); @@ -298,12 +292,16 @@ private WebTokenRequest GetWebTokenRequest(WebAccountProvider provider) private async Task GetWebAccountProvider() { - // Find the provider for general MSA login. - // Org accounts will work out of the box, but MSA's won't work unless the app is associated with the store. - return await WebAuthenticationCoreManager.FindAccountProviderAsync(WebAccountProviderId); + // TODO: Enable devs to turn on/off which account sources they wish to integrate with. + + // MSA - Works + // return await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, MicrosoftAccountAuthority); + + // AAD - Fails complaining about 'client_assertion' or 'client_secret' + // return await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, AzureActiveDirectoryAuthority); - // Find the provider for org account login. - // return await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftAccountProviderId, AzureADAuthority); + // Both + return await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId); } } } From 1c58e6c2dab0ac70ba260bb0677c269d15c82022 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Fri, 2 Apr 2021 10:37:23 -0700 Subject: [PATCH 05/24] Added TryLoginSilentAsync method --- .../WindowsProvider.cs | 67 ++++++++++++++++--- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 1fa919f..769789b 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; @@ -26,15 +27,27 @@ public class WindowsProvider : BaseProvider private const string MicrosoftProviderId = "https://login.microsoft.com"; private const string AzureActiveDirectoryAuthority = "organizations"; private const string MicrosoftAccountAuthority = "consumers"; - private const string SettingsKeyWamAccountId = "WamAccountId"; - private const string SettingsKeyWamProviderId = "WamProviderId"; private static readonly string[] DefaultScopes = { "User.Read", }; - private ApplicationDataContainer _appSettings; + /// + /// Gets a cache of important values for the signed in user. + /// + protected IDictionary Settings => ApplicationData.Current.LocalSettings.Values; + + /// + /// The settings key for the active account id. + /// + protected const string SettingsKeyWamAccountId = "WamAccountId"; + + /// + /// The settings key for the active provider id. + /// + protected const string SettingsKeyWamProviderId = "WamProviderId"; + private string _clientId; private string[] _scopes; private WebAccount _webAccount; @@ -46,7 +59,6 @@ public class WindowsProvider : BaseProvider /// public WindowsProvider (string clientId, string[] scopes = null) { - _appSettings = ApplicationData.Current.LocalSettings; _clientId = clientId; _scopes = scopes ?? DefaultScopes; _webAccount = null; @@ -78,11 +90,31 @@ public override async Task LoginAsync() } } + /// + /// Try logging in silently. + /// + /// A representing the asynchronous operation. + public async Task TryLoginSilentAsync() + { + if (_webAccount != null || State != ProviderState.SignedOut) + { + return; + } + + // The state will get updated as part of the auth flow. + var token = await GetTokenAsync(true); + + if (token == null) + { + await LogoutAsync(); + } + } + /// public override async Task LogoutAsync() { - _appSettings.Values.Remove(SettingsKeyWamAccountId); - _appSettings.Values.Remove(SettingsKeyWamProviderId); + Settings.Remove(SettingsKeyWamAccountId); + Settings.Remove(SettingsKeyWamProviderId); if (_webAccount != null) { @@ -178,8 +210,8 @@ private async Task SetAccountAsync(WebAccount account) // Save off the account ids. _webAccount = account; - _appSettings.Values[SettingsKeyWamAccountId] = account.Id; - _appSettings.Values[SettingsKeyWamProviderId] = account.WebAccountProvider.Id; + Settings[SettingsKeyWamAccountId] = account.Id; + Settings[SettingsKeyWamProviderId] = account.WebAccountProvider.Id; State = ProviderState.SignedIn; } @@ -191,6 +223,21 @@ private async Task AuthenticateSilentAsync() WebTokenRequestResult authResult = null; var account = _webAccount; + if (account == null) + { + // Check the cache for an existing user + if (Settings[SettingsKeyWamAccountId] is string savedAccountId) + { + var savedProvider = await GetWebAccountProviderAsync(); + if (Settings[SettingsKeyWamProviderId] is string savedProviderId) + { + savedProvider = await WebAuthenticationCoreManager.FindAccountProviderAsync(savedProviderId); + } + + account = await WebAuthenticationCoreManager.FindAccountAsync(savedProvider, savedAccountId); + } + } + if (account != null) { // Prepare a request to get a token. @@ -249,7 +296,7 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti try { - WebAccountProvider webAccountProvider = await GetWebAccountProvider(); + WebAccountProvider webAccountProvider = await GetWebAccountProviderAsync(); var providerCommand = new WebAccountProviderCommand(webAccountProvider, WebAccountProviderCommandInvoked); e.WebAccountProviderCommands.Add(providerCommand); @@ -290,7 +337,7 @@ private WebTokenRequest GetWebTokenRequest(WebAccountProvider provider) return webTokenRequest; } - private async Task GetWebAccountProvider() + private async Task GetWebAccountProviderAsync() { // TODO: Enable devs to turn on/off which account sources they wish to integrate with. From 968977fe86443221024c001152b6b583b4de689e Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Fri, 2 Apr 2021 14:37:34 -0700 Subject: [PATCH 06/24] Updates --- .../WindowsProvider.cs | 125 ++++++++++++------ SampleTest/App.xaml.cs | 56 ++++---- 2 files changed, 115 insertions(+), 66 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 769789b..053254d 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -25,8 +25,10 @@ public class WindowsProvider : BaseProvider private const string GraphResourceProperty = "https://graph.microsoft.com"; private const string MicrosoftProviderId = "https://login.microsoft.com"; - private const string AzureActiveDirectoryAuthority = "organizations"; - private const string MicrosoftAccountAuthority = "consumers"; + //// private const string AzureActiveDirectoryAuthority = "organizations"; + //// private const string MicrosoftAccountAuthority = "consumers"; + //// private const string MicrosoftAccountClientId = "none"; + //// private const string MicrosoftAccountScopeRequested = "wl.basic"; private static readonly string[] DefaultScopes = { @@ -55,9 +57,9 @@ public class WindowsProvider : BaseProvider /// /// Initializes a new instance of the class. /// - /// - /// - public WindowsProvider (string clientId, string[] scopes = null) + /// Registered ClientId. + /// List of Scopes to initially request. + public WindowsProvider(string clientId, string[] scopes = null) { _clientId = clientId; _scopes = scopes ?? DefaultScopes; @@ -94,7 +96,7 @@ public override async Task LoginAsync() /// Try logging in silently. /// /// A representing the asynchronous operation. - public async Task TryLoginSilentAsync() + public async Task TrySilentLoginAsync() { if (_webAccount != null || State != ProviderState.SignedOut) { @@ -130,7 +132,7 @@ public override async Task LogoutAsync() _webAccount = null; } - State = ProviderState.SignedOut; + SetState(ProviderState.SignedOut); } private async Task GetTokenAsync(bool silentOnly = false) @@ -147,14 +149,21 @@ private async Task GetTokenAsync(bool silentOnly = false) { if (State == ProviderState.SignedOut) { - State = ProviderState.Loading; + SetState(ProviderState.Loading); } // Attempt to authenticate silently. var authResult = await AuthenticateSilentAsync(); - if (authResult?.ResponseStatus != WebTokenRequestStatus.Success && !silentOnly) + // Authenticate with user interaction as appropriate. + if (authResult?.ResponseStatus != WebTokenRequestStatus.Success) { + if (silentOnly) + { + // Silent login may fail if we don't have a cached account, and that's ok. + return null; + } + // Attempt to authenticate interactively. authResult = await AuthenticateInteractiveAsync(); } @@ -173,14 +182,24 @@ private async Task GetTokenAsync(bool silentOnly = false) var authToken = authResult.ResponseData[0].Token; return authToken; } + else if (authResult?.ResponseStatus == WebTokenRequestStatus.UserCancel) + { + return null; + } else if (authResult?.ResponseError != null) { throw new Exception(authResult.ResponseError.ErrorCode + ": " + authResult.ResponseError.ErrorMessage); } + else + { + // Authentication response was not successful or cancelled, but is also missing a ResponseError. + throw new Exception("Authentication response was not successful, but is also missing a ResponseError."); + } } catch (Exception e) { System.Diagnostics.Debug.WriteLine(e.Message); + await LogoutAsync(); } return null; @@ -190,21 +209,13 @@ private async Task SetAccountAsync(WebAccount account) { if (account == null) { - if (_webAccount != null) - { - await LogoutAsync(); - } - else - { - State = ProviderState.SignedOut; - } - - return; + // Clear account + await LogoutAsync(); + return; } - - if (account.Id == _webAccount?.Id) + else if (account.Id == _webAccount?.Id) { - // no change + // No change return; } @@ -213,7 +224,7 @@ private async Task SetAccountAsync(WebAccount account) Settings[SettingsKeyWamAccountId] = account.Id; Settings[SettingsKeyWamProviderId] = account.WebAccountProvider.Id; - State = ProviderState.SignedIn; + SetState(ProviderState.SignedIn); } private async Task AuthenticateSilentAsync() @@ -226,14 +237,10 @@ private async Task AuthenticateSilentAsync() if (account == null) { // Check the cache for an existing user - if (Settings[SettingsKeyWamAccountId] is string savedAccountId) + if (Settings[SettingsKeyWamAccountId] is string savedAccountId && + Settings[SettingsKeyWamProviderId] is string savedProviderId) { - var savedProvider = await GetWebAccountProviderAsync(); - if (Settings[SettingsKeyWamProviderId] is string savedProviderId) - { - savedProvider = await WebAuthenticationCoreManager.FindAccountProviderAsync(savedProviderId); - } - + var savedProvider = await WebAuthenticationCoreManager.FindAccountProviderAsync(savedProviderId); account = await WebAuthenticationCoreManager.FindAccountAsync(savedProvider, savedAccountId); } } @@ -278,34 +285,54 @@ private async Task AuthenticateInteractiveAsync() } } + /// + /// Show the AccountSettingsPane and wait for the user to make a selection, then process the authentication result. + /// private async Task ShowAddAccountAndGetResultAsync() { + // The AccountSettingsPane uses events to support the flow of authentication events. + // Ultimately we need access to the user's selected account provider from the AccountSettingsPane, which is available + // in the WebAccountProviderCommandInvoked function for the chosen provider. + // To ensure no funny business, the entire AccountSettingsPane flow is contained here. var addAccountTaskCompletionSource = new TaskCompletionSource(); + // Handle the selected account provider async void WebAccountProviderCommandInvoked(WebAccountProviderCommand command) { - var webTokenRequest = GetWebTokenRequest(command.WebAccountProvider); + try + { + var webTokenRequest = GetWebTokenRequest(command.WebAccountProvider); - var authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest); - addAccountTaskCompletionSource.SetResult(authResult); + var authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest); + addAccountTaskCompletionSource.SetResult(authResult); + } + catch (Exception ex) + { + addAccountTaskCompletionSource.SetException(ex); + } } + // Build the AccountSettingsPane and configure it with available providers. async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSettingsPaneCommandsRequestedEventArgs e) { var deferral = e.GetDeferral(); try { - WebAccountProvider webAccountProvider = await GetWebAccountProviderAsync(); + // Configure available providers. + List webAccountProviders = await GetWebAccountProvidersAsync(); - var providerCommand = new WebAccountProviderCommand(webAccountProvider, WebAccountProviderCommandInvoked); - e.WebAccountProviderCommands.Add(providerCommand); + foreach (WebAccountProvider webAccountProvider in webAccountProviders) + { + var providerCommand = new WebAccountProviderCommand(webAccountProvider, WebAccountProviderCommandInvoked); + e.WebAccountProviderCommands.Add(providerCommand); + } // TODO: Expose configuration so developers can have some control over the popup. } - catch + catch (Exception ex) { - await LogoutAsync(); + addAccountTaskCompletionSource.SetException(ex); } finally { @@ -318,6 +345,7 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti try { + // Show the AccountSettingsPane and wait for the result. await AccountsSettingsPane.ShowAddAccountAsync(); var authResult = await addAccountTaskCompletionSource.Task; @@ -331,14 +359,16 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti private WebTokenRequest GetWebTokenRequest(WebAccountProvider provider) { - var webTokenRequest = new WebTokenRequest(provider, string.Join(',', _scopes), _clientId); + WebTokenRequest webTokenRequest = new WebTokenRequest(provider, string.Join(',', _scopes), _clientId); webTokenRequest.Properties.Add("resource", GraphResourceProperty); return webTokenRequest; } - private async Task GetWebAccountProviderAsync() + private async Task> GetWebAccountProvidersAsync() { + List providers = new List(); + // TODO: Enable devs to turn on/off which account sources they wish to integrate with. // MSA - Works @@ -348,7 +378,20 @@ private async Task GetWebAccountProviderAsync() // return await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, AzureActiveDirectoryAuthority); // Both - return await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId); + providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId)); + + return providers; + } + + // Instead of updating the State value directly, use SetState to ensure that provider events are fired on the proper thread. + private void SetState(ProviderState state) + { + if (State == state) + { + return; + } + + State = state; } } } diff --git a/SampleTest/App.xaml.cs b/SampleTest/App.xaml.cs index e6497e2..f40d65d 100644 --- a/SampleTest/App.xaml.cs +++ b/SampleTest/App.xaml.cs @@ -3,9 +3,8 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Net.Authentication; -using Microsoft.Toolkit.Graph.Providers.Uwp; +using CommunityToolkit.Uwp.Authentication; using System; -using System.Threading.Tasks; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; using Windows.UI.Xaml; @@ -44,34 +43,41 @@ private enum ProviderType /// /// Initialize the global authentication provider. /// - private void InitializeGlobalProvider() + private async void InitializeGlobalProvider() { if (ProviderManager.Instance.GlobalProvider != null) { return; } - // Provider config - string clientId = "YOUR_CLIENT_ID_HERE"; - string[] scopes = { "User.Read", "User.ReadBasic.All", "People.Read", "Calendars.Read", "Mail.Read", "Group.Read.All", "ChannelMessage.Read.All" }; - - switch(_providerType) + await Window.Current.Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () => { - // Mock provider - case ProviderType.Mock: - ProviderManager.Instance.GlobalProvider = new MockProvider(signedIn: true); - break; - - // Msal provider - case ProviderType.Msal: - ProviderManager.Instance.GlobalProvider = new MsalProvider(clientId, scopes); - break; - - // Windows provider - case ProviderType.Windows: - ProviderManager.Instance.GlobalProvider = new WindowsProvider(clientId, scopes); - break; - } + // Provider config + string clientId = "YOUR_CLIENT_ID_HERE"; + string[] scopes = { "User.Read", "User.ReadBasic.All", "People.Read", "Calendars.Read", "Mail.Read", "Group.Read.All", "ChannelMessage.Read.All" }; + + switch (_providerType) + { + // Mock provider + case ProviderType.Mock: + ProviderManager.Instance.GlobalProvider = new MockProvider(signedIn: true); + break; + + // Msal provider + case ProviderType.Msal: + var msalProvider = new MsalProvider(clientId, scopes); + ProviderManager.Instance.GlobalProvider = msalProvider; + await msalProvider.TrySilentSignInAsync(); + break; + + // Windows provider + case ProviderType.Windows: + WindowsProvider windowsProvider = new WindowsProvider(clientId, scopes); + ProviderManager.Instance.GlobalProvider = windowsProvider; + await windowsProvider.TrySilentLoginAsync(); + break; + } + }); } /// @@ -81,8 +87,6 @@ private void InitializeGlobalProvider() /// Details about the launch request and process. protected override void OnLaunched(LaunchActivatedEventArgs e) { - Task.Run(InitializeGlobalProvider); - Frame rootFrame = Window.Current.Content as Frame; // Do not repeat app initialization when the Window already has content, @@ -114,6 +118,8 @@ protected override void OnLaunched(LaunchActivatedEventArgs e) } // Ensure the current window is active Window.Current.Activate(); + + InitializeGlobalProvider(); } } From 080b9f249a93740c2a57bdbb526ebc8192379e11 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Fri, 2 Apr 2021 14:44:05 -0700 Subject: [PATCH 07/24] Removed empty line --- SampleTest/App.xaml.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/SampleTest/App.xaml.cs b/SampleTest/App.xaml.cs index f40d65d..dcd74a5 100644 --- a/SampleTest/App.xaml.cs +++ b/SampleTest/App.xaml.cs @@ -28,7 +28,6 @@ public App() this.Suspending += OnSuspending; } - // Which provider should be used for authentication? private readonly ProviderType _providerType = ProviderType.Mock; From 9d22f6e78adada0f30169860307da8d9b959ac4f Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 6 Apr 2021 12:57:48 -0700 Subject: [PATCH 08/24] Added configuration options for the AccountsSettingsPane --- .../WindowsProvider.cs | 131 +++++++++++++----- SampleTest/App.xaml.cs | 14 +- 2 files changed, 111 insertions(+), 34 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 053254d..0cf4a18 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -13,6 +13,27 @@ namespace CommunityToolkit.Uwp.Authentication { + /// + /// Enumeration of available authentication providers. + /// + public enum WebAccountProviderType + { + /// + /// Enable public MSAs to authenticate. + /// + Consumer, + + /// + /// Enable organizational accounts to authenticate. + /// + Organizational, + + /// + /// Enable both consumer and organizational accounts to authenticate. + /// + All, + } + /// /// An authentication provider based on the native AccountSettingsPane in Windows. /// @@ -25,44 +46,46 @@ public class WindowsProvider : BaseProvider private const string GraphResourceProperty = "https://graph.microsoft.com"; private const string MicrosoftProviderId = "https://login.microsoft.com"; - //// private const string AzureActiveDirectoryAuthority = "organizations"; - //// private const string MicrosoftAccountAuthority = "consumers"; + private const string AzureActiveDirectoryAuthority = "organizations"; + private const string MicrosoftAccountAuthority = "consumers"; //// private const string MicrosoftAccountClientId = "none"; //// private const string MicrosoftAccountScopeRequested = "wl.basic"; + private const string SettingsKeyAccountId = "WindowsProvider_AccountId"; + private const string SettingsKeyProviderId = "WindowsProvider_ProviderId"; private static readonly string[] DefaultScopes = { "User.Read", }; + private static readonly AccountsSettingsPaneConfig DefaultAccountsSettingsPaneConfig = + new AccountsSettingsPaneConfig() + { + WebAccountProviderType = WebAccountProviderType.All, + }; + /// /// Gets a cache of important values for the signed in user. /// protected IDictionary Settings => ApplicationData.Current.LocalSettings.Values; - /// - /// The settings key for the active account id. - /// - protected const string SettingsKeyWamAccountId = "WamAccountId"; - - /// - /// The settings key for the active provider id. - /// - protected const string SettingsKeyWamProviderId = "WamProviderId"; - private string _clientId; private string[] _scopes; private WebAccount _webAccount; + private AccountsSettingsPaneConfig _accountsSettingsPaneConfig; /// /// Initializes a new instance of the class. /// /// Registered ClientId. /// List of Scopes to initially request. - public WindowsProvider(string clientId, string[] scopes = null) + /// Configuration values for the AccountsSettingsPane. + public WindowsProvider(string clientId, string[] scopes = null, AccountsSettingsPaneConfig? accountsSettingsPaneConfig = null) { _clientId = clientId; _scopes = scopes ?? DefaultScopes; + _accountsSettingsPaneConfig = accountsSettingsPaneConfig ?? DefaultAccountsSettingsPaneConfig; + _webAccount = null; State = ProviderState.SignedOut; @@ -115,8 +138,8 @@ public async Task TrySilentLoginAsync() /// public override async Task LogoutAsync() { - Settings.Remove(SettingsKeyWamAccountId); - Settings.Remove(SettingsKeyWamProviderId); + Settings.Remove(SettingsKeyAccountId); + Settings.Remove(SettingsKeyProviderId); if (_webAccount != null) { @@ -221,8 +244,8 @@ private async Task SetAccountAsync(WebAccount account) // Save off the account ids. _webAccount = account; - Settings[SettingsKeyWamAccountId] = account.Id; - Settings[SettingsKeyWamProviderId] = account.WebAccountProvider.Id; + Settings[SettingsKeyAccountId] = account.Id; + Settings[SettingsKeyProviderId] = account.WebAccountProvider.Id; SetState(ProviderState.SignedIn); } @@ -237,8 +260,8 @@ private async Task AuthenticateSilentAsync() if (account == null) { // Check the cache for an existing user - if (Settings[SettingsKeyWamAccountId] is string savedAccountId && - Settings[SettingsKeyWamProviderId] is string savedProviderId) + if (Settings[SettingsKeyAccountId] is string savedAccountId && + Settings[SettingsKeyProviderId] is string savedProviderId) { var savedProvider = await WebAuthenticationCoreManager.FindAccountProviderAsync(savedProviderId); account = await WebAuthenticationCoreManager.FindAccountAsync(savedProvider, savedAccountId); @@ -320,7 +343,7 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti try { // Configure available providers. - List webAccountProviders = await GetWebAccountProvidersAsync(); + List webAccountProviders = await GetWebAccountProvidersAsync(_accountsSettingsPaneConfig.WebAccountProviderType); foreach (WebAccountProvider webAccountProvider in webAccountProviders) { @@ -328,7 +351,21 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti e.WebAccountProviderCommands.Add(providerCommand); } - // TODO: Expose configuration so developers can have some control over the popup. + // Expose configuration so developers can have some control over the popup. + var headerText = _accountsSettingsPaneConfig.HeaderText; + if (!string.IsNullOrWhiteSpace(headerText)) + { + e.HeaderText = headerText; + } + + var commands = _accountsSettingsPaneConfig.Commands; + if (commands != null) + { + foreach (var command in commands) + { + e.Commands.Add(command); + } + } } catch (Exception ex) { @@ -365,25 +402,32 @@ private WebTokenRequest GetWebTokenRequest(WebAccountProvider provider) return webTokenRequest; } - private async Task> GetWebAccountProvidersAsync() + private async Task> GetWebAccountProvidersAsync(WebAccountProviderType providerType) { List providers = new List(); - // TODO: Enable devs to turn on/off which account sources they wish to integrate with. - - // MSA - Works - // return await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, MicrosoftAccountAuthority); - - // AAD - Fails complaining about 'client_assertion' or 'client_secret' - // return await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, AzureActiveDirectoryAuthority); - - // Both - providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId)); + switch (providerType) + { + case WebAccountProviderType.Consumer: + // MSA + providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, MicrosoftAccountAuthority)); + break; + + case WebAccountProviderType.Organizational: + // AAD - Fails complaining about 'client_assertion' or 'client_secret' + providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, AzureActiveDirectoryAuthority)); + break; + + case WebAccountProviderType.All: + default: + // Both + providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId)); + break; + } return providers; } - // Instead of updating the State value directly, use SetState to ensure that provider events are fired on the proper thread. private void SetState(ProviderState state) { if (State == state) @@ -394,4 +438,25 @@ private void SetState(ProviderState state) State = state; } } + + /// + /// Configuration values for the AccountsSettingsPane. + /// + public struct AccountsSettingsPaneConfig + { + /// + /// Gets or sets the header text for the accounts settings pane. + /// + public string HeaderText { get; set; } + + /// + /// Gets or sets the SettingsCommand collection for the account settings pane. + /// + public IList Commands { get; set; } + + /// + /// Gets or sets the type of authentication providers that should be available through the accounts settings pane. + /// + public WebAccountProviderType WebAccountProviderType { get; set; } + } } diff --git a/SampleTest/App.xaml.cs b/SampleTest/App.xaml.cs index dcd74a5..71c8495 100644 --- a/SampleTest/App.xaml.cs +++ b/SampleTest/App.xaml.cs @@ -5,8 +5,11 @@ using CommunityToolkit.Net.Authentication; using CommunityToolkit.Uwp.Authentication; using System; +using System.Collections.Generic; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; +using Windows.UI.ApplicationSettings; +using Windows.UI.Popups; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Navigation; @@ -71,7 +74,11 @@ await Window.Current.Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriori // Windows provider case ProviderType.Windows: - WindowsProvider windowsProvider = new WindowsProvider(clientId, scopes); + WindowsProvider windowsProvider = new WindowsProvider(clientId, scopes, new AccountsSettingsPaneConfig() + { + HeaderText = "Custom header text goes here.", + Commands = new List() { new SettingsCommand("MySettingsCommandId", "Click me!", OnSettingsCommandInvoked) } + }); ProviderManager.Instance.GlobalProvider = windowsProvider; await windowsProvider.TrySilentLoginAsync(); break; @@ -79,6 +86,11 @@ await Window.Current.Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriori }); } + private void OnSettingsCommandInvoked(IUICommand command) + { + System.Diagnostics.Debug.WriteLine("AccountsSettingsPane command invoked: " + command.Id); + } + /// /// Invoked when the application is launched normally by the end user. Other entry points /// will be used such as when the application is launched to open a specific file. From 072092468c220024d7f0d360de390ea968c26969 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 6 Apr 2021 13:21:04 -0700 Subject: [PATCH 09/24] Fixed pane cancellation in WindowsProvider --- .../WindowsProvider.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 0cf4a18..e0caf4c 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -363,7 +363,11 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti { foreach (var command in commands) { - e.Commands.Add(command); + e.Commands.Add(new SettingsCommand(command.Id, command.Label, (uic) => + { + command.Invoked.Invoke(command); + addAccountTaskCompletionSource.SetCanceled(); + })); } } } @@ -388,6 +392,11 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti var authResult = await addAccountTaskCompletionSource.Task; return authResult; } + catch (TaskCanceledException) + { + // The task was cancelled. Do nothing. + return null; + } finally { pane.AccountCommandsRequested -= OnAccountCommandsRequested; From 50027e4c0a32583d572d5d847b91d8a87fa0a4bb Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 6 Apr 2021 13:21:51 -0700 Subject: [PATCH 10/24] Updates to sample --- SampleTest/App.xaml.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SampleTest/App.xaml.cs b/SampleTest/App.xaml.cs index 71c8495..4a3fb39 100644 --- a/SampleTest/App.xaml.cs +++ b/SampleTest/App.xaml.cs @@ -74,10 +74,11 @@ await Window.Current.Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriori // Windows provider case ProviderType.Windows: + var settingsCommandId = Guid.NewGuid(); WindowsProvider windowsProvider = new WindowsProvider(clientId, scopes, new AccountsSettingsPaneConfig() { HeaderText = "Custom header text goes here.", - Commands = new List() { new SettingsCommand("MySettingsCommandId", "Click me!", OnSettingsCommandInvoked) } + Commands = new List() { new SettingsCommand(settingsCommandId, "Click me!", OnSettingsCommandInvoked) } }); ProviderManager.Instance.GlobalProvider = windowsProvider; await windowsProvider.TrySilentLoginAsync(); @@ -88,7 +89,7 @@ await Window.Current.Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriori private void OnSettingsCommandInvoked(IUICommand command) { - System.Diagnostics.Debug.WriteLine("AccountsSettingsPane command invoked: " + command.Id); + System.Diagnostics.Debug.WriteLine("AccountsSettingsPane command invoked: " + command.Label); } /// From aa2058519808c6dc56defc9f6db20e6b8b60a881 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 6 Apr 2021 13:34:36 -0700 Subject: [PATCH 11/24] adjusting provider creation --- .../WindowsProvider.cs | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index e0caf4c..da13614 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -415,23 +415,16 @@ private async Task> GetWebAccountProvidersAsync(WebAcco { List providers = new List(); - switch (providerType) + if (providerType == WebAccountProviderType.Consumer || providerType == WebAccountProviderType.All) { - case WebAccountProviderType.Consumer: - // MSA - providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, MicrosoftAccountAuthority)); - break; - - case WebAccountProviderType.Organizational: - // AAD - Fails complaining about 'client_assertion' or 'client_secret' - providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, AzureActiveDirectoryAuthority)); - break; - - case WebAccountProviderType.All: - default: - // Both - providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId)); - break; + // MSA + providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, MicrosoftAccountAuthority)); + } + + if (providerType == WebAccountProviderType.Organizational || providerType == WebAccountProviderType.All) + { + // AAD - Works pre-store association. Once associated, fails complaining about 'client_assertion' or 'client_secret' for corp account. + providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, AzureActiveDirectoryAuthority)); } return providers; From f85e655c2502f4391f5c177643921a22db346de1 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 6 Apr 2021 14:12:59 -0700 Subject: [PATCH 12/24] tweaks and comments --- .../WindowsProvider.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index da13614..b1adfd9 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -58,12 +58,6 @@ public class WindowsProvider : BaseProvider "User.Read", }; - private static readonly AccountsSettingsPaneConfig DefaultAccountsSettingsPaneConfig = - new AccountsSettingsPaneConfig() - { - WebAccountProviderType = WebAccountProviderType.All, - }; - /// /// Gets a cache of important values for the signed in user. /// @@ -84,7 +78,7 @@ public WindowsProvider(string clientId, string[] scopes = null, AccountsSettings { _clientId = clientId; _scopes = scopes ?? DefaultScopes; - _accountsSettingsPaneConfig = accountsSettingsPaneConfig ?? DefaultAccountsSettingsPaneConfig; + _accountsSettingsPaneConfig = accountsSettingsPaneConfig ?? default; _webAccount = null; @@ -319,9 +313,12 @@ private async Task ShowAddAccountAndGetResultAsync() // To ensure no funny business, the entire AccountSettingsPane flow is contained here. var addAccountTaskCompletionSource = new TaskCompletionSource(); + bool webAccountProviderCommandWasInvoked = false; + // Handle the selected account provider async void WebAccountProviderCommandInvoked(WebAccountProviderCommand command) { + webAccountProviderCommandWasInvoked = true; try { var webTokenRequest = GetWebTokenRequest(command.WebAccountProvider); @@ -389,7 +386,7 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti // Show the AccountSettingsPane and wait for the result. await AccountsSettingsPane.ShowAddAccountAsync(); - var authResult = await addAccountTaskCompletionSource.Task; + var authResult = webAccountProviderCommandWasInvoked ? await addAccountTaskCompletionSource.Task : null; return authResult; } catch (TaskCanceledException) @@ -460,5 +457,18 @@ public struct AccountsSettingsPaneConfig /// Gets or sets the type of authentication providers that should be available through the accounts settings pane. /// public WebAccountProviderType WebAccountProviderType { get; set; } + + /// + /// Initializes a new instance of the struct. + /// + /// The header text for the accounts settings pane. + /// The SettingsCommand collection for the account settings pane. + /// The type of authentication providers that should be available through the accounts settings pane. + public AccountsSettingsPaneConfig(string headerText, IList commands, WebAccountProviderType webAccountProviderType = WebAccountProviderType.Consumer) + { + HeaderText = headerText; + Commands = commands; + WebAccountProviderType = webAccountProviderType; + } } } From 89d913c4ac605a36e438740aa4250bcafb0d84c5 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 6 Apr 2021 14:22:34 -0700 Subject: [PATCH 13/24] Exposed GetTokenAsync method --- CommunityToolkit.Uwp.Authentication/WindowsProvider.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index b1adfd9..8ce0cd5 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -152,7 +152,12 @@ public override async Task LogoutAsync() SetState(ProviderState.SignedOut); } - private async Task GetTokenAsync(bool silentOnly = false) + /// + /// Retrieve a token for the authenticated user. + /// + /// Determines if the acquisition should be done without prompts to the user. + /// A token string for the authenticated user. + public async Task GetTokenAsync(bool silentOnly = false) { var internetConnectionProfile = NetworkInformation.GetInternetConnectionProfile(); if (internetConnectionProfile == null) From 3ff40f2da0ad7b9d0c6b2299d857efa123d76cfc Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 6 Apr 2021 16:58:46 -0700 Subject: [PATCH 14/24] Removed hooks for AAD login, which was broken anyway --- .../WindowsProvider.cs | 103 +++++------------- 1 file changed, 30 insertions(+), 73 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 8ce0cd5..9363ef0 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -1,4 +1,8 @@ -using System; +// 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.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; @@ -13,27 +17,6 @@ namespace CommunityToolkit.Uwp.Authentication { - /// - /// Enumeration of available authentication providers. - /// - public enum WebAccountProviderType - { - /// - /// Enable public MSAs to authenticate. - /// - Consumer, - - /// - /// Enable organizational accounts to authenticate. - /// - Organizational, - - /// - /// Enable both consumer and organizational accounts to authenticate. - /// - All, - } - /// /// An authentication provider based on the native AccountSettingsPane in Windows. /// @@ -44,19 +27,16 @@ public class WindowsProvider : BaseProvider /// public static string RedirectUri => string.Format("ms-appx-web://Microsoft.AAD.BrokerPlugIn/{0}", WebAuthenticationBroker.GetCurrentApplicationCallbackUri().Host.ToUpper()); - private const string GraphResourceProperty = "https://graph.microsoft.com"; - private const string MicrosoftProviderId = "https://login.microsoft.com"; - private const string AzureActiveDirectoryAuthority = "organizations"; + private const string AuthenticationHeaderScheme = "Bearer"; + private const string GraphResourcePropertyKey = "resource"; + private const string GraphResourcePropertyValue = "https://graph.microsoft.com"; private const string MicrosoftAccountAuthority = "consumers"; - //// private const string MicrosoftAccountClientId = "none"; - //// private const string MicrosoftAccountScopeRequested = "wl.basic"; + private const string MicrosoftProviderId = "https://login.microsoft.com"; private const string SettingsKeyAccountId = "WindowsProvider_AccountId"; private const string SettingsKeyProviderId = "WindowsProvider_ProviderId"; - private static readonly string[] DefaultScopes = - { - "User.Read", - }; + // Default/minimal scopes for authentication, if none are provided. + private static readonly string[] DefaultScopes = { "User.Read" }; /// /// Gets a cache of important values for the signed in user. @@ -66,7 +46,7 @@ public class WindowsProvider : BaseProvider private string _clientId; private string[] _scopes; private WebAccount _webAccount; - private AccountsSettingsPaneConfig _accountsSettingsPaneConfig; + private AccountsSettingsPaneConfig? _accountsSettingsPaneConfig; /// /// Initializes a new instance of the class. @@ -78,7 +58,7 @@ public WindowsProvider(string clientId, string[] scopes = null, AccountsSettings { _clientId = clientId; _scopes = scopes ?? DefaultScopes; - _accountsSettingsPaneConfig = accountsSettingsPaneConfig ?? default; + _accountsSettingsPaneConfig = accountsSettingsPaneConfig; _webAccount = null; @@ -89,7 +69,7 @@ public WindowsProvider(string clientId, string[] scopes = null, AccountsSettings public override async Task AuthenticateRequestAsync(HttpRequestMessage request) { string token = await GetTokenAsync(); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + request.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationHeaderScheme, token); } /// @@ -149,7 +129,7 @@ public override async Task LogoutAsync() _webAccount = null; } - SetState(ProviderState.SignedOut); + State = ProviderState.SignedOut; } /// @@ -171,7 +151,7 @@ public async Task GetTokenAsync(bool silentOnly = false) { if (State == ProviderState.SignedOut) { - SetState(ProviderState.Loading); + State = ProviderState.Loading; } // Attempt to authenticate silently. @@ -246,7 +226,7 @@ private async Task SetAccountAsync(WebAccount account) Settings[SettingsKeyAccountId] = account.Id; Settings[SettingsKeyProviderId] = account.WebAccountProvider.Id; - SetState(ProviderState.SignedIn); + State = ProviderState.SignedIn; } private async Task AuthenticateSilentAsync() @@ -345,7 +325,7 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti try { // Configure available providers. - List webAccountProviders = await GetWebAccountProvidersAsync(_accountsSettingsPaneConfig.WebAccountProviderType); + List webAccountProviders = await GetWebAccountProvidersAsync(); foreach (WebAccountProvider webAccountProvider in webAccountProviders) { @@ -353,14 +333,15 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti e.WebAccountProviderCommands.Add(providerCommand); } - // Expose configuration so developers can have some control over the popup. - var headerText = _accountsSettingsPaneConfig.HeaderText; + // Apply the configured header. + var headerText = _accountsSettingsPaneConfig?.HeaderText; if (!string.IsNullOrWhiteSpace(headerText)) { e.HeaderText = headerText; } - var commands = _accountsSettingsPaneConfig.Commands; + // Apply any configured commands. + var commands = _accountsSettingsPaneConfig?.Commands; if (commands != null) { foreach (var command in commands) @@ -391,6 +372,8 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti // Show the AccountSettingsPane and wait for the result. await AccountsSettingsPane.ShowAddAccountAsync(); + // If an account was selected, the WebAccountProviderCommand will be invoked. + // If not, the AccountsSettingsPane must have been cancelled or closed. var authResult = webAccountProviderCommandWasInvoked ? await addAccountTaskCompletionSource.Task : null; return authResult; } @@ -408,39 +391,20 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti private WebTokenRequest GetWebTokenRequest(WebAccountProvider provider) { WebTokenRequest webTokenRequest = new WebTokenRequest(provider, string.Join(',', _scopes), _clientId); - webTokenRequest.Properties.Add("resource", GraphResourceProperty); + webTokenRequest.Properties.Add(GraphResourcePropertyKey, GraphResourcePropertyValue); return webTokenRequest; } - private async Task> GetWebAccountProvidersAsync(WebAccountProviderType providerType) + private async Task> GetWebAccountProvidersAsync() { - List providers = new List(); - - if (providerType == WebAccountProviderType.Consumer || providerType == WebAccountProviderType.All) - { - // MSA - providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, MicrosoftAccountAuthority)); - } + var providers = new List(); - if (providerType == WebAccountProviderType.Organizational || providerType == WebAccountProviderType.All) - { - // AAD - Works pre-store association. Once associated, fails complaining about 'client_assertion' or 'client_secret' for corp account. - providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, AzureActiveDirectoryAuthority)); - } + // MSA + providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, MicrosoftAccountAuthority)); return providers; } - - private void SetState(ProviderState state) - { - if (State == state) - { - return; - } - - State = state; - } } /// @@ -458,22 +422,15 @@ public struct AccountsSettingsPaneConfig /// public IList Commands { get; set; } - /// - /// Gets or sets the type of authentication providers that should be available through the accounts settings pane. - /// - public WebAccountProviderType WebAccountProviderType { get; set; } - /// /// Initializes a new instance of the struct. /// /// The header text for the accounts settings pane. /// The SettingsCommand collection for the account settings pane. - /// The type of authentication providers that should be available through the accounts settings pane. - public AccountsSettingsPaneConfig(string headerText, IList commands, WebAccountProviderType webAccountProviderType = WebAccountProviderType.Consumer) + public AccountsSettingsPaneConfig(string headerText, IList commands) { HeaderText = headerText; Commands = commands; - WebAccountProviderType = webAccountProviderType; } } } From ff3f48b50b86b3688f1e47686be44c2c2c9bf7e8 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 6 Apr 2021 17:01:49 -0700 Subject: [PATCH 15/24] Adding WindowsProvider tests --- .../Providers/Test_WindowsProvider.cs | 101 ++++++++++++++++++ UnitTests/UnitTests.UWP/UnitTests.UWP.csproj | 5 + 2 files changed, 106 insertions(+) create mode 100644 UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs diff --git a/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs b/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs new file mode 100644 index 0000000..c2c9d89 --- /dev/null +++ b/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs @@ -0,0 +1,101 @@ +// 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 CommunityToolkit.Net.Authentication; +using CommunityToolkit.Uwp.Authentication; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Threading.Tasks; + +namespace UnitTests.UWP.Authentication +{ + [TestClass] + public class Test_WindowsProvider + { + private const string ClientId = ""; + + /// + /// Create a new instance of the WindowsProvider and check that is has the proper default state. + /// + [TestCategory("Providers")] + [TestMethod] + public void Test_WindowsProvider_Default() + { + WindowsProvider provider = new WindowsProvider(ClientId); + + Assert.AreEqual(ProviderState.SignedOut, provider.State); + } + + /// + /// Create a new instance of the MockProvider and initiates login. + /// The test checks that the appropriate events are fired and that the provider transitions + /// through the different states as expected. + /// + [TestCategory("Providers")] + [TestMethod] + public async Task Test_WindowsProvider_LoginAsync() + { + // Create the new provider. + WindowsProvider provider = new WindowsProvider(ClientId); + + // Run logout to ensure that no cached users affect the test. + await provider.LogoutAsync(); + + // The newly created provider should be in a logged out state. + Assert.AreEqual(ProviderState.SignedOut, provider.State); + + // Listen for changes in the provider state and count them. + int eventCount = 0; + provider.StateChanged += (s, e) => + { + eventCount += 1; + + // Ensure that the states are properly reported through the StateChanged event. + switch (e.OldState) + { + case ProviderState.SignedOut: + // Login has been initiated, the provider should now be loading. + Assert.AreEqual(ProviderState.Loading, e.NewState); + + // Loading should be the first event fired. + Assert.AreEqual(eventCount, 1); + break; + + case ProviderState.Loading: + // The provider has completed login, the provider should now be signed in. + Assert.AreEqual(ProviderState.SignedIn, e.NewState); + + // SignedIn should be the second event fired. + Assert.AreEqual(eventCount, 2); + break; + + case ProviderState.SignedIn: + // The provider has completed login, the provider should now be signed in. + Assert.AreEqual(ProviderState.SignedOut, e.NewState); + + // SignedIn should be the second event fired. + Assert.AreEqual(eventCount, 3); + break; + + default: + // This is unexpected, something went wrong during the test. + Assert.Fail("The provider has transitioned from an unexpected state: " + Enum.GetName(typeof(ProviderState), e.OldState)); + break; + } + }; + + // Initiate logout. + await provider.LoginAsync(); + + // Logout has completed, the provider should be signed out. + Assert.AreEqual(ProviderState.SignedIn, provider.State); + + // Initiate logout, which should skip loading, and go straight to signed out. + await provider.LogoutAsync(); + + // Ensure the proper number of events were fired. + Assert.AreEqual(eventCount, 3); + } + } +} diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj index ca5b3a9..6e55334 100644 --- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj +++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj @@ -122,6 +122,7 @@ + UnitTestApp.xaml @@ -191,6 +192,10 @@ {b323a2e1-66ef-4037-95b7-2defa051b4b1} CommunityToolkit.Net.Authentication + + {2E4A708A-DF53-4863-B797-E14CDC6B90FA} + CommunityToolkit.Uwp.Authentication + {42252EE8-7E68-428F-972B-6D2DD3AA12CC} CommunityToolkit.Uwp.Graph.Controls From 279096601bdf8d42796d37cdfe981a1c3e346166 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Wed, 7 Apr 2021 12:40:33 -0700 Subject: [PATCH 16/24] Added more comments and adjusted function naming --- .../WindowsProvider.cs | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 9363ef0..a487a54 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -93,20 +93,11 @@ public override async Task LoginAsync() /// Try logging in silently. /// /// A representing the asynchronous operation. - public async Task TrySilentLoginAsync() + public async Task TrySilentLoginAsync() { - if (_webAccount != null || State != ProviderState.SignedOut) - { - return; - } - // The state will get updated as part of the auth flow. var token = await GetTokenAsync(true); - - if (token == null) - { - await LogoutAsync(); - } + return token != null; } /// @@ -271,12 +262,17 @@ private async Task AuthenticateInteractiveAsync() var account = _webAccount; if (account != null) { - var webTokenRequest = GetWebTokenRequest(account.WebAccountProvider); + // We already have the account. + var webAccountProvider = account.WebAccountProvider; + var webTokenRequest = GetWebTokenRequest(webAccountProvider); authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest, account); } else { - authResult = await ShowAddAccountAndGetResultAsync(); + // We don't have an account. Prompt the user to provide one. + var webAccountProvider = await ShowAccountSettingsPaneAndGetProviderAsync(); + var webTokenRequest = GetWebTokenRequest(webAccountProvider); + authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest); } return authResult; @@ -290,30 +286,27 @@ private async Task AuthenticateInteractiveAsync() /// /// Show the AccountSettingsPane and wait for the user to make a selection, then process the authentication result. /// - private async Task ShowAddAccountAndGetResultAsync() + private async Task ShowAccountSettingsPaneAndGetProviderAsync() { // The AccountSettingsPane uses events to support the flow of authentication events. // Ultimately we need access to the user's selected account provider from the AccountSettingsPane, which is available // in the WebAccountProviderCommandInvoked function for the chosen provider. - // To ensure no funny business, the entire AccountSettingsPane flow is contained here. - var addAccountTaskCompletionSource = new TaskCompletionSource(); + // The entire AccountSettingsPane flow is contained here. + var webAccountProviderTaskCompletionSource = new TaskCompletionSource(); bool webAccountProviderCommandWasInvoked = false; // Handle the selected account provider - async void WebAccountProviderCommandInvoked(WebAccountProviderCommand command) + void WebAccountProviderCommandInvoked(WebAccountProviderCommand command) { webAccountProviderCommandWasInvoked = true; try { - var webTokenRequest = GetWebTokenRequest(command.WebAccountProvider); - - var authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest); - addAccountTaskCompletionSource.SetResult(authResult); + webAccountProviderTaskCompletionSource.SetResult(command.WebAccountProvider); } catch (Exception ex) { - addAccountTaskCompletionSource.SetException(ex); + webAccountProviderTaskCompletionSource.SetException(ex); } } @@ -346,17 +339,19 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti { foreach (var command in commands) { + // We don't actually use the provided commands directly. Instead, we make new commands + // with matching ids and labels, but we override the invoked action so we can cancel the TaskCompletionSource. e.Commands.Add(new SettingsCommand(command.Id, command.Label, (uic) => { command.Invoked.Invoke(command); - addAccountTaskCompletionSource.SetCanceled(); + webAccountProviderTaskCompletionSource.SetCanceled(); })); } } } catch (Exception ex) { - addAccountTaskCompletionSource.SetException(ex); + webAccountProviderTaskCompletionSource.SetException(ex); } finally { @@ -374,12 +369,12 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti // If an account was selected, the WebAccountProviderCommand will be invoked. // If not, the AccountsSettingsPane must have been cancelled or closed. - var authResult = webAccountProviderCommandWasInvoked ? await addAccountTaskCompletionSource.Task : null; - return authResult; + var webAccountProvider = webAccountProviderCommandWasInvoked ? await webAccountProviderTaskCompletionSource.Task : null; + return webAccountProvider; } catch (TaskCanceledException) { - // The task was cancelled. Do nothing. + // The task was cancelled. No provider was chosen. return null; } finally From 550fb6bd5623033b559b45d5de12f1ecfab3c4e4 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Wed, 7 Apr 2021 14:30:35 -0700 Subject: [PATCH 17/24] Added docs --- .../WindowsProvider.cs | 15 ++++ Docs/WindowsProvider.md | 81 +++++++++++++++++++ Windows-Toolkit-Graph-Controls.sln | 6 +- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 Docs/WindowsProvider.md diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index a487a54..3f73f8f 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -38,6 +38,21 @@ public class WindowsProvider : BaseProvider // Default/minimal scopes for authentication, if none are provided. private static readonly string[] DefaultScopes = { "User.Read" }; + /// + /// Gets the client id obtained from Azure registration. + /// + public string ClientId => _clientId; + + /// + /// Gets the list of scopes to pre-authorize during authentication. + /// + public string[] Scopes => _scopes; + + /// + /// Gets configuration values for the AccountsSettingsPane. + /// + public AccountsSettingsPaneConfig? AccountsSettingsPaneConfig => _accountsSettingsPaneConfig; + /// /// Gets a cache of important values for the signed in user. /// diff --git a/Docs/WindowsProvider.md b/Docs/WindowsProvider.md new file mode 100644 index 0000000..3c04f5a --- /dev/null +++ b/Docs/WindowsProvider.md @@ -0,0 +1,81 @@ +# WindowsProvider + +The WindowsProvider is an authentication provider for accessing locally configured accounts on Windows. +It extends IProvider and uses the native AccountsSettingsPane APIs for login. + +> [!IMPORTANT] +> Be sure to Register Client Id in Azure first following the guidance here: +> +> After finishing the initial registration page, you will also need to add an additional redirect URI. Click on "Add a Redirect URI" and check the "https://login.microsoftonline.com/common/oauth2/nativeclient" checkbox on that page. +> +> You'll also want to set the toggle to true for "Allow public client flows". +> +> Then click "Save". + +## Syntax + +```CSharp +// Simple +string clientId = "YOUR_CLIENT_ID_HERE"; +string[] scopes = new string[] { "User.Read" }; + +ProviderManager.Instance.GlobalProvider = new WindowsProvider(clientId, scopes); + +// Customized AccountsSettingsPane +IList commands = new List() +{ + new SettingsCommand(Guid.NewGuid(), "Click me!", OnSettingsCommandInvoked) +} +AccountsSettingsPaneConfig accountsSettingsPaneConfig = new AccountsSettingsPaneConfig() +{ + HeaderText = "Custom header text goes here.", + Commands = commands +} + +WindowsProvider windowsProvider = new WindowsProvider(clientId, scopes, accountsSettingsPaneConfig); +ProviderManager.Instance.GlobalProvider = windowsProvider; + +// Silent login +await windowsProvider.TrySilentLoginAsync(); +``` + +## Prerequisite Windows Store Association in Visual Studio +To get valid tokens and complete login, the app will need to be associated with the Microsoft Store. + +1. In Visual Studio Solution Explorer, right-click the UWP project, then select **Store -> Associate App with the Store...** + +2. In the wizard, click **Next**, sign in with your Windows developer account, type a name for your app in **Reserve a new app name**, then click **Reserve**. + +3. After completing the app registration, select the new app name, click **Next**, and then click **Associate**. This adds the required Windows Store registration information to the application manifest. + +> [!NOTE] +> You must have a Windows Developer account to use the WindowsProvider in your UWP app. You can [register a Microsoft developer account](https://developer.microsoft.com/store/register) if you don't already have one. + + +## Properties + +See IProvider for a full list of supported properties. + +| Property | Type | Description | +| -- | -- | -- | +| ClientId | string | Client Id obtained from Azure registration. | +| Scopes | ScopeSet | Comma-delimited list of scopes to pre-authorize from user during authentication. | +| AccountsSettingsPaneConfig | AccountsSettingsPaneConfig | Configuration values for the AccountsSettingsPane, shown during authentication. | +| RedirectUri | string | Static getter for retrieving a customized redirect uri to put in the Azure app registration. | + +### AccountsSettingsPane + +| Property | Type | Description | +| -- | -- | -- | +| HeaderText | string | Gets or sets the header text for the accounts settings pane. | +| Commands | IList | Gets or sets the SettingsCommand collection for the account settings pane. | + + +## Methods + +See IProvider for a full list of supported methods. + +| Method | Arguments | Returns | Description | +| -- | -- | -- | -- | +| GetTokenAsync | bool silentOnly = true | Task | Retrieve a token for the authenticated user. | +| TrySilentLoginAsync | | Task | Try logging in silently, without prompts. | diff --git a/Windows-Toolkit-Graph-Controls.sln b/Windows-Toolkit-Graph-Controls.sln index e5e6c7c..1455ceb 100644 --- a/Windows-Toolkit-Graph-Controls.sln +++ b/Windows-Toolkit-Graph-Controls.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29230.61 @@ -36,6 +35,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTests", "UnitTests", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.UWP", "UnitTests\UnitTests.UWP\UnitTests.UWP.csproj", "{6B33B26C-008B-4ADB-B317-EF996CD6755B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{EACBCDF3-8CCC-4A30-87E9-75FCA815831E}" + ProjectSection(SolutionItems) = preProject + Docs\WindowsProvider.md = Docs\WindowsProvider.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CI|Any CPU = CI|Any CPU From 73df92739a72dd29b09d43090d8b5af12e2236ac Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 8 Apr 2021 12:53:57 -0700 Subject: [PATCH 18/24] Working test! --- .../WindowsProvider.cs | 13 +- .../Providers/Test_WindowsProvider.cs | 118 +++++++++--------- 2 files changed, 70 insertions(+), 61 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 3f73f8f..10f8cd3 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -374,11 +374,13 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti } } - var pane = AccountsSettingsPane.GetForCurrentView(); - pane.AccountCommandsRequested += OnAccountCommandsRequested; - + AccountsSettingsPane pane = null; try { + // GetForCurrentView may throw an exception if the current view isn't ready yet. + pane = AccountsSettingsPane.GetForCurrentView(); + pane.AccountCommandsRequested += OnAccountCommandsRequested; + // Show the AccountSettingsPane and wait for the result. await AccountsSettingsPane.ShowAddAccountAsync(); @@ -394,7 +396,10 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti } finally { - pane.AccountCommandsRequested -= OnAccountCommandsRequested; + if (pane != null) + { + pane.AccountCommandsRequested -= OnAccountCommandsRequested; + } } } diff --git a/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs b/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs index c2c9d89..b1ead43 100644 --- a/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs +++ b/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs @@ -4,6 +4,7 @@ using CommunityToolkit.Net.Authentication; using CommunityToolkit.Uwp.Authentication; +using Microsoft.Toolkit.Uwp; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Threading.Tasks; @@ -11,7 +12,7 @@ namespace UnitTests.UWP.Authentication { [TestClass] - public class Test_WindowsProvider + public class Test_WindowsProvider : VisualUITestBase { private const string ClientId = ""; @@ -36,66 +37,69 @@ public void Test_WindowsProvider_Default() [TestMethod] public async Task Test_WindowsProvider_LoginAsync() { - // Create the new provider. - WindowsProvider provider = new WindowsProvider(ClientId); - - // Run logout to ensure that no cached users affect the test. - await provider.LogoutAsync(); - - // The newly created provider should be in a logged out state. - Assert.AreEqual(ProviderState.SignedOut, provider.State); - - // Listen for changes in the provider state and count them. - int eventCount = 0; - provider.StateChanged += (s, e) => + await App.DispatcherQueue.EnqueueAsync(async () => { - eventCount += 1; - - // Ensure that the states are properly reported through the StateChanged event. - switch (e.OldState) - { - case ProviderState.SignedOut: - // Login has been initiated, the provider should now be loading. - Assert.AreEqual(ProviderState.Loading, e.NewState); - - // Loading should be the first event fired. - Assert.AreEqual(eventCount, 1); - break; + // Create the new provider. + WindowsProvider provider = new WindowsProvider(ClientId); - case ProviderState.Loading: - // The provider has completed login, the provider should now be signed in. - Assert.AreEqual(ProviderState.SignedIn, e.NewState); + // Run logout to ensure that no cached users affect the test. + await provider.LogoutAsync(); - // SignedIn should be the second event fired. - Assert.AreEqual(eventCount, 2); - break; + // The newly created provider should be in a logged out state. + Assert.AreEqual(ProviderState.SignedOut, provider.State); - case ProviderState.SignedIn: - // The provider has completed login, the provider should now be signed in. - Assert.AreEqual(ProviderState.SignedOut, e.NewState); - - // SignedIn should be the second event fired. - Assert.AreEqual(eventCount, 3); - break; - - default: - // This is unexpected, something went wrong during the test. - Assert.Fail("The provider has transitioned from an unexpected state: " + Enum.GetName(typeof(ProviderState), e.OldState)); - break; - } - }; - - // Initiate logout. - await provider.LoginAsync(); - - // Logout has completed, the provider should be signed out. - Assert.AreEqual(ProviderState.SignedIn, provider.State); - - // Initiate logout, which should skip loading, and go straight to signed out. - await provider.LogoutAsync(); - - // Ensure the proper number of events were fired. - Assert.AreEqual(eventCount, 3); + // Listen for changes in the provider state and count them. + int eventCount = 0; + provider.StateChanged += (s, e) => + { + eventCount += 1; + + // Ensure that the states are properly reported through the StateChanged event. + switch (e.OldState) + { + case ProviderState.SignedOut: + // Login has been initiated, the provider should now be loading. + Assert.AreEqual(ProviderState.Loading, e.NewState); + + // Loading should be the first event fired. + Assert.AreEqual(eventCount, 1); + break; + + case ProviderState.Loading: + // The provider has completed login, the provider should now be signed in. + Assert.AreEqual(ProviderState.SignedIn, e.NewState); + + // SignedIn should be the second event fired. + Assert.AreEqual(eventCount, 2); + break; + + case ProviderState.SignedIn: + // The provider has completed login, the provider should now be signed in. + Assert.AreEqual(ProviderState.SignedOut, e.NewState); + + // SignedIn should be the second event fired. + Assert.AreEqual(eventCount, 3); + break; + + default: + // This is unexpected, something went wrong during the test. + Assert.Fail("The provider has transitioned from an unexpected state: " + Enum.GetName(typeof(ProviderState), e.OldState)); + break; + } + }; + + // Initiate logout. + await provider.LoginAsync(); + + // Logout has completed, the provider should be signed out. + Assert.AreEqual(ProviderState.SignedIn, provider.State); + + // Initiate logout, which should skip loading, and go straight to signed out. + await provider.LogoutAsync(); + + // Ensure the proper number of events were fired. + Assert.AreEqual(eventCount, 3); + }); } } } From 6e95272f8b1aff370ccdf21dac433739a14a3009 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 8 Apr 2021 13:28:56 -0700 Subject: [PATCH 19/24] minor tweak to enable empty client id. Good for ultra basic auth, with no Graph access. --- CommunityToolkit.Uwp.Authentication/WindowsProvider.cs | 6 ++++-- UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 10f8cd3..73458fb 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -66,10 +66,10 @@ public class WindowsProvider : BaseProvider /// /// Initializes a new instance of the class. /// - /// Registered ClientId. + /// Registered ClientId. You can access an acount without one, but any Graph requests will fail. /// List of Scopes to initially request. /// Configuration values for the AccountsSettingsPane. - public WindowsProvider(string clientId, string[] scopes = null, AccountsSettingsPaneConfig? accountsSettingsPaneConfig = null) + public WindowsProvider(string clientId = "", string[] scopes = null, AccountsSettingsPaneConfig? accountsSettingsPaneConfig = null) { _clientId = clientId; _scopes = scopes ?? DefaultScopes; @@ -155,6 +155,7 @@ public async Task GetTokenAsync(bool silentOnly = false) try { + var initialState = State; if (State == ProviderState.SignedOut) { State = ProviderState.Loading; @@ -169,6 +170,7 @@ public async Task GetTokenAsync(bool silentOnly = false) if (silentOnly) { // Silent login may fail if we don't have a cached account, and that's ok. + State = initialState; return null; } diff --git a/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs b/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs index b1ead43..a411952 100644 --- a/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs +++ b/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs @@ -14,6 +14,7 @@ namespace UnitTests.UWP.Authentication [TestClass] public class Test_WindowsProvider : VisualUITestBase { + // Note: Authenticaiton will work without a client id, however any Graph requests will fail. private const string ClientId = ""; /// From 61f9664c78da550db7b93aa6921b9a219b91d800 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Mon, 12 Apr 2021 14:09:46 -0700 Subject: [PATCH 20/24] PR updates --- .../MsalProvider.cs | 8 +- .../WindowsProvider.cs | 76 +++++++++++++++---- SampleTest/App.xaml.cs | 33 +++----- 3 files changed, 79 insertions(+), 38 deletions(-) diff --git a/CommunityToolkit.Net.Authentication.Msal/MsalProvider.cs b/CommunityToolkit.Net.Authentication.Msal/MsalProvider.cs index 5cd2de1..fff7aa6 100644 --- a/CommunityToolkit.Net.Authentication.Msal/MsalProvider.cs +++ b/CommunityToolkit.Net.Authentication.Msal/MsalProvider.cs @@ -34,7 +34,8 @@ public class MsalProvider : BaseProvider /// Registered ClientId. /// RedirectUri for auth response. /// List of Scopes to initially request. - public MsalProvider(string clientId, string[] scopes = null, string redirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient") + /// Determines whether the provider attempts to silently log in upon instantionation. + public MsalProvider(string clientId, string[] scopes = null, string redirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient", bool autoSignIn = true) { var client = PublicClientApplicationBuilder.Create(clientId) .WithAuthority(AzureCloudInstance.AzurePublic, AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount) @@ -47,7 +48,10 @@ public MsalProvider(string clientId, string[] scopes = null, string redirectUri Client = client; - _ = TrySilentSignInAsync(); + if (autoSignIn) + { + Task.Run(TrySilentSignInAsync); + } } /// diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 73458fb..5af1182 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -18,7 +18,19 @@ namespace CommunityToolkit.Uwp.Authentication { /// - /// An authentication provider based on the native AccountSettingsPane in Windows. + /// An enumeration of the available authentication providers for use in the AccountsSettingsPane. + /// + [Flags] + public enum WebAccountProviderType + { + /// + /// Authenticate public/consumer MSA accounts. + /// + MSA, + } + + /// + /// An authentication provider based on the native AccountsSettingsPane in Windows. /// public class WindowsProvider : BaseProvider { @@ -38,10 +50,8 @@ public class WindowsProvider : BaseProvider // Default/minimal scopes for authentication, if none are provided. private static readonly string[] DefaultScopes = { "User.Read" }; - /// - /// Gets the client id obtained from Azure registration. - /// - public string ClientId => _clientId; + // The default account providers available in the AccountsSettingsPane. + private static readonly WebAccountProviderType DefaultWebAccountsProviderType = WebAccountProviderType.MSA; /// /// Gets the list of scopes to pre-authorize during authentication. @@ -53,31 +63,45 @@ public class WindowsProvider : BaseProvider /// public AccountsSettingsPaneConfig? AccountsSettingsPaneConfig => _accountsSettingsPaneConfig; + /// + /// Gets the configuration values for determining the available web account providers. + /// + public WebAccountProviderConfig WebAccountProviderConfig => _webAccountProviderConfig; + /// /// Gets a cache of important values for the signed in user. /// protected IDictionary Settings => ApplicationData.Current.LocalSettings.Values; - private string _clientId; private string[] _scopes; private WebAccount _webAccount; private AccountsSettingsPaneConfig? _accountsSettingsPaneConfig; + private WebAccountProviderConfig _webAccountProviderConfig; /// /// Initializes a new instance of the class. /// - /// Registered ClientId. You can access an acount without one, but any Graph requests will fail. /// List of Scopes to initially request. /// Configuration values for the AccountsSettingsPane. - public WindowsProvider(string clientId = "", string[] scopes = null, AccountsSettingsPaneConfig? accountsSettingsPaneConfig = null) + /// Configuration value for determining the available web account providers. + /// Determines whether the provider attempts to silently log in upon instantionation. + public WindowsProvider(string[] scopes = null, AccountsSettingsPaneConfig? accountsSettingsPaneConfig = null, WebAccountProviderConfig? webAccountProviderConfig = null, bool autoSignIn = true) { - _clientId = clientId; _scopes = scopes ?? DefaultScopes; _accountsSettingsPaneConfig = accountsSettingsPaneConfig; + _webAccountProviderConfig = webAccountProviderConfig ?? new WebAccountProviderConfig() + { + WebAccountProviderType = DefaultWebAccountsProviderType, + }; _webAccount = null; State = ProviderState.SignedOut; + + if (autoSignIn) + { + _ = TrySilentSignInAsync(); + } } /// @@ -92,7 +116,7 @@ public override async Task LoginAsync() { if (_webAccount != null || State != ProviderState.SignedOut) { - await LogoutAsync(); + return; } // The state will get updated as part of the auth flow. @@ -105,11 +129,16 @@ public override async Task LoginAsync() } /// - /// Try logging in silently. + /// Tries to check if the user is logged in without prompting to login. /// /// A representing the asynchronous operation. - public async Task TrySilentLoginAsync() + public async Task TrySilentSignInAsync() { + if (_webAccount != null && State == ProviderState.SignedIn) + { + return true; + } + // The state will get updated as part of the auth flow. var token = await GetTokenAsync(true); return token != null; @@ -407,7 +436,7 @@ async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSetti private WebTokenRequest GetWebTokenRequest(WebAccountProvider provider) { - WebTokenRequest webTokenRequest = new WebTokenRequest(provider, string.Join(',', _scopes), _clientId); + WebTokenRequest webTokenRequest = new WebTokenRequest(provider, string.Join(',', _scopes)); webTokenRequest.Properties.Add(GraphResourcePropertyKey, GraphResourcePropertyValue); return webTokenRequest; @@ -418,7 +447,10 @@ private async Task> GetWebAccountProvidersAsync() var providers = new List(); // MSA - providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, MicrosoftAccountAuthority)); + if ((_webAccountProviderConfig.WebAccountProviderType & WebAccountProviderType.MSA) == WebAccountProviderType.MSA) + { + providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, MicrosoftAccountAuthority)); + } return providers; } @@ -450,4 +482,20 @@ public AccountsSettingsPaneConfig(string headerText, IList comm Commands = commands; } } + + /// + /// Configuration values for what type of authentication providers to enable. + /// + public struct WebAccountProviderConfig + { + /// + /// Gets or sets the registered ClientId. Required for AAD login. + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the types of accounts providers that should be available to the user. + /// + public WebAccountProviderType WebAccountProviderType { get; set; } + } } diff --git a/SampleTest/App.xaml.cs b/SampleTest/App.xaml.cs index 4a3fb39..7648530 100644 --- a/SampleTest/App.xaml.cs +++ b/SampleTest/App.xaml.cs @@ -5,11 +5,8 @@ using CommunityToolkit.Net.Authentication; using CommunityToolkit.Uwp.Authentication; using System; -using System.Collections.Generic; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; -using Windows.UI.ApplicationSettings; -using Windows.UI.Popups; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Navigation; @@ -32,7 +29,7 @@ public App() } // Which provider should be used for authentication? - private readonly ProviderType _providerType = ProviderType.Mock; + private readonly ProviderType _providerType = ProviderType.Windows; // List of available authentication providers. private enum ProviderType @@ -52,52 +49,44 @@ private async void InitializeGlobalProvider() return; } - await Window.Current.Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () => + await Window.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { // Provider config string clientId = "YOUR_CLIENT_ID_HERE"; string[] scopes = { "User.Read", "User.ReadBasic.All", "People.Read", "Calendars.Read", "Mail.Read", "Group.Read.All", "ChannelMessage.Read.All" }; + bool autoSignIn = true; switch (_providerType) { // Mock provider case ProviderType.Mock: - ProviderManager.Instance.GlobalProvider = new MockProvider(signedIn: true); + ProviderManager.Instance.GlobalProvider = new MockProvider(signedIn: autoSignIn); break; // Msal provider case ProviderType.Msal: - var msalProvider = new MsalProvider(clientId, scopes); - ProviderManager.Instance.GlobalProvider = msalProvider; - await msalProvider.TrySilentSignInAsync(); + ProviderManager.Instance.GlobalProvider = new MsalProvider(clientId: clientId, scopes: scopes, autoSignIn: autoSignIn); break; // Windows provider case ProviderType.Windows: - var settingsCommandId = Guid.NewGuid(); - WindowsProvider windowsProvider = new WindowsProvider(clientId, scopes, new AccountsSettingsPaneConfig() - { - HeaderText = "Custom header text goes here.", - Commands = new List() { new SettingsCommand(settingsCommandId, "Click me!", OnSettingsCommandInvoked) } - }); - ProviderManager.Instance.GlobalProvider = windowsProvider; - await windowsProvider.TrySilentLoginAsync(); + ProviderManager.Instance.GlobalProvider = new WindowsProvider(scopes: scopes, autoSignIn: autoSignIn); break; } }); } - private void OnSettingsCommandInvoked(IUICommand command) - { - System.Diagnostics.Debug.WriteLine("AccountsSettingsPane command invoked: " + command.Label); - } + //private void OnSettingsCommandInvoked(IUICommand command) + //{ + // System.Diagnostics.Debug.WriteLine("AccountsSettingsPane command invoked: " + command.Label); + //} /// /// Invoked when the application is launched normally by the end user. Other entry points /// will be used such as when the application is launched to open a specific file. /// /// Details about the launch request and process. - protected override void OnLaunched(LaunchActivatedEventArgs e) + protected override async void OnLaunched(LaunchActivatedEventArgs e) { Frame rootFrame = Window.Current.Content as Frame; From 0293e320431e43ed0c149f42234e022072179577 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 13 Apr 2021 09:07:44 -0700 Subject: [PATCH 21/24] Added constructor to WebAccountProviderConfig --- .../WindowsProvider.cs | 13 ++++++++++++- SampleTest/App.xaml.cs | 11 +++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 5af1182..87b6b30 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -476,7 +476,7 @@ public struct AccountsSettingsPaneConfig /// /// The header text for the accounts settings pane. /// The SettingsCommand collection for the account settings pane. - public AccountsSettingsPaneConfig(string headerText, IList commands) + public AccountsSettingsPaneConfig(string headerText = null, IList commands = null) { HeaderText = headerText; Commands = commands; @@ -497,5 +497,16 @@ public struct WebAccountProviderConfig /// Gets or sets the types of accounts providers that should be available to the user. /// public WebAccountProviderType WebAccountProviderType { get; set; } + + /// + /// Initializes a new instance of the struct. + /// + /// The types of accounts providers that should be available to the user. + /// The registered ClientId. Required for AAD login. + public WebAccountProviderConfig(WebAccountProviderType webAccountProviderType, string clientId = null) + { + WebAccountProviderType = webAccountProviderType; + ClientId = clientId; + } } } diff --git a/SampleTest/App.xaml.cs b/SampleTest/App.xaml.cs index 7648530..ed3812b 100644 --- a/SampleTest/App.xaml.cs +++ b/SampleTest/App.xaml.cs @@ -29,7 +29,7 @@ public App() } // Which provider should be used for authentication? - private readonly ProviderType _providerType = ProviderType.Windows; + private readonly ProviderType _providerType = ProviderType.Mock; // List of available authentication providers. private enum ProviderType @@ -70,23 +70,18 @@ await Window.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority. // Windows provider case ProviderType.Windows: - ProviderManager.Instance.GlobalProvider = new WindowsProvider(scopes: scopes, autoSignIn: autoSignIn); + ProviderManager.Instance.GlobalProvider = new WindowsProvider(scopes, autoSignIn: autoSignIn); break; } }); } - //private void OnSettingsCommandInvoked(IUICommand command) - //{ - // System.Diagnostics.Debug.WriteLine("AccountsSettingsPane command invoked: " + command.Label); - //} - /// /// Invoked when the application is launched normally by the end user. Other entry points /// will be used such as when the application is launched to open a specific file. /// /// Details about the launch request and process. - protected override async void OnLaunched(LaunchActivatedEventArgs e) + protected override void OnLaunched(LaunchActivatedEventArgs e) { Frame rootFrame = Window.Current.Content as Frame; From 12fa2bd4c1660afc51b73e27ee8eeccf115f2dbc Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 13 Apr 2021 09:13:25 -0700 Subject: [PATCH 22/24] Updated docs removed unused redirect uri --- .../WindowsProvider.cs | 5 -- Docs/WindowsProvider.md | 58 ++++++++++--------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index 87b6b30..a0d63b0 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -34,11 +34,6 @@ public enum WebAccountProviderType /// public class WindowsProvider : BaseProvider { - /// - /// 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 const string AuthenticationHeaderScheme = "Bearer"; private const string GraphResourcePropertyKey = "resource"; private const string GraphResourcePropertyValue = "https://graph.microsoft.com"; diff --git a/Docs/WindowsProvider.md b/Docs/WindowsProvider.md index 3c04f5a..fd7c00b 100644 --- a/Docs/WindowsProvider.md +++ b/Docs/WindowsProvider.md @@ -3,40 +3,44 @@ The WindowsProvider is an authentication provider for accessing locally configured accounts on Windows. It extends IProvider and uses the native AccountsSettingsPane APIs for login. -> [!IMPORTANT] -> Be sure to Register Client Id in Azure first following the guidance here: -> -> After finishing the initial registration page, you will also need to add an additional redirect URI. Click on "Add a Redirect URI" and check the "https://login.microsoftonline.com/common/oauth2/nativeclient" checkbox on that page. -> -> You'll also want to set the toggle to true for "Allow public client flows". -> -> Then click "Save". - ## Syntax ```CSharp -// Simple -string clientId = "YOUR_CLIENT_ID_HERE"; -string[] scopes = new string[] { "User.Read" }; - -ProviderManager.Instance.GlobalProvider = new WindowsProvider(clientId, scopes); - -// Customized AccountsSettingsPane -IList commands = new List() -{ - new SettingsCommand(Guid.NewGuid(), "Click me!", OnSettingsCommandInvoked) -} -AccountsSettingsPaneConfig accountsSettingsPaneConfig = new AccountsSettingsPaneConfig() +// Provider config +string clientId = "YOUR_CLIENT_ID_HERE"; // Not used with MSA login, but shown here for completeness. +string[] scopes = { "User.Read", "People.Read", "Calendars.Read", "Mail.Read" }; +bool autoSignIn = true; + +// Easily create a new WindowsProvider instance and set the GlobalProvider. +// The provider will attempt to sign in automatically. +ProviderManager.Instance.GlobalProvider = new WindowsProvider(scopes); + +// Additional parameters are also available, +// such as custom settings commands for the AccountsSettingsPane. +Guid settingsCommandId = Guid.NewGuid(); +void OnSettingsCommandInvoked(IUICommand command) { - HeaderText = "Custom header text goes here.", - Commands = commands + System.Diagnostics.Debug.WriteLine("AccountsSettingsPane command invoked: " + command.Id); } -WindowsProvider windowsProvider = new WindowsProvider(clientId, scopes, accountsSettingsPaneConfig); -ProviderManager.Instance.GlobalProvider = windowsProvider; +// Configure which types accounts should be available to choose from. The default is MSA, but AAD will come in the future. +// ClientId is not used with MSA login, but shown here for completeness. +var webAccountProviderConfig = new WebAccountProviderConfig(WebAccountProviderType.MSA, clientId); + +// Configure details to present in the AccountsSettingsPane, such as custom header text and links. +var accountsSettingsPaneConfig = new AccountsSettingsPaneConfig( + headerText: "Custom header text", + commands: new List() + { + new SettingsCommand(settingsCommandId: settingsCommandId, label: "Click me!", handler: OnSettingsCommandInvoked) + }); + +// Determine it the provider should automatically sign in or not. Default is true. +// Set to false to delay silent sign in until TrySilentSignInAsync is called. +bool autoSignIn = false; -// Silent login -await windowsProvider.TrySilentLoginAsync(); +// Set the GlobalProvider with the extra configuration +ProviderManager.Instance.GlobalProvider = new WindowsProvider(scopes, accountsSettingsPaneConfig, webAccountProviderConfig, autoSignIn); ``` ## Prerequisite Windows Store Association in Visual Studio From 6d849204c06ba0847cb6d427204c423b21d9a031 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Wed, 14 Apr 2021 15:14:58 -0700 Subject: [PATCH 23/24] Updated docs with latest guidance --- .../WindowsProvider.cs | 14 ++++-- Docs/WindowsProvider.md | 48 ++++++++++++++++--- SampleTest/App.xaml.cs | 3 +- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs index a0d63b0..c8e3fbb 100644 --- a/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -34,6 +34,12 @@ public enum WebAccountProviderType /// public class WindowsProvider : BaseProvider { + /// + /// Gets the redirect uri value based on the current app callback uri. + /// Used for configuring in Azure app registration. + /// + public static string RedirectUri => string.Format("ms-appx-web://Microsoft.AAD.BrokerPlugIn/{0}", WebAuthenticationBroker.GetCurrentApplicationCallbackUri().Host.ToUpper()); + private const string AuthenticationHeaderScheme = "Bearer"; private const string GraphResourcePropertyKey = "resource"; private const string GraphResourcePropertyValue = "https://graph.microsoft.com"; @@ -80,14 +86,14 @@ public class WindowsProvider : BaseProvider /// Configuration values for the AccountsSettingsPane. /// Configuration value for determining the available web account providers. /// Determines whether the provider attempts to silently log in upon instantionation. - public WindowsProvider(string[] scopes = null, AccountsSettingsPaneConfig? accountsSettingsPaneConfig = null, WebAccountProviderConfig? webAccountProviderConfig = null, bool autoSignIn = true) + public WindowsProvider(string[] scopes = null, WebAccountProviderConfig? webAccountProviderConfig = null, AccountsSettingsPaneConfig? accountsSettingsPaneConfig = null, bool autoSignIn = true) { _scopes = scopes ?? DefaultScopes; - _accountsSettingsPaneConfig = accountsSettingsPaneConfig; _webAccountProviderConfig = webAccountProviderConfig ?? new WebAccountProviderConfig() { WebAccountProviderType = DefaultWebAccountsProviderType, }; + _accountsSettingsPaneConfig = accountsSettingsPaneConfig; _webAccount = null; @@ -484,7 +490,7 @@ public AccountsSettingsPaneConfig(string headerText = null, IList - /// Gets or sets the registered ClientId. Required for AAD login. + /// Gets or sets the registered ClientId. Required for AAD login and admin consent. /// public string ClientId { get; set; } @@ -497,7 +503,7 @@ public struct WebAccountProviderConfig /// Initializes a new instance of the struct. /// /// The types of accounts providers that should be available to the user. - /// The registered ClientId. Required for AAD login. + /// The registered ClientId. Required for AAD login and admin consent. public WebAccountProviderConfig(WebAccountProviderType webAccountProviderType, string clientId = null) { WebAccountProviderType = webAccountProviderType; diff --git a/Docs/WindowsProvider.md b/Docs/WindowsProvider.md index fd7c00b..8e82847 100644 --- a/Docs/WindowsProvider.md +++ b/Docs/WindowsProvider.md @@ -7,7 +7,7 @@ It extends IProvider and uses the native AccountsSettingsPane APIs for login. ```CSharp // Provider config -string clientId = "YOUR_CLIENT_ID_HERE"; // Not used with MSA login, but shown here for completeness. +string clientId = "YOUR_CLIENT_ID_HERE"; // Only required for approving application or delegated permissions. string[] scopes = { "User.Read", "People.Read", "Calendars.Read", "Mail.Read" }; bool autoSignIn = true; @@ -24,7 +24,7 @@ void OnSettingsCommandInvoked(IUICommand command) } // Configure which types accounts should be available to choose from. The default is MSA, but AAD will come in the future. -// ClientId is not used with MSA login, but shown here for completeness. +// ClientId is only required for approving admin level consent. var webAccountProviderConfig = new WebAccountProviderConfig(WebAccountProviderType.MSA, clientId); // Configure details to present in the AccountsSettingsPane, such as custom header text and links. @@ -44,7 +44,7 @@ ProviderManager.Instance.GlobalProvider = new WindowsProvider(scopes, accountsSe ``` ## Prerequisite Windows Store Association in Visual Studio -To get valid tokens and complete login, the app will need to be associated with the Microsoft Store. +To get valid tokens and complete login, the app will need to be associated with the Microsoft Store. This will enable your app to authenticate consumer MSA accounts without any additional configuration. 1. In Visual Studio Solution Explorer, right-click the UWP project, then select **Store -> Associate App with the Store...** @@ -56,24 +56,58 @@ To get valid tokens and complete login, the app will need to be associated with > You must have a Windows Developer account to use the WindowsProvider in your UWP app. You can [register a Microsoft developer account](https://developer.microsoft.com/store/register) if you don't already have one. +## Prerequisite Configure Client Id in Partner Center + +If your product integrates with Azure AD and calls APIs that request either application permissions or delegated permissions that require administrator consent, you will also need to enter your Azure AD Client ID in Partner Center: + +https://partner.microsoft.com/en-us/dashboard/products/<YOUR-APP-ID>/administrator-consent + +This lets administrators who acquire the app for their organization grant consent for your product to act on behalf of all users in the tenant. + +> [!NOTE] +> You only need to specify the client id if you need admin consent for delegated permissions from your AAD app registration. Simple authentication for public accounts does not require a client id or any additional configuration. + +> [!IMPORTANT] +> Be sure to Register Client Id in Azure first following the guidance here: +> +> After finishing the initial registration page, you will also need to add an additional redirect URI. Click on "Add a Redirect URI" and add the value retrieved from running `WindowsProvider.RedirectUri`. +> +> You'll also want to set the toggle to true for "Allow public client flows". +> +> Then click "Save". + ## Properties See IProvider for a full list of supported properties. | Property | Type | Description | | -- | -- | -- | -| ClientId | string | Client Id obtained from Azure registration. | -| Scopes | ScopeSet | Comma-delimited list of scopes to pre-authorize from user during authentication. | +| Scopes | string[] | List of scopes to pre-authorize on the user during authentication. | +| WebAccountsProviderConfig | WebAccountsProviderConfig | configuration values for determining the available web account providers. | | AccountsSettingsPaneConfig | AccountsSettingsPaneConfig | Configuration values for the AccountsSettingsPane, shown during authentication. | | RedirectUri | string | Static getter for retrieving a customized redirect uri to put in the Azure app registration. | -### AccountsSettingsPane +### WebAccountProviderConfig + +| Property | Type | Description | +| -- | -- | -- | +| ClientId | string | Client Id obtained from Azure registration. | +| WebAccountsProviderType | WebAccountsProviderType | The types of accounts providers that should be available to the user. | + +### AccountsSettingsPaneConfig | Property | Type | Description | | -- | -- | -- | | HeaderText | string | Gets or sets the header text for the accounts settings pane. | | Commands | IList | Gets or sets the SettingsCommand collection for the account settings pane. | +## Enums + +### WebAccountProviderType + +| Value | Description | +| -- | -- | +| MSA | Enable authentication of public/consumer MSA accounts. | ## Methods @@ -82,4 +116,4 @@ See IProvider for a full list of supported methods. | Method | Arguments | Returns | Description | | -- | -- | -- | -- | | GetTokenAsync | bool silentOnly = true | Task | Retrieve a token for the authenticated user. | -| TrySilentLoginAsync | | Task | Try logging in silently, without prompts. | +| TrySilentSignInAsync | | Task | Try logging in silently, without prompts. | \ No newline at end of file diff --git a/SampleTest/App.xaml.cs b/SampleTest/App.xaml.cs index ed3812b..c06f456 100644 --- a/SampleTest/App.xaml.cs +++ b/SampleTest/App.xaml.cs @@ -70,7 +70,8 @@ await Window.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority. // Windows provider case ProviderType.Windows: - ProviderManager.Instance.GlobalProvider = new WindowsProvider(scopes, autoSignIn: autoSignIn); + var webAccountProviderConfig = new WebAccountProviderConfig(WebAccountProviderType.MSA, clientId); + ProviderManager.Instance.GlobalProvider = new WindowsProvider(scopes, webAccountProviderConfig: webAccountProviderConfig, autoSignIn: autoSignIn); break; } }); From af0507ad3f356cb53604c84f83b15e2e218301b8 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Thu, 15 Apr 2021 08:21:59 -0700 Subject: [PATCH 24/24] Removed uneccesary client id from WindowsProvider tests --- UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs b/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs index a411952..095420d 100644 --- a/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs +++ b/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs @@ -14,9 +14,6 @@ namespace UnitTests.UWP.Authentication [TestClass] public class Test_WindowsProvider : VisualUITestBase { - // Note: Authenticaiton will work without a client id, however any Graph requests will fail. - private const string ClientId = ""; - /// /// Create a new instance of the WindowsProvider and check that is has the proper default state. /// @@ -24,7 +21,7 @@ public class Test_WindowsProvider : VisualUITestBase [TestMethod] public void Test_WindowsProvider_Default() { - WindowsProvider provider = new WindowsProvider(ClientId); + WindowsProvider provider = new WindowsProvider(); Assert.AreEqual(ProviderState.SignedOut, provider.State); } @@ -41,7 +38,7 @@ public async Task Test_WindowsProvider_LoginAsync() await App.DispatcherQueue.EnqueueAsync(async () => { // Create the new provider. - WindowsProvider provider = new WindowsProvider(ClientId); + WindowsProvider provider = new WindowsProvider(); // Run logout to ensure that no cached users affect the test. await provider.LogoutAsync();