Skip to content

Commit

Permalink
Coe updates and added functionality
Browse files Browse the repository at this point in the history
Added WIA support
bugfix in ManagedIdentityClientApp
added support for multiple scopes in single factory
  • Loading branch information
jformacek committed Sep 25, 2022
2 parents ec672c9 + 5ee7e79 commit fdfe5d9
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 70 deletions.
126 changes: 97 additions & 29 deletions Authentication/AadAuthenticationFactory.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Identity.Client;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Security;
Expand Down Expand Up @@ -37,11 +38,7 @@ public class AadAuthenticationFactory
public string[] Scopes {get {return _scopes;}}
private readonly string[] _scopes;

/// <summary>
/// Authentication mode for public client flows
/// </summary>
public AuthenticationMode AuthMode { get {return _authMode;}}
private readonly AuthenticationMode _authMode;
//type of auth flow to use
private readonly AuthenticationFlow _flow;

/// <summary>
Expand All @@ -50,14 +47,21 @@ public class AadAuthenticationFactory
public string UserName { get { return _userNameHint; } }
private readonly string _userNameHint;

/// <summary>
/// Password for ROPC flow
/// </summary>
private readonly SecureString _resourceOwnerPassword;

private readonly IPublicClientApplication _publicClientApplication;
private readonly IConfidentialClientApplication _confidentialClientApplication;
private readonly ManagedIdentityClientApplication _managedIdentityClientApplication;
private readonly string _defaultClientId = "1950a258-227b-4e31-a9cf-717495945fc2";

/// <summary>
/// Creates factory that supporrts Public client flows with Interactive or DeviceCode authentication
/// </summary>
/// <param name="tenantId">DNS name or Id of tenant that authenticates user</param>
/// <param name="clientId">ClientId to use</param>
/// <param name="clientId">ClientId to use. If not specified, clientId of Azure Powershell is used</param>
/// <param name="scopes">List of scopes that clients asks for</param>
/// <param name="loginApi">AAD endpoint that will handle the authentication.</param>
/// <param name="authenticationMode">Type of public client flow to use</param>
Expand All @@ -70,7 +74,11 @@ public AadAuthenticationFactory(
AuthenticationMode authenticationMode = AuthenticationMode.Interactive,
string userNameHint = null)
{
_clientId = clientId;
if (string.IsNullOrWhiteSpace(clientId))
_clientId = _defaultClientId;
else
_clientId = clientId;

_loginApi = loginApi;
_scopes = scopes;
_userNameHint = userNameHint;
Expand Down Expand Up @@ -149,33 +157,69 @@ public AadAuthenticationFactory(

var builder = ConfidentialClientApplicationBuilder.Create(_clientId)
.WithCertificate(clientCertificate)
.WithAuthority($"{_loginApi}/{tenantId}");
.WithAuthority($"{_loginApi}/{tenantId}")
.WithHttpClientFactory(new GcMsalHttpClientFactory());

_confidentialClientApplication = builder.Build();
}

/// <summary>
/// Creates factory that supports ManagedIdentity authentication
/// Creates factory that supports UserAssignedIdentity authentication with provided client id
/// </summary>
/// <param name="scopes">Required scopes to obtain. Currently obtains all assigned scopes for first resource in the array of scopes.</param>
public AadAuthenticationFactory(string[] scopes)
/// <param name="clientId">AppId of User Assigned Identity</param>
/// <param name="scopes">Required scopes to obtain. Currently obtains all assigned scopes for first resource in the array.</param>
public AadAuthenticationFactory(string clientId, string[] scopes)
{
_scopes = scopes;
_managedIdentityClientApplication = new ManagedIdentityClientApplication(new GcMsalHttpClientFactory());
_flow = AuthenticationFlow.ManagedIdentity;
if (!string.IsNullOrWhiteSpace(clientId))
{
_clientId = clientId;
}
else
{
_clientId=null;
}
_managedIdentityClientApplication = new ManagedIdentityClientApplication(new GcMsalHttpClientFactory(), _clientId);
_flow = AuthenticationFlow.UserAssignedIdentity;
}

/// <summary>
/// Creates factory that supports UserAssignedIdentity authentication with provided client id
/// Creates factory that supporrts Public client ROPC flow
/// </summary>
/// <param name="clientId">AppId of User Assigned Identity</param>
/// <param name="scopes">Required scopes to obtain. Currently obtains all assigned scopes for first resource in the array.</param>
public AadAuthenticationFactory(string clientId, string[] scopes)
/// <param name="tenantId">DNS name or Id of tenant that authenticates user</param>
/// <param name="clientId">ClientId to use</param>
/// <param name="scopes">List of scopes that clients asks for</param>
/// <param name="loginApi">AAD endpoint that will handle the authentication.</param>
/// <param name="userName">Resource owner username and password</param>
/// <param name="password">Resource owner password</param>
public AadAuthenticationFactory(
string tenantId,
string clientId,
string[] scopes,
string userName,
SecureString password,
string loginApi = "https://login.microsoftonline.com"
)
{
if (string.IsNullOrWhiteSpace(clientId))
_clientId = _defaultClientId;
else
_clientId = clientId;

_loginApi = loginApi;
_scopes = scopes;
_clientId = clientId;
_managedIdentityClientApplication = new ManagedIdentityClientApplication(new GcMsalHttpClientFactory(), clientId);
_flow = AuthenticationFlow.UserAssignedIdentity;
_userNameHint = userName;
_resourceOwnerPassword = password;
_tenantId = tenantId;

_flow = AuthenticationFlow.ResourceOwnerPassword;

var builder = PublicClientApplicationBuilder.Create(_clientId)
.WithDefaultRedirectUri()
.WithAuthority($"{_loginApi}/{tenantId}")
.WithHttpClientFactory(new GcMsalHttpClientFactory());

_publicClientApplication = builder.Build();
}

/// <summary>
Expand All @@ -184,10 +228,12 @@ public AadAuthenticationFactory(string clientId, string[] scopes)
/// </summary>
/// <returns cref="AuthenticationResult">Authentication result object either returned fropm MSAL libraries, or - for ManagedIdentity - constructed from Managed Identity endpoint response, as returned by cref="ManagedIdentityClientApplication.ApiVersion" version of endpoint</returns>
/// <exception cref="ArgumentException">Throws if unsupported authentication mode or flow detected</exception>
public async Task<AuthenticationResult> AuthenticateAsync()
public async Task<AuthenticationResult> AuthenticateAsync(string[] requiredScopes = null)
{
using CancellationTokenSource cts = new(TimeSpan.FromMinutes(2));
AuthenticationResult result;
if (null == requiredScopes)
requiredScopes = _scopes;
switch(_flow)
{
case AuthenticationFlow.PublicClientWithWia:
Expand All @@ -200,7 +246,7 @@ public async Task<AuthenticationResult> AuthenticateAsync()
account = accounts.Where(x => string.Compare(x.Username, _userNameHint, true) == 0).FirstOrDefault();
if (null!=account)
{
result = await _publicClientApplication.AcquireTokenSilent(_scopes, account)
result = await _publicClientApplication.AcquireTokenSilent(requiredScopes, account)
.ExecuteAsync();
}
else
Expand All @@ -222,12 +268,12 @@ public async Task<AuthenticationResult> AuthenticateAsync()
account = accounts.Where(x => string.Compare(x.Username, _userNameHint, true) == 0).FirstOrDefault();
try
{
result = await _publicClientApplication.AcquireTokenSilent(_scopes, account)
result = await _publicClientApplication.AcquireTokenSilent(requiredScopes, account)
.ExecuteAsync(cts.Token);
}
catch (MsalUiRequiredException)
{
result = await _publicClientApplication.AcquireTokenInteractive(_scopes).ExecuteAsync(cts.Token);
result = await _publicClientApplication.AcquireTokenInteractive(requiredScopes).ExecuteAsync(cts.Token);
}
return result;
}
Expand All @@ -241,12 +287,12 @@ public async Task<AuthenticationResult> AuthenticateAsync()
account = accounts.Where(x => string.Compare(x.Username, _userNameHint, true) == 0).FirstOrDefault();
try
{
result = await _publicClientApplication.AcquireTokenSilent(_scopes, account)
result = await _publicClientApplication.AcquireTokenSilent(requiredScopes, account)
.ExecuteAsync(cts.Token);
}
catch (MsalUiRequiredException)
{
result = await _publicClientApplication.AcquireTokenWithDeviceCode(_scopes, callback =>
result = await _publicClientApplication.AcquireTokenWithDeviceCode(requiredScopes, callback =>
{
Console.WriteLine(callback.Message);
return Task.FromResult(0);
Expand All @@ -255,12 +301,34 @@ public async Task<AuthenticationResult> AuthenticateAsync()
return result;
}
case AuthenticationFlow.ConfidentialClient:
return await _confidentialClientApplication.AcquireTokenForClient(_scopes).ExecuteAsync(cts.Token);
return await _confidentialClientApplication.AcquireTokenForClient(requiredScopes).ExecuteAsync(cts.Token);
//System Managed identity
case AuthenticationFlow.ManagedIdentity:
return await _managedIdentityClientApplication.AcquireTokenForClientAsync(_scopes, cts.Token);
return await _managedIdentityClientApplication.AcquireTokenForClientAsync(requiredScopes, cts.Token);
//User managed identity
case AuthenticationFlow.UserAssignedIdentity:
return await _managedIdentityClientApplication.AcquireTokenForClientAsync(_scopes, cts.Token);
return await _managedIdentityClientApplication.AcquireTokenForClientAsync(requiredScopes, cts.Token);
//ROPC flow
case AuthenticationFlow.ResourceOwnerPassword:
{
var accounts = await _publicClientApplication.GetAccountsAsync();
IAccount account;
if (string.IsNullOrWhiteSpace(_userNameHint))
account = accounts.FirstOrDefault();
else
account = accounts.Where(x => string.Compare(x.Username, _userNameHint, true) == 0).FirstOrDefault();

try
{
result = await _publicClientApplication.AcquireTokenSilent(requiredScopes, account)
.ExecuteAsync(cts.Token);
}
catch (MsalUiRequiredException)
{
result = await _publicClientApplication.AcquireTokenByUsernamePassword(requiredScopes, _userNameHint, _resourceOwnerPassword).ExecuteAsync(cts.Token);
}
return result;
}
}

throw new ArgumentException($"Unsupported authentication flow: {_flow}");
Expand Down
2 changes: 1 addition & 1 deletion Authentication/Authentication.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>GreyCorbel.Identity.Authentication</AssemblyName>
<RootNamespace>GreyCorbel.Identity.Authentication</RootNamespace>
<Version>1.1.1</Version>
<Version>1.2.0</Version>
<Authors>Jiri Formacek</Authors>
<Company>GreyCorbel Solutions</Company>
<Product>Unified AAD Authentication client library for Public, Confidential and ManagedIdentity client authentication</Product>
Expand Down
3 changes: 2 additions & 1 deletion Authentication/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ enum AuthenticationFlow
PublicClientWithWia,
ConfidentialClient,
ManagedIdentity,
UserAssignedIdentity
UserAssignedIdentity,
ResourceOwnerPassword
}
}
15 changes: 9 additions & 6 deletions Authentication/ManagedIdentityClientApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ class ManagedIdentityClientApplication:TokenProvider

{
ITokenProvider _tokenProvider = null;
AuthenticationResult _cachedToken = null;
Dictionary <string,AuthenticationResult> _cachedTokens = new Dictionary<string, AuthenticationResult>(StringComparer.InvariantCultureIgnoreCase);
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);


public ManagedIdentityClientApplication(IMsalHttpClientFactory factory, string clientId = null)
:base(factory, clientId)
{
Expand All @@ -34,19 +33,23 @@ public ManagedIdentityClientApplication(IMsalHttpClientFactory factory, string c
public override async Task<AuthenticationResult> AcquireTokenForClientAsync(string[] scopes, CancellationToken cancellationToken)
{
await _lock.WaitAsync().ConfigureAwait(false);

try
{
if (null == _cachedToken || _cachedToken.ExpiresOn.UtcDateTime < DateTime.UtcNow.AddSeconds(-_ticketOverlapSeconds))
string resource = ScopeHelper.ScopeToResource(scopes);
if(! _cachedTokens.ContainsKey(resource) || _cachedTokens[resource].ExpiresOn.UtcDateTime < DateTime.UtcNow.AddSeconds(-_ticketOverlapSeconds))
{
if (null != _tokenProvider)
{
_cachedToken = await _tokenProvider.AcquireTokenForClientAsync(scopes, cancellationToken).ConfigureAwait(false);
_cachedTokens[resource] = await _tokenProvider.AcquireTokenForClientAsync(scopes, cancellationToken).ConfigureAwait(false);
}
else
throw new InvalidOperationException("Token provider not initialized");
}
return _cachedToken;
return _cachedTokens[resource];
}
catch(Exception ex)
{
throw ex;
}
finally
{
Expand Down
2 changes: 1 addition & 1 deletion Authentication/TokenProviders/AppServiceTokenProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public override async Task<AuthenticationResult> AcquireTokenForClientAsync(stri

HttpRequestMessage CreateRequestMessage(string[] scopes)
{
using HttpRequestMessage message = new HttpRequestMessage();
HttpRequestMessage message = new HttpRequestMessage();
message.Method = HttpMethod.Get;
StringBuilder sb= new StringBuilder(IdentityEndpoint);
message.Headers.Add(SecretHeaderName, IdentityHeader);
Expand Down
2 changes: 1 addition & 1 deletion Authentication/TokenProviders/VMIdentityTokenProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public override async Task<AuthenticationResult> AcquireTokenForClientAsync(stri

HttpRequestMessage CreateRequestMessage(string[] scopes)
{
using HttpRequestMessage message = new HttpRequestMessage();
HttpRequestMessage message = new HttpRequestMessage();
message.Method = HttpMethod.Get;
StringBuilder sb = new StringBuilder("http://169.254.169.254/metadata/identity/oauth2/token");

Expand Down
Binary file modified Module/AadAuthenticationFactory/AadAuthenticationFactory.psd1
Binary file not shown.
Loading

0 comments on commit fdfe5d9

Please sign in to comment.