Skip to content

Commit

Permalink
Federated Credentials (#6838)
Browse files Browse the repository at this point in the history
* Federated Credentials

* Corrected duplicate dependencies

---------

Co-authored-by: Tracy Boehrer <trboehre@microsoft.com>
  • Loading branch information
tracyboehrer and Tracy Boehrer committed Aug 21, 2024
1 parent b11f048 commit bfbbbca
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Net.Http;
using System.Threading;
using Microsoft.Extensions.Logging;

namespace Microsoft.Bot.Connector.Authentication
{
/// <summary>
/// Federated Credentials auth implementation.
/// </summary>
public class FederatedAppCredentials : AppCredentials
{
private readonly string _clientId;

/// <summary>
/// Initializes a new instance of the <see cref="FederatedAppCredentials"/> class.
/// </summary>
/// <param name="appId">App ID for the Application.</param>
/// <param name="clientId">Client ID for the managed identity assigned to the bot.</param>
/// <param name="channelAuthTenant">Optional. The token tenant.</param>
/// <param name="oAuthScope">Optional. The scope for the token.</param>
/// <param name="customHttpClient">Optional <see cref="HttpClient"/> to be used when acquiring tokens.</param>
/// <param name="logger">Optional <see cref="ILogger"/> to gather telemetry data while acquiring and managing credentials.</param>
public FederatedAppCredentials(string appId, string clientId, string channelAuthTenant = null, string oAuthScope = null, HttpClient customHttpClient = null, ILogger logger = null)
: base(channelAuthTenant, customHttpClient, logger, oAuthScope)
{
if (string.IsNullOrWhiteSpace(appId))
{
throw new ArgumentNullException(nameof(appId));
}

MicrosoftAppId = appId;
_clientId = clientId;
}

/// <inheritdoc/>
protected override Lazy<IAuthenticator> BuildIAuthenticator()
{
return new Lazy<IAuthenticator>(
() => new FederatedAuthenticator(MicrosoftAppId, _clientId, OAuthEndpoint, OAuthScope, CustomHttpClient, Logger),
LazyThreadSafetyMode.ExecutionAndPublication);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;

namespace Microsoft.Bot.Connector.Authentication
{
/// <summary>
/// Abstraction to acquire tokens from a Federated Credentials Application.
/// </summary>
internal class FederatedAuthenticator : IAuthenticator
{
private readonly string _authority;
private readonly string _scope;
private readonly string _clientId;
private readonly ILogger _logger;
private readonly IConfidentialClientApplication _clientApplication;
private readonly ManagedIdentityClientAssertion _managedIdentityClientAssertion;

/// <summary>
/// Initializes a new instance of the <see cref="FederatedAuthenticator"/> class.
/// </summary>
/// <param name="appId">App id for the Application.</param>
/// <param name="clientId">Client id for the managed identity to be used for acquiring tokens.</param>
/// <param name="scope">Resource for which to acquire the token.</param>
/// <param name="authority">Login endpoint for request.</param>
/// <param name="customHttpClient">A customized instance of the HttpClient class.</param>
/// <param name="logger">The type used to perform logging.</param>
public FederatedAuthenticator(string appId, string clientId, string authority, string scope, HttpClient customHttpClient = null, ILogger logger = null)
{
if (string.IsNullOrWhiteSpace(appId))
{
throw new ArgumentNullException(nameof(appId));
}

if (string.IsNullOrWhiteSpace(scope))
{
throw new ArgumentNullException(nameof(scope));
}

_authority = authority;
_scope = scope;
_clientId = clientId;
_logger = logger ?? NullLogger.Instance;
_clientApplication = CreateClientApplication(appId, customHttpClient);
_managedIdentityClientAssertion = new ManagedIdentityClientAssertion(_clientId);
}

/// <inheritdoc/>
public async Task<AuthenticatorResult> GetTokenAsync(bool forceRefresh = false)
{
var watch = Stopwatch.StartNew();

var result = await Retry
.Run(() => AcquireTokenAsync(forceRefresh), HandleTokenProviderException)
.ConfigureAwait(false);

watch.Stop();
_logger.LogInformation($"GetTokenAsync: Acquired token using MSI in {watch.ElapsedMilliseconds}.");

return result;
}

private async Task<AuthenticatorResult> AcquireTokenAsync(bool forceRefresh)
{
const string scopePostFix = "/.default";
var scope = _scope;

if (!scope.EndsWith(scopePostFix, StringComparison.OrdinalIgnoreCase))
{
scope = $"{scope}{scopePostFix}";
}

_logger.LogDebug($"AcquireTokenAsync: authority={_authority}, scope={scope}");

var authResult = await _clientApplication
.AcquireTokenForClient(new[] { scope })
.WithAuthority(_authority, true)
.WithForceRefresh(forceRefresh)
.ExecuteAsync()
.ConfigureAwait(false);
return new AuthenticatorResult
{
AccessToken = authResult.AccessToken,
ExpiresOn = authResult.ExpiresOn
};
}

private RetryParams HandleTokenProviderException(Exception e, int retryCount)
{
_logger.LogError(e, "Exception when trying to acquire token using Federated Credentials!");

if (e is MsalServiceException exception)
{
// stop retrying for all except for throttling response
if (exception.StatusCode != 429)
{
return RetryParams.StopRetrying;
}
}

return RetryParams.DefaultBackOff(retryCount);
}

private IConfidentialClientApplication CreateClientApplication(string appId, HttpClient customHttpClient = null)
{
_logger.LogDebug($"CreateClientApplication for appId={appId}");

var clientBuilder = ConfidentialClientApplicationBuilder
.Create(appId)
.WithClientAssertion((AssertionRequestOptions options) => FetchExternalTokenAsync())
.WithCacheOptions(CacheOptions.EnableSharedCacheOptions); // for more cache options see https://learn.microsoft.com/entra/msal/dotnet/how-to/token-cache-serialization?tabs=msal

if (customHttpClient != null)
{
clientBuilder.WithHttpClientFactory(new ConstantHttpClientFactory(customHttpClient));
}

return clientBuilder.Build();
}

private async Task<string> FetchExternalTokenAsync()
{
return await _managedIdentityClientAssertion.GetSignedAssertionAsync(default).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Rest;

namespace Microsoft.Bot.Connector.Authentication
{
/// <summary>
/// A Federated Credentials implementation of the <see cref="ServiceClientCredentialsFactory"/> interface.
/// </summary>
public class FederatedServiceClientCredentialsFactory : ServiceClientCredentialsFactory
{
private readonly string _appId;
private readonly string _clientId;
private readonly string _tenantId;
private readonly HttpClient _httpClient;
private readonly ILogger _logger;

/// <summary>
/// Initializes a new instance of the <see cref="FederatedServiceClientCredentialsFactory"/> class.
/// </summary>
/// <param name="appId">Microsoft application Id.</param>
/// <param name="clientId">Managed Identity Client Id.</param>
/// <param name="tenantId">The app tenant.</param>
/// <param name="httpClient">A custom httpClient to use.</param>
/// <param name="logger">A logger instance to use.</param>
/// This enables authentication App Registration + Federated Credentials.
public FederatedServiceClientCredentialsFactory(
string appId,
string clientId,
string tenantId = null,
HttpClient httpClient = null,
ILogger logger = null)
: base()
{
if (string.IsNullOrWhiteSpace(appId))
{
throw new ArgumentNullException(nameof(appId));
}

if (string.IsNullOrWhiteSpace(clientId))
{
throw new ArgumentNullException(nameof(clientId));
}

_appId = appId;
_clientId = clientId;
_tenantId = tenantId;
_httpClient = httpClient;
_logger = logger;
}

/// <inheritdoc />
public override Task<bool> IsValidAppIdAsync(string appId, CancellationToken cancellationToken)
{
return Task.FromResult(appId == _appId);
}

/// <inheritdoc />
public override Task<bool> IsAuthenticationDisabledAsync(CancellationToken cancellationToken)
{
// Auth is always enabled for Certificate.
return Task.FromResult(false);
}

/// <inheritdoc />
public override Task<ServiceClientCredentials> CreateCredentialsAsync(
string appId, string audience, string loginEndpoint, bool validateAuthority, CancellationToken cancellationToken)
{
if (appId != _appId)
{
throw new InvalidOperationException("Invalid App ID.");
}

return Task.FromResult<ServiceClientCredentials>(new FederatedAppCredentials(
_appId,
_clientId,
channelAuthTenant: _tenantId,
oAuthScope: audience,
customHttpClient: _httpClient,
logger: _logger));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.63.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.0.1" />
<PackageReference Include="Microsoft.Identity.Web.Certificateless" Version="3.0.1" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.63.0" />
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.24" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Bot.Schema" Condition=" '$(ReleasePackageVersion)' == '' " Version="$(LocalPackageVersion)" />
Expand Down

0 comments on commit bfbbbca

Please sign in to comment.