diff --git a/CommunityToolkit.Authentication.Msal/CommunityToolkit.Authentication.Msal.csproj b/CommunityToolkit.Authentication.Msal/CommunityToolkit.Authentication.Msal.csproj index 6ae6da7..501d753 100644 --- a/CommunityToolkit.Authentication.Msal/CommunityToolkit.Authentication.Msal.csproj +++ b/CommunityToolkit.Authentication.Msal/CommunityToolkit.Authentication.Msal.csproj @@ -1,8 +1,10 @@ - + - netstandard2.0 - + netstandard2.0;uap10.0;net5.0-windows10.0.17763.0;netcoreapp3.1 + 10.0.17763.0 + 7 + Windows Community Toolkit .NET Standard Auth Services This library provides an authentication provider based on the native Windows dialogues. It is part of the Windows Community Toolkit. @@ -18,6 +20,10 @@ + + + + diff --git a/CommunityToolkit.Authentication.Msal/MsalProvider.cs b/CommunityToolkit.Authentication.Msal/MsalProvider.cs index 419abdb..9f41e58 100644 --- a/CommunityToolkit.Authentication.Msal/MsalProvider.cs +++ b/CommunityToolkit.Authentication.Msal/MsalProvider.cs @@ -2,6 +2,7 @@ // 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.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -11,6 +12,16 @@ using System.Threading.Tasks; using Microsoft.Graph; using Microsoft.Identity.Client; +#if WINDOWS_UWP +using Windows.Security.Authentication.Web; +#else +using System.Diagnostics; +#endif + +#if NETCOREAPP3_1 +using Microsoft.Identity.Client.Desktop; +#endif + namespace CommunityToolkit.Authentication { /// @@ -18,52 +29,73 @@ namespace CommunityToolkit.Authentication /// public class MsalProvider : BaseProvider { + /// + /// A prefix value used to create the redirect URI value for use in AAD. + /// + public static readonly string MSAccountBrokerRedirectUriPrefix = "ms-appx-web://microsoft.aad.brokerplugin/"; + private static readonly SemaphoreSlim SemaphoreSlim = new (1); + /// + /// Gets or sets the currently authenticated user account. + /// + public IAccount Account { get; protected set; } + /// - public override string CurrentAccountId => _account?.HomeAccountId?.Identifier; + public override string CurrentAccountId => Account?.HomeAccountId?.Identifier; /// - /// Gets the MSAL.NET Client used to authenticate the user. + /// Gets or sets the MSAL.NET Client used to authenticate the user. /// - protected IPublicClientApplication Client { get; private set; } + public IPublicClientApplication Client { get; protected set; } /// /// Gets an array of scopes to use for accessing Graph resources. /// - protected string[] Scopes { get; private set; } - - private IAccount _account; + protected string[] Scopes { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class using a configuration object. /// - /// Registered ClientId. - /// RedirectUri for auth response. + /// Registered ClientId in Azure Acitve Directory. /// List of Scopes to initially request. - /// 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) + /// Determines whether the provider attempts to silently log in upon creation. + public MsalProvider(IPublicClientApplication client, string[] scopes = null, bool autoSignIn = true) { - var client = PublicClientApplicationBuilder.Create(clientId) - .WithAuthority(AzureCloudInstance.AzurePublic, AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount) - .WithRedirectUri(redirectUri) - .WithClientName(ProviderManager.ClientName) - .WithClientVersion(Assembly.GetExecutingAssembly().GetName().Version.ToString()) - .Build(); - - Scopes = scopes.Select(s => s.ToLower()).ToArray() ?? new string[] { string.Empty }; - Client = client; + Scopes = scopes.Select(s => s.ToLower()).ToArray() ?? new string[] { string.Empty }; if (autoSignIn) { - _ = TrySilentSignInAsync(); + TrySilentSignInAsync(); + } + } + + /// + /// Initializes a new instance of the class with default configuration values. + /// + /// Registered client id in Azure Acitve Directory. + /// RedirectUri for auth response. + /// List of Scopes to initially request. + /// Determines whether the provider attempts to silently log in upon creation. + /// Determines if organizational accounts should be enabled/disabled. + /// Registered tenant id in Azure Active Directory. + public MsalProvider(string clientId, string[] scopes = null, string redirectUri = null, bool autoSignIn = true, bool listWindowsWorkAndSchoolAccounts = true, string tenantId = null) + { + Client = CreatePublicClientApplication(clientId, tenantId, redirectUri, listWindowsWorkAndSchoolAccounts); + Scopes = scopes.Select(s => s.ToLower()).ToArray() ?? new string[] { string.Empty }; + + if (autoSignIn) + { + TrySilentSignInAsync(); } } /// public override async Task AuthenticateRequestAsync(HttpRequestMessage request) { + AddSdkVersion(request); + string token; // Check if any specific scopes are being requested. @@ -87,7 +119,7 @@ namespace CommunityToolkit.Authentication /// public override async Task TrySilentSignInAsync() { - if (_account != null && State == ProviderState.SignedIn) + if (Account != null && State == ProviderState.SignedIn) { return true; } @@ -108,7 +140,7 @@ namespace CommunityToolkit.Authentication /// public override async Task SignInAsync() { - if (_account != null || State != ProviderState.SignedOut) + if (Account != null || State != ProviderState.SignedOut) { return; } @@ -129,10 +161,10 @@ namespace CommunityToolkit.Authentication /// public override async Task SignOutAsync() { - if (_account != null) + if (Account != null) { - await Client.RemoveAsync(_account); - _account = null; + await Client.RemoveAsync(Account); + Account = null; } State = ProviderState.SignedOut; @@ -144,7 +176,48 @@ namespace CommunityToolkit.Authentication return this.GetTokenWithScopesAsync(Scopes, silentOnly); } - private async Task GetTokenWithScopesAsync(string[] scopes, bool silentOnly = false) + /// + /// Create an instance of using the provided config and some default values. + /// + /// Registered ClientId. + /// An optional tenant id. + /// Redirect uri for auth response. + /// Determines if organizational accounts should be supported. + /// A new instance of . + protected IPublicClientApplication CreatePublicClientApplication(string clientId, string tenantId, string redirectUri, bool listWindowsWorkAndSchoolAccounts) + { + var authority = listWindowsWorkAndSchoolAccounts ? AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount : AadAuthorityAudience.PersonalMicrosoftAccount; + + var clientBuilder = PublicClientApplicationBuilder.Create(clientId) + .WithAuthority(AzureCloudInstance.AzurePublic, authority) + .WithClientName(ProviderManager.ClientName) + .WithClientVersion(Assembly.GetExecutingAssembly().GetName().Version.ToString()); + + if (tenantId != null) + { + clientBuilder = clientBuilder.WithTenantId(tenantId); + } + +#if WINDOWS_UWP || NET5_0_WINDOWS10_0_17763_0 + clientBuilder = clientBuilder.WithBroker(); +#elif NETCOREAPP3_1 + clientBuilder = clientBuilder.WithWindowsBroker(); +#endif + + clientBuilder = (redirectUri != null) + ? clientBuilder.WithRedirectUri(redirectUri) + : clientBuilder.WithDefaultRedirectUri(); + + return clientBuilder.Build(); + } + + /// + /// Retrieve an authorization token using the provided scopes. + /// + /// An array of scopes to pass along with the Graph request. + /// A value to determine whether account broker UI should be shown, if required by MSAL. + /// A representing the result of the asynchronous operation. + protected async Task GetTokenWithScopesAsync(string[] scopes, bool silentOnly = false) { await SemaphoreSlim.WaitAsync(); @@ -153,7 +226,7 @@ namespace CommunityToolkit.Authentication AuthenticationResult authResult = null; try { - var account = _account ?? (await Client.GetAccountsAsync()).FirstOrDefault(); + var account = Account ?? (await Client.GetAccountsAsync()).FirstOrDefault(); if (account != null) { authResult = await Client.AcquireTokenSilent(scopes, account).ExecuteAsync(); @@ -172,14 +245,26 @@ namespace CommunityToolkit.Authentication { try { - if (_account != null) + var paramBuilder = Client.AcquireTokenInteractive(scopes); + + if (Account != null) { - authResult = await Client.AcquireTokenInteractive(scopes).WithPrompt(Prompt.NoPrompt).WithAccount(_account).ExecuteAsync(); - } - else - { - authResult = await Client.AcquireTokenInteractive(scopes).WithPrompt(Prompt.NoPrompt).ExecuteAsync(); + paramBuilder = paramBuilder.WithAccount(Account); } + +#if WINDOWS_UWP + // For UWP, specify NoPrompt for the least intrusive user experience. + paramBuilder = paramBuilder.WithPrompt(Prompt.NoPrompt); +#else + // Otherwise, get the process by FriendlyName from Application Domain + var friendlyName = AppDomain.CurrentDomain.FriendlyName; + var proc = Process.GetProcessesByName(friendlyName).First(); + + var windowHandle = proc.MainWindowHandle; + paramBuilder = paramBuilder.WithParentActivityOrWindow(windowHandle); +#endif + + authResult = await paramBuilder.ExecuteAsync(); } catch { @@ -188,7 +273,7 @@ namespace CommunityToolkit.Authentication } } - _account = authResult?.Account; + Account = authResult?.Account; return authResult?.AccessToken; } diff --git a/CommunityToolkit.Authentication.Msal/MsalProviderExtensions.cs b/CommunityToolkit.Authentication.Msal/MsalProviderExtensions.cs new file mode 100644 index 0000000..63d2930 --- /dev/null +++ b/CommunityToolkit.Authentication.Msal/MsalProviderExtensions.cs @@ -0,0 +1,35 @@ +// 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.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Identity.Client.Extensions.Msal; + +namespace CommunityToolkit.Authentication.Extensions +{ + /// + /// Helpers for working with the MsalProvider. + /// + public static class MsalProviderExtensions + { + /// + /// Helper function to initialize the token cache for non-UWP apps. MSAL handles this automatically on UWP. + /// + /// The instance of to init the cache for. + /// Properties for configuring the storage cache. + /// Passing null uses the default TraceSource logger. + /// A representing the result of the asynchronous operation. + public static async Task InitTokenCacheAsync( + this MsalProvider provider, + StorageCreationProperties storageProperties, + TraceSource logger = null) + { +#if !WINDOWS_UWP + // Token cache persistence (not required on UWP as MSAL does it for you) + var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties, logger); + cacheHelper.RegisterCache(provider.Client.UserTokenCache); +#endif + } + } +} diff --git a/CommunityToolkit.Authentication.Uwp/WindowsProvider.cs b/CommunityToolkit.Authentication.Uwp/WindowsProvider.cs index 914e259..57bc2ea 100644 --- a/CommunityToolkit.Authentication.Uwp/WindowsProvider.cs +++ b/CommunityToolkit.Authentication.Uwp/WindowsProvider.cs @@ -110,6 +110,8 @@ namespace CommunityToolkit.Authentication /// public override async Task AuthenticateRequestAsync(HttpRequestMessage request) { + AddSdkVersion(request); + string token = await GetTokenAsync(); request.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationHeaderScheme, token); } diff --git a/CommunityToolkit.Graph/Extensions/GraphExtensions.Users.cs b/CommunityToolkit.Graph/Extensions/GraphExtensions.Users.cs index 98d82af..087e998 100644 --- a/CommunityToolkit.Graph/Extensions/GraphExtensions.Users.cs +++ b/CommunityToolkit.Graph/Extensions/GraphExtensions.Users.cs @@ -71,7 +71,7 @@ namespace CommunityToolkit.Graph.Extensions .Photo .Content .Request() - .WithScopes(new string[] { "user.readbasic.all" }) + .WithScopes(new string[] { "user.read" }) .GetAsync(); } diff --git a/SampleTest/SampleTest.csproj b/SampleTest/SampleTest.csproj index 58e74aa..309669f 100644 --- a/SampleTest/SampleTest.csproj +++ b/SampleTest/SampleTest.csproj @@ -155,7 +155,6 @@ - diff --git a/Samples/WpfMsalProviderSample/App.xaml.cs b/Samples/WpfMsalProviderSample/App.xaml.cs deleted file mode 100644 index d8a33ce..0000000 --- a/Samples/WpfMsalProviderSample/App.xaml.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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.Authentication; -using System; -using System.Windows; - -namespace WpfMsalProviderSample -{ - public partial class App : Application - { - protected override void OnActivated(EventArgs e) - { - if (ProviderManager.Instance.GlobalProvider == null) - { - string clientId = "YOUR-CLIENT-ID-HERE"; - string[] scopes = new string[] { "User.Read" }; - string redirectUri = "http://localhost"; - ProviderManager.Instance.GlobalProvider = new MsalProvider(clientId, scopes, redirectUri); - } - - base.OnActivated(e); - } - } -} diff --git a/Samples/WpfNet5WindowsMsalProviderSample/App.xaml b/Samples/WpfNet5WindowsMsalProviderSample/App.xaml new file mode 100644 index 0000000..35cad72 --- /dev/null +++ b/Samples/WpfNet5WindowsMsalProviderSample/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/Samples/WpfNet5WindowsMsalProviderSample/App.xaml.cs b/Samples/WpfNet5WindowsMsalProviderSample/App.xaml.cs new file mode 100644 index 0000000..928b0ec --- /dev/null +++ b/Samples/WpfNet5WindowsMsalProviderSample/App.xaml.cs @@ -0,0 +1,51 @@ +// 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.Threading.Tasks; +using System.Windows; +using CommunityToolkit.Authentication; +using CommunityToolkit.Authentication.Extensions; +using Microsoft.Identity.Client.Extensions.Msal; + +namespace WpfNet5WindowsMsalProviderSample +{ + public partial class App : Application + { + static readonly string ClientId = "YOUR-CLIENT-ID-HERE"; + static readonly string[] Scopes = new string[] { "User.Read" }; + + protected override void OnActivated(EventArgs e) + { + InitializeGlobalProviderAsync(); + base.OnActivated(e); + } + + private async Task InitializeGlobalProviderAsync() + { + if (ProviderManager.Instance.GlobalProvider == null) + { + var provider = new MsalProvider(ClientId, Scopes, null, false, true); + + // Configure the token cache storage for non-UWP applications. + var storageProperties = new StorageCreationPropertiesBuilder(CacheConfig.CacheFileName, CacheConfig.CacheDir) + .WithLinuxKeyring( + CacheConfig.LinuxKeyRingSchema, + CacheConfig.LinuxKeyRingCollection, + CacheConfig.LinuxKeyRingLabel, + CacheConfig.LinuxKeyRingAttr1, + CacheConfig.LinuxKeyRingAttr2) + .WithMacKeyChain( + CacheConfig.KeyChainServiceName, + CacheConfig.KeyChainAccountName) + .Build(); + await provider.InitTokenCacheAsync(storageProperties); + + ProviderManager.Instance.GlobalProvider = provider; + + await provider.TrySilentSignInAsync(); + } + } + } +} diff --git a/Samples/WpfMsalProviderSample/AssemblyInfo.cs b/Samples/WpfNet5WindowsMsalProviderSample/AssemblyInfo.cs similarity index 100% rename from Samples/WpfMsalProviderSample/AssemblyInfo.cs rename to Samples/WpfNet5WindowsMsalProviderSample/AssemblyInfo.cs diff --git a/Samples/WpfNet5WindowsMsalProviderSample/CacheConfig.cs b/Samples/WpfNet5WindowsMsalProviderSample/CacheConfig.cs new file mode 100644 index 0000000..5986a84 --- /dev/null +++ b/Samples/WpfNet5WindowsMsalProviderSample/CacheConfig.cs @@ -0,0 +1,27 @@ +// 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.Collections.Generic; +using Microsoft.Identity.Client.Extensions.Msal; + +namespace WpfNet5WindowsMsalProviderSample +{ + /// + /// https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/wiki/Cross-platform-Token-Cache + /// + class CacheConfig + { + public const string CacheFileName = "msal_cache.dat"; + public const string CacheDir = "MSAL_CACHE"; + + public const string KeyChainServiceName = "msal_service"; + public const string KeyChainAccountName = "msal_account"; + + public const string LinuxKeyRingSchema = "com.contoso.devtools.tokencache"; + public const string LinuxKeyRingCollection = MsalCacheHelper.LinuxKeyRingDefaultCollection; + public const string LinuxKeyRingLabel = "MSAL token cache for all Contoso dev tool apps."; + public static readonly KeyValuePair LinuxKeyRingAttr1 = new KeyValuePair("Version", "1"); + public static readonly KeyValuePair LinuxKeyRingAttr2 = new KeyValuePair("ProductGroup", "MyApps"); + } +} diff --git a/Samples/WpfNet5WindowsMsalProviderSample/LoginButton.xaml b/Samples/WpfNet5WindowsMsalProviderSample/LoginButton.xaml new file mode 100644 index 0000000..d6dfc4f --- /dev/null +++ b/Samples/WpfNet5WindowsMsalProviderSample/LoginButton.xaml @@ -0,0 +1,14 @@ + + +