-
Notifications
You must be signed in to change notification settings - Fork 486
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Federated Credentials * Corrected duplicate dependencies --------- Co-authored-by: Tracy Boehrer <[email protected]>
- Loading branch information
1 parent
b11f048
commit bfbbbca
Showing
4 changed files
with
272 additions
and
1 deletion.
There are no files selected for viewing
47 changes: 47 additions & 0 deletions
47
libraries/Microsoft.Bot.Connector/Authentication/FederatedAppCredentials.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
134 changes: 134 additions & 0 deletions
134
libraries/Microsoft.Bot.Connector/Authentication/FederatedAuthenticator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
89 changes: 89 additions & 0 deletions
89
libraries/Microsoft.Bot.Connector/Authentication/FederatedServiceClientCredentialsFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters