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/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..c8e3fbb --- /dev/null +++ b/CommunityToolkit.Uwp.Authentication/WindowsProvider.cs @@ -0,0 +1,513 @@ +// 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; +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 CommunityToolkit.Uwp.Authentication +{ + /// + /// 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 + { + /// + /// 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"; + private const string MicrosoftAccountAuthority = "consumers"; + private const string MicrosoftProviderId = "https://login.microsoft.com"; + private const string SettingsKeyAccountId = "WindowsProvider_AccountId"; + private const string SettingsKeyProviderId = "WindowsProvider_ProviderId"; + + // Default/minimal scopes for authentication, if none are provided. + private static readonly string[] DefaultScopes = { "User.Read" }; + + // 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. + /// + public string[] Scopes => _scopes; + + /// + /// Gets configuration values for the AccountsSettingsPane. + /// + 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[] _scopes; + private WebAccount _webAccount; + private AccountsSettingsPaneConfig? _accountsSettingsPaneConfig; + private WebAccountProviderConfig _webAccountProviderConfig; + + /// + /// Initializes a new instance of the class. + /// + /// List of Scopes to initially request. + /// 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, WebAccountProviderConfig? webAccountProviderConfig = null, AccountsSettingsPaneConfig? accountsSettingsPaneConfig = null, bool autoSignIn = true) + { + _scopes = scopes ?? DefaultScopes; + _webAccountProviderConfig = webAccountProviderConfig ?? new WebAccountProviderConfig() + { + WebAccountProviderType = DefaultWebAccountsProviderType, + }; + _accountsSettingsPaneConfig = accountsSettingsPaneConfig; + + _webAccount = null; + + State = ProviderState.SignedOut; + + if (autoSignIn) + { + _ = TrySilentSignInAsync(); + } + } + + /// + public override async Task AuthenticateRequestAsync(HttpRequestMessage request) + { + string token = await GetTokenAsync(); + request.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationHeaderScheme, token); + } + + /// + public override async Task LoginAsync() + { + if (_webAccount != null || State != ProviderState.SignedOut) + { + return; + } + + // The state will get updated as part of the auth flow. + var token = await GetTokenAsync(); + + if (token == null) + { + await LogoutAsync(); + } + } + + /// + /// Tries to check if the user is logged in without prompting to login. + /// + /// A representing the asynchronous operation. + 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; + } + + /// + public override async Task LogoutAsync() + { + Settings.Remove(SettingsKeyAccountId); + Settings.Remove(SettingsKeyProviderId); + + if (_webAccount != null) + { + try + { + await _webAccount.SignOutAsync(); + } + catch + { + // Failed to remove an account. + } + + _webAccount = null; + } + + State = ProviderState.SignedOut; + } + + /// + /// 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) + { + // We are not online, no token for you. + // TODO: Is there anything special to do when we go offline? + return null; + } + + try + { + var initialState = State; + if (State == ProviderState.SignedOut) + { + State = ProviderState.Loading; + } + + // Attempt to authenticate silently. + var authResult = await AuthenticateSilentAsync(); + + // 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. + State = initialState; + return null; + } + + // Attempt to authenticate interactively. + authResult = await AuthenticateInteractiveAsync(); + } + + 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?.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; + } + + private async Task SetAccountAsync(WebAccount account) + { + if (account == null) + { + // Clear account + await LogoutAsync(); + return; + } + else if (account.Id == _webAccount?.Id) + { + // No change + return; + } + + // Save off the account ids. + _webAccount = account; + Settings[SettingsKeyAccountId] = account.Id; + Settings[SettingsKeyProviderId] = account.WebAccountProvider.Id; + + State = ProviderState.SignedIn; + } + + private async Task AuthenticateSilentAsync() + { + try + { + WebTokenRequestResult authResult = null; + + var account = _webAccount; + if (account == null) + { + // Check the cache for an existing user + if (Settings[SettingsKeyAccountId] is string savedAccountId && + Settings[SettingsKeyProviderId] is string savedProviderId) + { + var savedProvider = await WebAuthenticationCoreManager.FindAccountProviderAsync(savedProviderId); + account = await WebAuthenticationCoreManager.FindAccountAsync(savedProvider, savedAccountId); + } + } + + if (account != null) + { + // Prepare a request to get a token. + var webTokenRequest = GetWebTokenRequest(account.WebAccountProvider); + authResult = await WebAuthenticationCoreManager.GetTokenSilentlyAsync(webTokenRequest, account); + } + + return authResult; + } + catch (HttpRequestException) + { + throw; /* probably offline, no point continuing to interactive auth */ + } + } + + private async Task AuthenticateInteractiveAsync() + { + try + { + WebTokenRequestResult authResult = null; + + var account = _webAccount; + if (account != null) + { + // We already have the account. + var webAccountProvider = account.WebAccountProvider; + var webTokenRequest = GetWebTokenRequest(webAccountProvider); + authResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest, account); + } + else + { + // 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; + } + catch (HttpRequestException) + { + throw; /* probably offline, no point continuing to interactive auth */ + } + } + + /// + /// Show the AccountSettingsPane and wait for the user to make a selection, then process the authentication result. + /// + 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. + // The entire AccountSettingsPane flow is contained here. + var webAccountProviderTaskCompletionSource = new TaskCompletionSource(); + + bool webAccountProviderCommandWasInvoked = false; + + // Handle the selected account provider + void WebAccountProviderCommandInvoked(WebAccountProviderCommand command) + { + webAccountProviderCommandWasInvoked = true; + try + { + webAccountProviderTaskCompletionSource.SetResult(command.WebAccountProvider); + } + catch (Exception ex) + { + webAccountProviderTaskCompletionSource.SetException(ex); + } + } + + // Build the AccountSettingsPane and configure it with available providers. + async void OnAccountCommandsRequested(AccountsSettingsPane sender, AccountsSettingsPaneCommandsRequestedEventArgs e) + { + var deferral = e.GetDeferral(); + + try + { + // Configure available providers. + List webAccountProviders = await GetWebAccountProvidersAsync(); + + foreach (WebAccountProvider webAccountProvider in webAccountProviders) + { + var providerCommand = new WebAccountProviderCommand(webAccountProvider, WebAccountProviderCommandInvoked); + e.WebAccountProviderCommands.Add(providerCommand); + } + + // Apply the configured header. + var headerText = _accountsSettingsPaneConfig?.HeaderText; + if (!string.IsNullOrWhiteSpace(headerText)) + { + e.HeaderText = headerText; + } + + // Apply any configured commands. + var commands = _accountsSettingsPaneConfig?.Commands; + if (commands != null) + { + 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); + webAccountProviderTaskCompletionSource.SetCanceled(); + })); + } + } + } + catch (Exception ex) + { + webAccountProviderTaskCompletionSource.SetException(ex); + } + finally + { + deferral.Complete(); + } + } + + 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(); + + // If an account was selected, the WebAccountProviderCommand will be invoked. + // If not, the AccountsSettingsPane must have been cancelled or closed. + var webAccountProvider = webAccountProviderCommandWasInvoked ? await webAccountProviderTaskCompletionSource.Task : null; + return webAccountProvider; + } + catch (TaskCanceledException) + { + // The task was cancelled. No provider was chosen. + return null; + } + finally + { + if (pane != null) + { + pane.AccountCommandsRequested -= OnAccountCommandsRequested; + } + } + } + + private WebTokenRequest GetWebTokenRequest(WebAccountProvider provider) + { + WebTokenRequest webTokenRequest = new WebTokenRequest(provider, string.Join(',', _scopes)); + webTokenRequest.Properties.Add(GraphResourcePropertyKey, GraphResourcePropertyValue); + + return webTokenRequest; + } + + private async Task> GetWebAccountProvidersAsync() + { + var providers = new List(); + + // MSA + if ((_webAccountProviderConfig.WebAccountProviderType & WebAccountProviderType.MSA) == WebAccountProviderType.MSA) + { + providers.Add(await WebAuthenticationCoreManager.FindAccountProviderAsync(MicrosoftProviderId, MicrosoftAccountAuthority)); + } + + return providers; + } + } + + /// + /// 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; } + + /// + /// Initializes a new instance of the struct. + /// + /// The header text for the accounts settings pane. + /// The SettingsCommand collection for the account settings pane. + public AccountsSettingsPaneConfig(string headerText = null, IList commands = null) + { + HeaderText = headerText; + 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 and admin consent. + /// + 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; } + + /// + /// 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 and admin consent. + public WebAccountProviderConfig(WebAccountProviderType webAccountProviderType, string clientId = null) + { + WebAccountProviderType = webAccountProviderType; + ClientId = clientId; + } + } +} diff --git a/Docs/WindowsProvider.md b/Docs/WindowsProvider.md new file mode 100644 index 0000000..8e82847 --- /dev/null +++ b/Docs/WindowsProvider.md @@ -0,0 +1,119 @@ +# 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. + +## Syntax + +```CSharp +// Provider config +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; + +// 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) +{ + System.Diagnostics.Debug.WriteLine("AccountsSettingsPane command invoked: " + command.Id); +} + +// Configure which types accounts should be available to choose from. The default is MSA, but AAD will come in the future. +// 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. +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; + +// Set the GlobalProvider with the extra configuration +ProviderManager.Instance.GlobalProvider = new WindowsProvider(scopes, accountsSettingsPaneConfig, webAccountProviderConfig, autoSignIn); +``` + +## 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. 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...** + +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. + + +## 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 | +| -- | -- | -- | +| 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. | + +### 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 + +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. | +| 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 a31d579..c06f456 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 CommunityToolkit.Uwp.Authentication; using System; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; @@ -27,7 +28,6 @@ public App() this.Suspending += OnSuspending; } - // Which provider should be used for authentication? private readonly ProviderType _providerType = ProviderType.Mock; @@ -35,35 +35,46 @@ public App() private enum ProviderType { Mock, - Msal + Msal, + Windows } /// /// 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.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { - // 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; - } + // 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: autoSignIn); + break; + + // Msal provider + case ProviderType.Msal: + ProviderManager.Instance.GlobalProvider = new MsalProvider(clientId: clientId, scopes: scopes, autoSignIn: autoSignIn); + break; + + // Windows provider + case ProviderType.Windows: + var webAccountProviderConfig = new WebAccountProviderConfig(WebAccountProviderType.MSA, clientId); + ProviderManager.Instance.GlobalProvider = new WindowsProvider(scopes, webAccountProviderConfig: webAccountProviderConfig, autoSignIn: autoSignIn); + break; + } + }); } /// diff --git a/SampleTest/SampleTest.csproj b/SampleTest/SampleTest.csproj index 353be98..4607273 100644 --- a/SampleTest/SampleTest.csproj +++ b/SampleTest/SampleTest.csproj @@ -176,6 +176,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 diff --git a/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs b/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs new file mode 100644 index 0000000..095420d --- /dev/null +++ b/UnitTests/UnitTests.UWP/Providers/Test_WindowsProvider.cs @@ -0,0 +1,103 @@ +// 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.Toolkit.Uwp; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Threading.Tasks; + +namespace UnitTests.UWP.Authentication +{ + [TestClass] + public class Test_WindowsProvider : VisualUITestBase + { + /// + /// 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(); + + 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() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + // Create the new provider. + WindowsProvider provider = new WindowsProvider(); + + // 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 diff --git a/Windows-Toolkit-Graph-Controls.sln b/Windows-Toolkit-Graph-Controls.sln index c25ab7e..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 @@ -30,10 +29,17 @@ 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 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTests", "UnitTests", "{FECA0506-1FFF-4BF2-B266-B4ADC7E00879}" 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 @@ -256,6 +262,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 {6B33B26C-008B-4ADB-B317-EF996CD6755B}.CI|Any CPU.ActiveCfg = Debug|x86 {6B33B26C-008B-4ADB-B317-EF996CD6755B}.CI|Any CPU.Build.0 = Debug|x86 {6B33B26C-008B-4ADB-B317-EF996CD6755B}.CI|Any CPU.Deploy.0 = Debug|x86