Skip to content
This repository has been archived by the owner on Jul 22, 2024. It is now read-only.

Commit

Permalink
Authentication feature update (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
Isaiah Williams authored Dec 19, 2019
1 parent ba8b731 commit d8910b3
Show file tree
Hide file tree
Showing 18 changed files with 452 additions and 209 deletions.
7 changes: 6 additions & 1 deletion ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@

# Change Log

## Upcoming Release
## 3.0.2 - December 2019

* Authentication
* Addressed issue [#230](https://github.com/microsoft/Partner-Center-PowerShell/issues/230) that was caused by a deadlock

## 3.0.1 - December 2019

* Authentication
* Updating the [Connect-PartnerCenter](https://docs.microsoft.com/powershell/module/partnercenter/Connect-PartnerCenter) command to make the `CertificateThumbprint` parameter required for the `ServicePrincipalCertificate` parameter set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ public void AppendAuthenticator(Func<IAuthenticator> constructor)
return;
}
}
}
}
172 changes: 139 additions & 33 deletions src/PowerShell/Authenticators/DelegatingAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ namespace Microsoft.Store.PartnerCenter.PowerShell.Authenticators
using System.Threading;
using System.Threading.Tasks;
using Extensions;
using Factories;
using Identity.Client;
using Models.Authentication;
using Utilities;

/// <summary>
/// Provides a chain of responsibility pattern for authenticators.
Expand Down Expand Up @@ -39,38 +39,166 @@ internal abstract class DelegatingAuthenticator : IAuthenticator
public abstract bool CanAuthenticate(AuthenticationParameters parameters);

/// <summary>
///
/// Gets an aptly configured client.
/// </summary>
/// <param name="account"></param>
/// <param name="environment"></param>
/// <param name="redirectUri"></param>
/// <returns></returns>
public IClientApplicationBase GetClient(PartnerAccount account, PartnerEnvironment environment, string redirectUri = null)
/// <param name="account">The account information to be used when generating the client.</param>
/// <param name="environment">The environment where the client is connecting.</param>
/// <param name="redirectUri">The redirect URI for the client.</param>
/// <returns>An aptly configured client.</returns>
public async Task<IClientApplicationBase> GetClientAsync(PartnerAccount account, PartnerEnvironment environment, string redirectUri = null)
{
IClientApplicationBase app;

if (account.IsPropertySet(PartnerAccountPropertyType.CertificateThumbprint) || account.IsPropertySet(PartnerAccountPropertyType.ServicePrincipalSecret))
{
app = SharedTokenCacheClientFactory.CreateConfidentialClient(
app = await CreateConfidentialClientAsync(
$"{environment.ActiveDirectoryAuthority}{account.Tenant}",
account.GetProperty(PartnerAccountPropertyType.ApplicationId),
account.GetProperty(PartnerAccountPropertyType.ServicePrincipalSecret),
GetCertificate(account.GetProperty(PartnerAccountPropertyType.CertificateThumbprint)),
redirectUri,
account.Tenant);
account.Tenant).ConfigureAwait(false);
}
else
{
app = SharedTokenCacheClientFactory.CreatePublicClient(
app = await CreatePublicClient(
$"{environment.ActiveDirectoryAuthority}{account.Tenant}",
account.GetProperty(PartnerAccountPropertyType.ApplicationId),
redirectUri,
account.Tenant);
account.Tenant).ConfigureAwait(false);
}

return app;
}

/// <summary>
/// Determine if this request can be authenticated using the given authenticator, and authenticate if it can.
/// </summary>
/// <param name="parameters">The complex object containing authentication specific information.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns><c>true</c> if the request can be authenticated; otherwise <c>false</c>.</returns>
public async Task<AuthenticationResult> TryAuthenticateAsync(AuthenticationParameters parameters, CancellationToken cancellationToken = default)
{
if (CanAuthenticate(parameters))
{
return await AuthenticateAsync(parameters, cancellationToken).ConfigureAwait(false);
}

if (Next != null)
{
return await Next.TryAuthenticateAsync(parameters, cancellationToken).ConfigureAwait(false);
}

return null;
}

/// <summary>
/// Creates a confidential client used for generating tokens.
/// </summary>
/// <param name="authority">Address of the authority to issue the token</param>
/// <param name="clientId">Identifier of the client requesting the token.</param>
/// <param name="certificate">Certificate used by the client requesting the token.</param>
/// <param name="clientSecret">Secret of the client requesting the token.</param>
/// <param name="redirectUri">The redirect URI for the client.</param>
/// <param name="tenantId">Identifier of the tenant requesting the token.</param>
/// <returns>An aptly configured confidential client.</returns>
private static async Task<IConfidentialClientApplication> CreateConfidentialClientAsync(
string authority = null,
string clientId = null,
string clientSecret = null,
X509Certificate2 certificate = null,
string redirectUri = null,
string tenantId = null)
{
ConfidentialClientApplicationBuilder builder = ConfidentialClientApplicationBuilder.Create(clientId);

if (!string.IsNullOrEmpty(authority))
{
builder = builder.WithAuthority(authority);
}

if (!string.IsNullOrEmpty(clientSecret))
{
builder = builder.WithClientSecret(clientSecret);
}

if (certificate != null)
{
builder = builder.WithCertificate(certificate);
}

if (!string.IsNullOrEmpty(redirectUri))
{
builder = builder.WithRedirectUri(redirectUri);
}

if (!string.IsNullOrEmpty(tenantId))
{
builder = builder.WithTenantId(tenantId);
}

IConfidentialClientApplication client = builder.WithLogging((level, message, pii) =>
{
PartnerSession.Instance.DebugMessages.Enqueue($"[MSAL] {level} {message}");
}).Build();


PartnerTokenCache tokenCache = new PartnerTokenCache(clientId);

client.UserTokenCache.SetAfterAccess(tokenCache.AfterAccessNotification);
client.UserTokenCache.SetBeforeAccess(tokenCache.BeforeAccessNotification);

await Task.CompletedTask.ConfigureAwait(false);

return client;
}

/// <summary>
/// Creates a public client used for generating tokens.
/// </summary>
/// <param name="authority">Address of the authority to issue the token</param>
/// <param name="clientId">Identifier of the client requesting the token.</param>
/// <param name="redirectUri">The redirect URI for the client.</param>
/// <param name="tenantId">Identifier of the tenant requesting the token.</param>
/// <returns>An aptly configured public client.</returns>
private static async Task<IPublicClientApplication> CreatePublicClient(
string authority = null,
string clientId = null,
string redirectUri = null,
string tenantId = null)
{
PublicClientApplicationBuilder builder = PublicClientApplicationBuilder.Create(clientId);

if (!string.IsNullOrEmpty(authority))
{
builder = builder.WithAuthority(authority);
}

if (!string.IsNullOrEmpty(redirectUri))
{
builder = builder.WithRedirectUri(redirectUri);
}

if (!string.IsNullOrEmpty(tenantId))
{
builder = builder.WithTenantId(tenantId);
}

IPublicClientApplication client = builder.WithLogging((level, message, pii) =>
{
PartnerSession.Instance.DebugMessages.Enqueue($"[MSAL] {level} {message}");
}).Build();

PartnerTokenCache tokenCache = new PartnerTokenCache(clientId);

client.UserTokenCache.SetAfterAccess(tokenCache.AfterAccessNotification);
client.UserTokenCache.SetBeforeAccess(tokenCache.BeforeAccessNotification);

await Task.CompletedTask.ConfigureAwait(false);

return client;
}

/// <summary>
/// Gets the specified certificate.
/// </summary>
Expand Down Expand Up @@ -121,27 +249,5 @@ private bool FindCertificateByThumbprint(string thumbprint, StoreLocation storeL
store?.Close();
}
}

/// <summary>
/// Determine if this request can be authenticated using the given authenticator, and authenticate if it can.
/// </summary>
/// <param name="parameters">The complex object containing authentication specific information.</param>
/// <param name="token">The token based authentication information.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns><c>true</c> if the request can be authenticated; otherwise <c>false</c>.</returns>
public async Task<AuthenticationResult> TryAuthenticateAsync(AuthenticationParameters parameters, CancellationToken cancellationToken = default)
{
if (CanAuthenticate(parameters))
{
return await AuthenticateAsync(parameters, cancellationToken).ConfigureAwait(false);
}

if (Next != null)
{
return await Next.TryAuthenticateAsync(parameters, cancellationToken).ConfigureAwait(false);
}

return null;
}
}
}
4 changes: 2 additions & 2 deletions src/PowerShell/Authenticators/DeviceCodeAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ internal class DeviceCodeAuthenticator : DelegatingAuthenticator
/// </returns>
public override async Task<AuthenticationResult> AuthenticateAsync(AuthenticationParameters parameters, CancellationToken cancellationToken = default)
{
IPublicClientApplication app = GetClient(parameters.Account, parameters.Environment).AsPublicClient();
IClientApplicationBase app = await GetClientAsync(parameters.Account, parameters.Environment).ConfigureAwait(false);

ServiceClientTracing.Information($"[DeviceCodeAuthenticator] Calling AcquireTokenWithDeviceCode - Scopes: '{string.Join(", ", parameters.Scopes)}'");
return await app.AcquireTokenWithDeviceCode(parameters.Scopes, deviceCodeResult =>
return await app.AsPublicClient().AcquireTokenWithDeviceCode(parameters.Scopes, deviceCodeResult =>
{
WriteWarning(deviceCodeResult.Message);
return Task.CompletedTask;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public override async Task<AuthenticationResult> AuthenticateAsync(Authenticatio
}
}

app = GetClient(parameters.Account, parameters.Environment, redirectUri);
app = await GetClientAsync(parameters.Account, parameters.Environment, redirectUri).ConfigureAwait(false);

if (app is IConfidentialClientApplication)
{
Expand Down
2 changes: 1 addition & 1 deletion src/PowerShell/Authenticators/RefreshTokenAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal class RefreshTokenAuthenticator : DelegatingAuthenticator
/// </returns>
public override async Task<AuthenticationResult> AuthenticateAsync(AuthenticationParameters parameters, CancellationToken cancellationToken = default)
{
IClientApplicationBase app = GetClient(parameters.Account, parameters.Environment);
IClientApplicationBase app = await GetClientAsync(parameters.Account, parameters.Environment);

ServiceClientTracing.Information("[RefreshTokenAuthenticator] Calling GetAccountsAysnc");
IAccount account = await app.GetAccountAsync(parameters.Account.Identifier).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ internal class ServicePrincipalAuthenticator : DelegatingAuthenticator
/// </returns>
public override async Task<AuthenticationResult> AuthenticateAsync(AuthenticationParameters parameters, CancellationToken cancellationToken = default)
{
IConfidentialClientApplication app = GetClient(parameters.Account, parameters.Environment).AsConfidentialClient();
IClientApplicationBase app = await GetClientAsync(parameters.Account, parameters.Environment).ConfigureAwait(false);

return await app.AcquireTokenForClient(parameters.Scopes).ExecuteAsync(cancellationToken).ConfigureAwait(false);
return await app.AsConfidentialClient().AcquireTokenForClient(parameters.Scopes).ExecuteAsync(cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand Down
9 changes: 4 additions & 5 deletions src/PowerShell/Authenticators/SilentAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,16 @@ internal class SilentAuthenticator : DelegatingAuthenticator
/// </returns>
public override async Task<AuthenticationResult> AuthenticateAsync(AuthenticationParameters parameters, CancellationToken cancellationToken = default)
{
IPublicClientApplication app = GetClient(parameters.Account, parameters.Environment).AsPublicClient();
IClientApplicationBase app = await GetClientAsync(parameters.Account, parameters.Environment).ConfigureAwait(false);

ServiceClientTracing.Information(string.Format("[SilentAuthenticator] Calling GetAccountsAsync"));
IEnumerable<IAccount> accounts = await app.GetAccountsAsync().ConfigureAwait(false);
IEnumerable<IAccount> accounts = await app.AsPublicClient().GetAccountsAsync().ConfigureAwait(false);

ServiceClientTracing.Information($"[SilentAuthenticator] Calling AcquireTokenSilent - Scopes: '{string.Join(",", parameters.Scopes)}', UserId: '{((SilentParameters)parameters).UserId}', Number of accounts: '{accounts.Count()}'");
AuthenticationResult authResult = await app.AcquireTokenSilent(
AuthenticationResult authResult = await app.AsPublicClient().AcquireTokenSilent(
parameters.Scopes,
accounts.FirstOrDefault(a => a.HomeAccountId.ObjectId.Equals(((SilentParameters)parameters).UserId)))
.ExecuteAsync(cancellationToken)
.ConfigureAwait(false);
.ExecuteAsync(cancellationToken).ConfigureAwait(false);

return authResult;
}
Expand Down
6 changes: 3 additions & 3 deletions src/PowerShell/Commands/NewPartnerAccessToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ namespace Microsoft.Store.PartnerCenter.PowerShell.Commands
using System.Text;
using Extensions;
using Identity.Client;
using Microsoft.Store.PartnerCenter.PowerShell.Factories;
using Models.Authentication;
using Newtonsoft.Json.Linq;
using Utilities;

[Cmdlet(VerbsCommon.New, "PartnerAccessToken")]
[OutputType(typeof(AuthResult))]
Expand Down Expand Up @@ -210,7 +210,7 @@ public override void ExecuteCmdlet()
Message,
CancellationToken).ConfigureAwait(false);

byte[] cacheData = SharedTokenCacheClientFactory.GetMsalCacheStorage(ApplicationId).ReadData();
byte[] cacheData = PartnerTokenCache.GetMsalCacheStorage(ApplicationId).ReadData();

IEnumerable<string> knownPropertyNames = new[] { "AccessToken", "RefreshToken", "IdToken", "Account", "AppMetadata" };

Expand Down Expand Up @@ -257,7 +257,7 @@ public override void ExecuteCmdlet()

if (authResult.Account != null)
{
string key = SharedTokenCacheClientFactory.GetTokenCacheKey(authResult, applicationId);
string key = PartnerTokenCache.GetTokenCacheKey(authResult, applicationId);

if (tokens.ContainsKey(key))
{
Expand Down
6 changes: 5 additions & 1 deletion src/PowerShell/Commands/PartnerAsyncCmdlet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public abstract class PartnerAsyncCmdlet : PartnerPSCmdlet
/// </summary>
private readonly ConcurrentQueue<Task> outputTasks = new ConcurrentQueue<Task>();

public event EventHandler OnTaskCompleted;

/// <summary>
/// Gets the scheduler used for task execution.
/// </summary>
Expand All @@ -73,7 +75,7 @@ protected override void BeginProcessing()
{
base.BeginProcessing();

Scheduler = new ConcurrencyTaskScheduler(1000, CancellationToken);
Scheduler = new ConcurrencyTaskScheduler(100, CancellationToken);

Scheduler.OnError += Scheduler_OnError;

Expand Down Expand Up @@ -116,6 +118,8 @@ protected override void EndProcessing()
}
while (!Scheduler.CheckForComplete(500, CancellationToken));

OnTaskCompleted?.Invoke(this, null);

if (!outputTasks.IsEmpty)
{
while (outputTasks.TryDequeue(out Task task))
Expand Down
Loading

0 comments on commit d8910b3

Please sign in to comment.