Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="MSBuild.Sdk.Extras">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>

<TargetFrameworks>netstandard2.0;uap10.0;net5.0-windows10.0.17763.0;netcoreapp3.1</TargetFrameworks>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion>7</SupportedOSPlatformVersion>

<Title>Windows Community Toolkit .NET Standard Auth Services</Title>
<Description>
This library provides an authentication provider based on the native Windows dialogues. It is part of the Windows Community Toolkit.
Expand All @@ -18,6 +20,10 @@
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="2.19.1" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
<PackageReference Include="Microsoft.Identity.Client.Desktop" Version="4.37.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CommunityToolkit.Authentication\CommunityToolkit.Authentication.csproj" />
</ItemGroup>
Expand Down
151 changes: 118 additions & 33 deletions CommunityToolkit.Authentication.Msal/MsalProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -11,59 +12,90 @@
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
{
/// <summary>
/// <a href="https://github.com/AzureAD/microsoft-authentication-library-for-dotnet">MSAL.NET</a> provider helper for tracking authentication state.
/// </summary>
public class MsalProvider : BaseProvider
{
/// <summary>
/// A prefix value used to create the redirect URI value for use in AAD.
/// </summary>
public static readonly string MSAccountBrokerRedirectUriPrefix = "ms-appx-web://microsoft.aad.brokerplugin/";

private static readonly SemaphoreSlim SemaphoreSlim = new (1);

/// <summary>
/// Gets or sets the currently authenticated user account.
/// </summary>
public IAccount Account { get; protected set; }

/// <inheritdoc />
public override string CurrentAccountId => _account?.HomeAccountId?.Identifier;
public override string CurrentAccountId => Account?.HomeAccountId?.Identifier;

/// <summary>
/// Gets the MSAL.NET Client used to authenticate the user.
/// Gets or sets the MSAL.NET Client used to authenticate the user.
/// </summary>
protected IPublicClientApplication Client { get; private set; }
public IPublicClientApplication Client { get; protected set; }

/// <summary>
/// Gets an array of scopes to use for accessing Graph resources.
/// </summary>
protected string[] Scopes { get; private set; }
protected string[] Scopes { get; }

/// <summary>
/// Initializes a new instance of the <see cref="MsalProvider"/> class using a configuration object.
/// </summary>
/// <param name="client">Registered ClientId in Azure Acitve Directory.</param>
/// <param name="scopes">List of Scopes to initially request.</param>
/// <param name="autoSignIn">Determines whether the provider attempts to silently log in upon creation.</param>
public MsalProvider(IPublicClientApplication client, string[] scopes = null, bool autoSignIn = true)
{
Client = client;
Scopes = scopes.Select(s => s.ToLower()).ToArray() ?? new string[] { string.Empty };

private IAccount _account;
if (autoSignIn)
{
TrySilentSignInAsync();
}
}

/// <summary>
/// Initializes a new instance of the <see cref="MsalProvider"/> class.
/// Initializes a new instance of the <see cref="MsalProvider"/> class with default configuration values.
/// </summary>
/// <param name="clientId">Registered ClientId.</param>
/// <param name="clientId">Registered client id in Azure Acitve Directory.</param>
/// <param name="redirectUri">RedirectUri for auth response.</param>
/// <param name="scopes">List of Scopes to initially request.</param>
/// <param name="autoSignIn">Determines whether the provider attempts to silently log in upon instantionation.</param>
public MsalProvider(string clientId, string[] scopes = null, string redirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient", bool autoSignIn = true)
/// <param name="autoSignIn">Determines whether the provider attempts to silently log in upon creation.</param>
/// <param name="listWindowsWorkAndSchoolAccounts">Determines if organizational accounts should be enabled/disabled.</param>
/// <param name="tenantId">Registered tenant id in Azure Active Directory.</param>
public MsalProvider(string clientId, string[] scopes = null, string redirectUri = null, bool autoSignIn = true, bool listWindowsWorkAndSchoolAccounts = true, string tenantId = null)
{
var client = PublicClientApplicationBuilder.Create(clientId)
.WithAuthority(AzureCloudInstance.AzurePublic, AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount)
.WithRedirectUri(redirectUri)
.WithClientName(ProviderManager.ClientName)
.WithClientVersion(Assembly.GetExecutingAssembly().GetName().Version.ToString())
.Build();

Client = CreatePublicClientApplication(clientId, tenantId, redirectUri, listWindowsWorkAndSchoolAccounts);
Scopes = scopes.Select(s => s.ToLower()).ToArray() ?? new string[] { string.Empty };

Client = client;

if (autoSignIn)
{
_ = TrySilentSignInAsync();
TrySilentSignInAsync();
}
}

/// <inheritdoc/>
public override async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
AddSdkVersion(request);

string token;

// Check if any specific scopes are being requested.
Expand All @@ -87,7 +119,7 @@ optionsMiddleware is AuthenticationHandlerOption options &&
/// <inheritdoc/>
public override async Task<bool> TrySilentSignInAsync()
{
if (_account != null && State == ProviderState.SignedIn)
if (Account != null && State == ProviderState.SignedIn)
{
return true;
}
Expand All @@ -108,7 +140,7 @@ public override async Task<bool> TrySilentSignInAsync()
/// <inheritdoc/>
public override async Task SignInAsync()
{
if (_account != null || State != ProviderState.SignedOut)
if (Account != null || State != ProviderState.SignedOut)
{
return;
}
Expand All @@ -129,10 +161,10 @@ public override async Task SignInAsync()
/// <inheritdoc />
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;
Expand All @@ -144,7 +176,48 @@ public override Task<string> GetTokenAsync(bool silentOnly = false)
return this.GetTokenWithScopesAsync(Scopes, silentOnly);
}

private async Task<string> GetTokenWithScopesAsync(string[] scopes, bool silentOnly = false)
/// <summary>
/// Create an instance of <see cref="PublicClientApplication"/> using the provided config and some default values.
/// </summary>
/// <param name="clientId">Registered ClientId.</param>
/// <param name="tenantId">An optional tenant id.</param>
/// <param name="redirectUri">Redirect uri for auth response.</param>
/// <param name="listWindowsWorkAndSchoolAccounts">Determines if organizational accounts should be supported.</param>
/// <returns>A new instance of <see cref="PublicClientApplication"/>.</returns>
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();
}

/// <summary>
/// Retrieve an authorization token using the provided scopes.
/// </summary>
/// <param name="scopes">An array of scopes to pass along with the Graph request.</param>
/// <param name="silentOnly">A value to determine whether account broker UI should be shown, if required by MSAL.</param>
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
protected async Task<string> GetTokenWithScopesAsync(string[] scopes, bool silentOnly = false)
{
await SemaphoreSlim.WaitAsync();

Expand All @@ -153,7 +226,7 @@ private async Task<string> GetTokenWithScopesAsync(string[] scopes, bool silentO
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();
Expand All @@ -172,14 +245,26 @@ private async Task<string> GetTokenWithScopesAsync(string[] scopes, bool silentO
{
try
{
if (_account != null)
{
authResult = await Client.AcquireTokenInteractive(scopes).WithPrompt(Prompt.NoPrompt).WithAccount(_account).ExecuteAsync();
}
else
var paramBuilder = Client.AcquireTokenInteractive(scopes);

if (Account != null)
{
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
{
Expand All @@ -188,7 +273,7 @@ private async Task<string> GetTokenWithScopesAsync(string[] scopes, bool silentO
}
}

_account = authResult?.Account;
Account = authResult?.Account;

return authResult?.AccessToken;
}
Expand Down
35 changes: 35 additions & 0 deletions CommunityToolkit.Authentication.Msal/MsalProviderExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Helpers for working with the MsalProvider.
/// </summary>
public static class MsalProviderExtensions
{
/// <summary>
/// Helper function to initialize the token cache for non-UWP apps. MSAL handles this automatically on UWP.
/// </summary>
/// <param name="provider">The instance of <see cref="MsalProvider"/> to init the cache for.</param>
/// <param name="storageProperties">Properties for configuring the storage cache.</param>
/// <param name="logger">Passing null uses the default TraceSource logger.</param>
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
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
}
}
}
2 changes: 2 additions & 0 deletions CommunityToolkit.Authentication.Uwp/WindowsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ public WindowsProvider(string[] scopes = null, WebAccountProviderConfig? webAcco
/// <inheritdoc />
public override async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
AddSdkVersion(request);

string token = await GetTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationHeaderScheme, token);
}
Expand Down
2 changes: 1 addition & 1 deletion CommunityToolkit.Graph/Extensions/GraphExtensions.Users.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public static async Task<Stream> GetUserPhoto(this GraphServiceClient graph, str
.Photo
.Content
.Request()
.WithScopes(new string[] { "user.readbasic.all" })
.WithScopes(new string[] { "user.read" })
.GetAsync();
}

Expand Down
1 change: 0 additions & 1 deletion SampleTest/SampleTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@
</ItemGroup>
<ItemGroup>
<Content Include="Assets\FileIcon.png" />
<None Include="Package.StoreAssociation.xml" />
<Content Include="Properties\Default.rd.xml" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\SplashScreen.scale-200.png" />
Expand Down
26 changes: 0 additions & 26 deletions Samples/WpfMsalProviderSample/App.xaml.cs

This file was deleted.

9 changes: 9 additions & 0 deletions Samples/WpfNet5WindowsMsalProviderSample/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Application x:Class="WpfNet5WindowsMsalProviderSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfNet5WindowsMsalProviderSample"
StartupUri="MainWindow.xaml">
<Application.Resources>

</Application.Resources>
</Application>
Loading