diff --git a/src/libraries/Authentication/Authentication.Msal/Interfaces/IMSALConnectionSettings.cs b/src/libraries/Authentication/Authentication.Msal/Interfaces/IMSALConnectionSettings.cs index cee4d3b4..c9254146 100644 --- a/src/libraries/Authentication/Authentication.Msal/Interfaces/IMSALConnectionSettings.cs +++ b/src/libraries/Authentication/Authentication.Msal/Interfaces/IMSALConnectionSettings.cs @@ -9,16 +9,44 @@ public interface IMSALConnectionSettings : IConnectionSettings { public string ClientSecret { get; set; } + /// + /// Auth Type to use for the connection + /// AuthTypes AuthType { get; set; } + /// + /// Certificate thumbprint to use for the connection when using a certificate that is resident on the machine + /// string CertificateThumbPrint { get; set; } + /// + /// Subject name to search a cert for. + /// string CertificateSubjectName { get; set; } + /// + /// Cert store name to use. + /// string CertificateStoreName { get; set; } + /// + /// Only use valid certs. Defaults to true. + /// public bool ValidCertificateOnly { get; set; } + /// + /// Use x5c for certs. Defaults to false. + /// public bool SendX5C { get; set; } + + /// + /// ClientId of the ManagedIdentity used with FederatedCredentials + /// + public string FederatedClientId { get; set; } + + /// + /// Token path used for the workload identity, like the MSAL example for AKS, equal to AZURE_FEDERATED_TOKEN_FILE. + /// + public string FederatedTokenFile { get; set; } } } \ No newline at end of file diff --git a/src/libraries/Authentication/Authentication.Msal/Model/AuthTypes.cs b/src/libraries/Authentication/Authentication.Msal/Model/AuthTypes.cs index ad0d9aa9..bd41effc 100644 --- a/src/libraries/Authentication/Authentication.Msal/Model/AuthTypes.cs +++ b/src/libraries/Authentication/Authentication.Msal/Model/AuthTypes.cs @@ -10,6 +10,7 @@ public enum AuthTypes ClientSecret, UserManagedIdentity, SystemManagedIdentity, - FederatedCredentials + FederatedCredentials, + WorkloadIdentity } } diff --git a/src/libraries/Authentication/Authentication.Msal/Model/ConnectionSettings.cs b/src/libraries/Authentication/Authentication.Msal/Model/ConnectionSettings.cs index bcff5161..5f016291 100644 --- a/src/libraries/Authentication/Authentication.Msal/Model/ConnectionSettings.cs +++ b/src/libraries/Authentication/Authentication.Msal/Model/ConnectionSettings.cs @@ -4,6 +4,7 @@ using Microsoft.Agents.Authentication.Msal.Interfaces; using Microsoft.Agents.Core; using Microsoft.Extensions.Configuration; +using Microsoft.Identity.Client; using System; namespace Microsoft.Agents.Authentication.Msal.Model @@ -29,51 +30,42 @@ public ConnectionSettings(IConfigurationSection msalConfigurationSection) : base ClientSecret = msalConfigurationSection.GetValue("ClientSecret", string.Empty); AuthType = msalConfigurationSection.GetValue("AuthType", AuthTypes.ClientSecret); FederatedClientId = msalConfigurationSection.GetValue("FederatedClientId", string.Empty); + FederatedTokenFile = msalConfigurationSection.GetValue("FederatedTokenFile", string.Empty); + AssertionRequestOptions = msalConfigurationSection.GetSection("AssertionRequestOptions").Get(); } ValidateConfiguration(); } - /// - /// Auth Type to use for the connection - /// + /// public AuthTypes AuthType { get; set; } = AuthTypes.ClientSecret; - /// - /// Certificate thumbprint to use for the connection when using a certificate that is resident on the machine - /// + /// public string CertificateThumbPrint { get; set; } - /// - /// Client Secret to use for the connection when using a client secret - /// + /// public string ClientSecret { get; set; } - /// - /// Subject name to search a cert for. - /// + /// public string CertificateSubjectName { get; set; } - /// - /// Cert store name to use. - /// + /// public string CertificateStoreName { get; set; } - /// - /// Only use valid certs. Defaults to true. - /// + /// public bool ValidCertificateOnly { get; set; } = true; - /// - /// Use x5c for certs. Defaults to false. - /// + /// public bool SendX5C { get; set; } = false; - /// - /// ClientId of the ManagedIdentity used with FederatedCredentials - /// + /// public string FederatedClientId { get; set; } + /// + public string FederatedTokenFile { get; set; } + + public AssertionRequestOptions AssertionRequestOptions { get; set; } + /// /// Validates required properties are present in the configuration for the requested authentication type. /// @@ -147,6 +139,20 @@ public void ValidateConfiguration() throw new ArgumentNullException(nameof(Authority), "TenantId or Authority is required"); } break; + case AuthTypes.WorkloadIdentity: + if (string.IsNullOrEmpty(ClientId)) + { + throw new ArgumentNullException(nameof(ClientId), "ClientId is required"); + } + if (string.IsNullOrEmpty(Authority) && string.IsNullOrEmpty(TenantId)) + { + throw new ArgumentNullException(nameof(Authority), "TenantId or Authority is required"); + } + if (AuthType == AuthTypes.WorkloadIdentity && string.IsNullOrEmpty(FederatedTokenFile)) + { + throw new ArgumentNullException(nameof(FederatedTokenFile), "FederatedTokenFile is required"); + } + break; default: break; } diff --git a/src/libraries/Authentication/Authentication.Msal/MsalAuth.cs b/src/libraries/Authentication/Authentication.Msal/MsalAuth.cs index e724869b..e6a41011 100644 --- a/src/libraries/Authentication/Authentication.Msal/MsalAuth.cs +++ b/src/libraries/Authentication/Authentication.Msal/MsalAuth.cs @@ -37,6 +37,7 @@ public class MsalAuth : IAccessTokenProvider, IOBOExchange, IMSALProvider private readonly ConnectionSettings _connectionSettings; private readonly ILogger _logger; private readonly ICertificateProvider _certificateProvider; + private ClientAssertionProviderBase _clientAssertion; /// /// Creates a MSAL Authentication Instance. @@ -229,12 +230,17 @@ private object InnerCreateClientApplication() } else if (_connectionSettings.AuthType == AuthTypes.FederatedCredentials) { - async Task FetchExternalTokenAsync() - { - var managedIdentityClientAssertion = new ManagedIdentityClientAssertion(_connectionSettings.FederatedClientId); - return await managedIdentityClientAssertion.GetSignedAssertionAsync(default).ConfigureAwait(false); - } - cAppBuilder.WithClientAssertion((AssertionRequestOptions options) => FetchExternalTokenAsync()); + // Reuse this instance so that the assertion is cached and only refreshed once it expires. + _clientAssertion = new ManagedIdentityClientAssertion(_connectionSettings.FederatedClientId, null, _logger); + + cAppBuilder.WithClientAssertion(async (AssertionRequestOptions options) => await _clientAssertion.GetSignedAssertionAsync(_connectionSettings.AssertionRequestOptions)); + } + else if (_connectionSettings.AuthType == AuthTypes.WorkloadIdentity) + { + // Reuse this instance so that the assertion is cached and only refreshed once it expires. + _clientAssertion = new AzureIdentityForKubernetesClientAssertion(_connectionSettings.FederatedTokenFile, _logger); + + cAppBuilder.WithClientAssertion(async (AssertionRequestOptions options) => await _clientAssertion.GetSignedAssertionAsync(_connectionSettings.AssertionRequestOptions)); } else { diff --git a/src/tests/Microsoft.Agents.Authentication.Msal.Tests/Model/ConnectionSettingsTests.cs b/src/tests/Microsoft.Agents.Authentication.Msal.Tests/Model/ConnectionSettingsTests.cs index 12d15a50..04bae0fb 100644 --- a/src/tests/Microsoft.Agents.Authentication.Msal.Tests/Model/ConnectionSettingsTests.cs +++ b/src/tests/Microsoft.Agents.Authentication.Msal.Tests/Model/ConnectionSettingsTests.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; +using System.Linq; using Xunit; namespace Microsoft.Agents.Authentication.Msal.Tests.Model @@ -170,5 +171,127 @@ public void ValidateConfiguration_ShouldThrowOnNullClientIdForUserManagedIdentit Assert.Throws(() => new ConnectionSettings(configuration.GetSection(SettingsSection))); } + + [Fact] + public void ValidateConfiguration_FederatedCredentials() + { + // Start with good + var configSettings = new Dictionary { + { "Connections:Settings:AuthType", "FederatedCredentials" }, + { "Connections:Settings:ClientId", "test-client-id" }, + { "Connections:Settings:AuthorityEndpoint", "https://botframework/test.com" }, + { "Connections:Settings:TenantId", "test-tenant-id" }, + { "Connections:Settings:FederatedClientId", "test-federated-client-id" } + }; + + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configSettings) + .Build(); + + var settings = new ConnectionSettings(configuration.GetSection(SettingsSection)); + + Assert.Equal(AuthTypes.FederatedCredentials, settings.AuthType); + Assert.Equal("test-client-id", settings.ClientId); + Assert.Equal("test-tenant-id", settings.TenantId); + Assert.Equal("https://botframework/test.com", settings.Authority); + Assert.Equal("test-federated-client-id", settings.FederatedClientId); + } + + [Fact] + public void ValidateConfiguration_ShouldThrowOnNullFederatedClientId() + { + // Start with good + var configSettings = new Dictionary { + { "Connections:Settings:AuthType", "FederatedCredentials" }, + { "Connections:Settings:ClientId", "test-client-id" }, + { "Connections:Settings:AuthorityEndpoint", "https://botframework/test.com" }, + { "Connections:Settings:TenantId", "test-tenant-id" }, + }; + + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configSettings) + .Build(); + + Assert.Throws(() => new ConnectionSettings(configuration.GetSection(SettingsSection))); + } + + [Fact] + public void ValidateConfiguration_WorkloadIdentity() + { + // Start with good + var configSettings = new Dictionary { + { "Connections:Settings:AuthType", "WorkloadIdentity" }, + { "Connections:Settings:ClientId", "test-client-id" }, + { "Connections:Settings:AuthorityEndpoint", "https://botframework/test.com" }, + { "Connections:Settings:TenantId", "test-tenant-id" }, + { "Connections:Settings:FederatedTokenFile", "test-token-file" } + }; + + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configSettings) + .Build(); + + var settings = new ConnectionSettings(configuration.GetSection(SettingsSection)); + + Assert.Equal(AuthTypes.WorkloadIdentity, settings.AuthType); + Assert.Equal("test-client-id", settings.ClientId); + Assert.Equal("test-tenant-id", settings.TenantId); + Assert.Equal("https://botframework/test.com", settings.Authority); + Assert.Equal("test-token-file", settings.FederatedTokenFile); + Assert.Null(settings.AssertionRequestOptions); + } + + [Fact] + public void ValidateConfiguration_ShouldThrowOnNullFederatedTokenFile() + { + // Start with good + var configSettings = new Dictionary { + { "Connections:Settings:AuthType", "WorkloadIdentity" }, + { "Connections:Settings:ClientId", "test-client-id" }, + { "Connections:Settings:AuthorityEndpoint", "https://botframework/test.com" }, + { "Connections:Settings:TenantId", "test-tenant-id" }, + }; + + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configSettings) + .Build(); + + Assert.Throws(() => new ConnectionSettings(configuration.GetSection(SettingsSection))); + } + + [Fact] + public void ValidateConfiguration_AssertionRequestOptions() + { + // Start with good + var configSettings = new Dictionary { + { "Connections:Settings:AuthType", "WorkloadIdentity" }, + { "Connections:Settings:ClientId", "test-client-id" }, + { "Connections:Settings:AuthorityEndpoint", "https://botframework/test.com" }, + { "Connections:Settings:TenantId", "test-tenant-id" }, + { "Connections:Settings:FederatedTokenFile", "test-token-file" }, + { "Connections:Settings:AssertionRequestOptions:ClientId", "option-client-id" }, + { "Connections:Settings:AssertionRequestOptions:TokenEndpoint", "option-token-endpoint" }, + { "Connections:Settings:AssertionRequestOptions:Claims", "option-claims" }, + { "Connections:Settings:AssertionRequestOptions:ClientCapabilities:0", "option-cap1" }, + { "Connections:Settings:AssertionRequestOptions:ClientCapabilities:1", "option-cap2" }, + }; + + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configSettings) + .Build(); + + var settings = new ConnectionSettings(configuration.GetSection(SettingsSection)); + + Assert.Equal(AuthTypes.WorkloadIdentity, settings.AuthType); + Assert.Equal("test-client-id", settings.ClientId); + Assert.Equal("test-tenant-id", settings.TenantId); + Assert.Equal("https://botframework/test.com", settings.Authority); + Assert.Equal("test-token-file", settings.FederatedTokenFile); + Assert.NotNull(settings.AssertionRequestOptions); + Assert.Equal("option-client-id", settings.AssertionRequestOptions.ClientID); + Assert.Equal("option-token-endpoint", settings.AssertionRequestOptions.TokenEndpoint); + Assert.Equal("option-claims", settings.AssertionRequestOptions.Claims); + Assert.Equal(2, settings.AssertionRequestOptions.ClientCapabilities.Count()); + } } }