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());
+ }
}
}