From bfbbbca26ed64aec62feebd7c88ef38e783ee9b0 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Wed, 21 Aug 2024 13:40:06 -0500 Subject: [PATCH] Federated Credentials (#6838) * Federated Credentials * Corrected duplicate dependencies --------- Co-authored-by: Tracy Boehrer --- .../Authentication/FederatedAppCredentials.cs | 47 ++++++ .../Authentication/FederatedAuthenticator.cs | 134 ++++++++++++++++++ ...ederatedServiceClientCredentialsFactory.cs | 89 ++++++++++++ .../Microsoft.Bot.Connector.csproj | 3 +- 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 libraries/Microsoft.Bot.Connector/Authentication/FederatedAppCredentials.cs create mode 100644 libraries/Microsoft.Bot.Connector/Authentication/FederatedAuthenticator.cs create mode 100644 libraries/Microsoft.Bot.Connector/Authentication/FederatedServiceClientCredentialsFactory.cs diff --git a/libraries/Microsoft.Bot.Connector/Authentication/FederatedAppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/FederatedAppCredentials.cs new file mode 100644 index 0000000000..628ca236b1 --- /dev/null +++ b/libraries/Microsoft.Bot.Connector/Authentication/FederatedAppCredentials.cs @@ -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 +{ + /// + /// Federated Credentials auth implementation. + /// + public class FederatedAppCredentials : AppCredentials + { + private readonly string _clientId; + + /// + /// Initializes a new instance of the class. + /// + /// App ID for the Application. + /// Client ID for the managed identity assigned to the bot. + /// Optional. The token tenant. + /// Optional. The scope for the token. + /// Optional to be used when acquiring tokens. + /// Optional to gather telemetry data while acquiring and managing credentials. + 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; + } + + /// + protected override Lazy BuildIAuthenticator() + { + return new Lazy( + () => new FederatedAuthenticator(MicrosoftAppId, _clientId, OAuthEndpoint, OAuthScope, CustomHttpClient, Logger), + LazyThreadSafetyMode.ExecutionAndPublication); + } + } +} diff --git a/libraries/Microsoft.Bot.Connector/Authentication/FederatedAuthenticator.cs b/libraries/Microsoft.Bot.Connector/Authentication/FederatedAuthenticator.cs new file mode 100644 index 0000000000..a3b87888cb --- /dev/null +++ b/libraries/Microsoft.Bot.Connector/Authentication/FederatedAuthenticator.cs @@ -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 +{ + /// + /// Abstraction to acquire tokens from a Federated Credentials Application. + /// + 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; + + /// + /// Initializes a new instance of the class. + /// + /// App id for the Application. + /// Client id for the managed identity to be used for acquiring tokens. + /// Resource for which to acquire the token. + /// Login endpoint for request. + /// A customized instance of the HttpClient class. + /// The type used to perform logging. + 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); + } + + /// + public async Task 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 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 FetchExternalTokenAsync() + { + return await _managedIdentityClientAssertion.GetSignedAssertionAsync(default).ConfigureAwait(false); + } + } +} diff --git a/libraries/Microsoft.Bot.Connector/Authentication/FederatedServiceClientCredentialsFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/FederatedServiceClientCredentialsFactory.cs new file mode 100644 index 0000000000..66514607c4 --- /dev/null +++ b/libraries/Microsoft.Bot.Connector/Authentication/FederatedServiceClientCredentialsFactory.cs @@ -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 +{ + /// + /// A Federated Credentials implementation of the interface. + /// + public class FederatedServiceClientCredentialsFactory : ServiceClientCredentialsFactory + { + private readonly string _appId; + private readonly string _clientId; + private readonly string _tenantId; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Microsoft application Id. + /// Managed Identity Client Id. + /// The app tenant. + /// A custom httpClient to use. + /// A logger instance to use. + /// 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; + } + + /// + public override Task IsValidAppIdAsync(string appId, CancellationToken cancellationToken) + { + return Task.FromResult(appId == _appId); + } + + /// + public override Task IsAuthenticationDisabledAsync(CancellationToken cancellationToken) + { + // Auth is always enabled for Certificate. + return Task.FromResult(false); + } + + /// + public override Task CreateCredentialsAsync( + string appId, string audience, string loginEndpoint, bool validateAuthority, CancellationToken cancellationToken) + { + if (appId != _appId) + { + throw new InvalidOperationException("Invalid App ID."); + } + + return Task.FromResult(new FederatedAppCredentials( + _appId, + _clientId, + channelAuthTenant: _tenantId, + oAuthScope: audience, + customHttpClient: _httpClient, + logger: _logger)); + } + } +} diff --git a/libraries/Microsoft.Bot.Connector/Microsoft.Bot.Connector.csproj b/libraries/Microsoft.Bot.Connector/Microsoft.Bot.Connector.csproj index 6c1583444a..67627e6767 100644 --- a/libraries/Microsoft.Bot.Connector/Microsoft.Bot.Connector.csproj +++ b/libraries/Microsoft.Bot.Connector/Microsoft.Bot.Connector.csproj @@ -29,8 +29,9 @@ - + +