From 17590d3c3f393d66717e0680fdb03fb09b0df409 Mon Sep 17 00:00:00 2001 From: Will Sugarman Date: Thu, 7 Dec 2023 12:01:19 -0800 Subject: [PATCH] Add | Add Workload Identity Support (#2159) --- .../SqlAuthenticationMethod.xml | 4 +++ src/Microsoft.Data.SqlClient.sln | 19 ------------- ...uthenticationProviderManager.NetCoreApp.cs | 2 ++ .../SqlAuthenticationProviderManager.cs | 2 ++ .../Microsoft/Data/SqlClient/SqlConnection.cs | 17 ++++++++++++ .../SqlClient/SqlInternalConnectionTds.cs | 3 +++ .../src/Microsoft/Data/SqlClient/TdsParser.cs | 3 +++ .../SqlAuthenticationProviderManager.cs | 4 +++ .../Microsoft/Data/SqlClient/SqlConnection.cs | 18 +++++++++++++ .../SqlClient/SqlInternalConnectionTds.cs | 6 ++++- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 6 +++++ .../Data/Common/DbConnectionStringCommon.cs | 12 ++++++++- .../ActiveDirectoryAuthenticationProvider.cs | 27 ++++++++++++++++--- .../Data/SqlClient/SqlConnectionString.cs | 6 +++++ .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 8 +++++- .../Microsoft.Data.SqlClient.Tests.csproj | 1 + .../SqlAuthenticationProviderTest.cs | 26 ++++++++++++++++++ .../SqlConnectionBasicTests.cs | 23 ++++++++++++++++ .../SqlConnectionStringBuilderTest.cs | 2 ++ .../ConnectivityTests/AADConnectionTest.cs | 21 +++++++++++++++ 20 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml index a52a2ec41a..d179ce1196 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml @@ -45,5 +45,9 @@ The authentication method uses Active Directory Default. Use this mode to connect to a SQL Database using multiple non-interactive authentication methods tried sequentially to acquire an access token. This method does not fallback to the "Active Directory Interactive" authentication method. 9 + + The authentication method uses Active Directory Workload Identity. Use a federated User Assigned Managed Identity to connect to SQL Database from Azure client environments that have enabled support for Workload Identity. The 'User Id' or 'UID' is required to be set to the "client ID" of the user identity. + 10 + diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index e1022d786e..b09409f828 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -11,9 +11,6 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TDS.Servers", "Microsoft.Data.SqlClient\tests\tools\TDS\TDS.Servers\TDS.Servers.csproj", "{978063D3-FBB5-4E10-8C45-67C90BE1B931}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TDS", "Microsoft.Data.SqlClient\tests\tools\TDS\TDS\TDS.csproj", "{8DC9D1A0-351B-47BC-A90F-B9DA542550E9}" - ProjectSection(ProjectDependencies) = postProject - {FDA6971D-9F57-4DA4-B10A-261C91684CFC} = {FDA6971D-9F57-4DA4-B10A-261C91684CFC} - EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Data.SqlClient.Tests", "Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj", "{D2D1E2D1-B6E0-489F-A36D-1F3047AB87B9}" ProjectSection(ProjectDependencies) = postProject @@ -28,7 +25,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "netfx", "netfx", "{3FDD425C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Address", "Microsoft.Data.SqlClient\tests\ManualTests\SQL\UdtTest\UDTs\Address\Address.csproj", "{D1392B54-998A-4F27-BC17-4CE149117BCC}" ProjectSection(ProjectDependencies) = postProject - {FDA6971D-9F57-4DA4-B10A-261C91684CFC} = {FDA6971D-9F57-4DA4-B10A-261C91684CFC} {37431336-5307-4184-9356-C4B7E47DC714} = {37431336-5307-4184-9356-C4B7E47DC714} {407890AC-9876-4FEF-A6F1-F36A876BAADE} = {407890AC-9876-4FEF-A6F1-F36A876BAADE} EndProjectSection @@ -40,13 +36,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Data.SqlClient.Ma EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Circle", "Microsoft.Data.SqlClient\tests\ManualTests\SQL\UdtTest\UDTs\Circle\Circle.csproj", "{6C88F00F-9597-43AD-9E5F-9B344DA3B16F}" ProjectSection(ProjectDependencies) = postProject - {FDA6971D-9F57-4DA4-B10A-261C91684CFC} = {FDA6971D-9F57-4DA4-B10A-261C91684CFC} {37431336-5307-4184-9356-C4B7E47DC714} = {37431336-5307-4184-9356-C4B7E47DC714} EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shapes", "Microsoft.Data.SqlClient\tests\ManualTests\SQL\UdtTest\UDTs\Shapes\Shapes.csproj", "{B73A7063-37C3-415D-AD53-BB3DA20ABD6E}" ProjectSection(ProjectDependencies) = postProject - {FDA6971D-9F57-4DA4-B10A-261C91684CFC} = {FDA6971D-9F57-4DA4-B10A-261C91684CFC} {37431336-5307-4184-9356-C4B7E47DC714} = {37431336-5307-4184-9356-C4B7E47DC714} EndProjectSection EndProject @@ -384,18 +378,6 @@ Global {9073ABEF-92E0-4702-BB23-2C99CEF9BDD7}.Release|x64.Build.0 = Release|Any CPU {9073ABEF-92E0-4702-BB23-2C99CEF9BDD7}.Release|x86.ActiveCfg = Release|Any CPU {9073ABEF-92E0-4702-BB23-2C99CEF9BDD7}.Release|x86.Build.0 = Release|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Debug|x64.ActiveCfg = Debug|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Debug|x64.Build.0 = Debug|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Debug|x86.ActiveCfg = Debug|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Debug|x86.Build.0 = Debug|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Release|Any CPU.Build.0 = Release|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Release|x64.ActiveCfg = Release|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Release|x64.Build.0 = Release|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Release|x86.ActiveCfg = Release|Any CPU - {FDA6971D-9F57-4DA4-B10A-261C91684CFC}.Release|x86.Build.0 = Release|Any CPU {F5DF2FDC-C860-4CB3-8B24-7C903C6FC076}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F5DF2FDC-C860-4CB3-8B24-7C903C6FC076}.Debug|x64.ActiveCfg = Debug|Any CPU {F5DF2FDC-C860-4CB3-8B24-7C903C6FC076}.Debug|x64.Build.0 = Debug|Any CPU @@ -511,7 +493,6 @@ Global {771F3F1E-7A68-4A9D-ADA8-A24F1D5BE71D} = {3FDD425C-FE01-4B56-863E-1FCDD0677CF5} {412BCCC8-19F6-489A-B594-E9A506816155} = {771F3F1E-7A68-4A9D-ADA8-A24F1D5BE71D} {9073ABEF-92E0-4702-BB23-2C99CEF9BDD7} = {C9726AED-D6A3-4AAC-BA04-92DD1F079594} - {FDA6971D-9F57-4DA4-B10A-261C91684CFC} = {0CC4817A-12F3-4357-912C-09315FAAD008} {71F356DC-DFA3-4163-8BFE-D268722CE189} = {ED952CF7-84DF-437A-B066-F516E9BE1C2C} {908C7DD3-C999-40A6-9433-9F5ACA7C36F5} = {71F356DC-DFA3-4163-8BFE-D268722CE189} {0CE216CE-8072-4985-B248-61F0D0BE9C2E} = {71F356DC-DFA3-4163-8BFE-D268722CE189} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs index 094114f357..dd57c60da8 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs @@ -153,6 +153,8 @@ private static SqlAuthenticationMethod AuthenticationEnumFromString(string authe return SqlAuthenticationMethod.ActiveDirectoryMSI; case ActiveDirectoryDefault: return SqlAuthenticationMethod.ActiveDirectoryDefault; + case ActiveDirectoryWorkloadIdentity: + return SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; default: throw SQL.UnsupportedAuthentication(authentication); } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index 401fc23466..0cb7a9b54c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -20,6 +20,7 @@ internal partial class SqlAuthenticationProviderManager private const string ActiveDirectoryManagedIdentity = "active directory managed identity"; private const string ActiveDirectoryMSI = "active directory msi"; private const string ActiveDirectoryDefault = "active directory default"; + private const string ActiveDirectoryWorkloadIdentity = "active directory workload identity"; private readonly IReadOnlyCollection _authenticationsWithAppSpecifiedProvider; private readonly ConcurrentDictionary _providers; @@ -45,6 +46,7 @@ private static void SetDefaultAuthProviders(SqlAuthenticationProviderManager ins instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, activeDirectoryAuthProvider); instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, activeDirectoryAuthProvider); instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDefault, activeDirectoryAuthProvider); + instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, activeDirectoryAuthProvider); } } /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs index 0a342add69..d9812157a5 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -202,6 +202,10 @@ public SqlConnection(string connectionString, SqlCredential credential) : this() { throw SQL.SettingCredentialWithNonInteractiveArgument(DbConnectionStringBuilderUtil.ActiveDirectoryDefaultString); } + else if (UsesActiveDirectoryWorkloadIdentity(connectionOptions)) + { + throw SQL.SettingCredentialWithNonInteractiveArgument(DbConnectionStringBuilderUtil.ActiveDirectoryWorkloadIdentityString); + } Credential = credential; } @@ -530,6 +534,11 @@ private bool UsesActiveDirectoryDefault(SqlConnectionString opt) return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault; } + private bool UsesActiveDirectoryWorkloadIdentity(SqlConnectionString opt) + { + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; + } + private bool UsesAuthentication(SqlConnectionString opt) { return opt != null && opt.Authentication != SqlAuthenticationMethod.NotSpecified; @@ -619,6 +628,10 @@ public override string ConnectionString { throw SQL.SettingNonInteractiveWithCredential(DbConnectionStringBuilderUtil.ActiveDirectoryDefaultString); } + else if (UsesActiveDirectoryWorkloadIdentity(connectionOptions)) + { + throw SQL.SettingNonInteractiveWithCredential(DbConnectionStringBuilderUtil.ActiveDirectoryWorkloadIdentityString); + } CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions); } @@ -999,6 +1012,10 @@ public SqlCredential Credential { throw SQL.SettingCredentialWithNonInteractiveInvalid(DbConnectionStringBuilderUtil.ActiveDirectoryDefaultString); } + else if (UsesActiveDirectoryWorkloadIdentity(connectionOptions)) + { + throw SQL.SettingCredentialWithNonInteractiveInvalid(DbConnectionStringBuilderUtil.ActiveDirectoryWorkloadIdentityString); + } CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 6a0ee2e0e0..f5195d497a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1330,6 +1330,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity // Since AD Integrated may be acting like Windows integrated, additionally check _fedAuthRequired || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired) || _accessTokenCallback != null) @@ -2159,6 +2160,7 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired), "Credentials aren't provided for calling MSAL"); Debug.Assert(fedAuthInfo != null, "info should not be null."); @@ -2406,6 +2408,7 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: case SqlAuthenticationMethod.ActiveDirectoryMSI: case SqlAuthenticationMethod.ActiveDirectoryDefault: + case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) { _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index e111a8e789..d87c9c402f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -7973,6 +7973,9 @@ internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionD case SqlAuthenticationMethod.ActiveDirectoryDefault: workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYDEFAULT; break; + case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: + workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYWORKLOADIDENTITY; + break; default: if (_connHandler._accessTokenCallback != null) { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index d0757807be..bc654f39f7 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -22,6 +22,7 @@ internal class SqlAuthenticationProviderManager private const string ActiveDirectoryManagedIdentity = "active directory managed identity"; private const string ActiveDirectoryMSI = "active directory msi"; private const string ActiveDirectoryDefault = "active directory default"; + private const string ActiveDirectoryWorkloadIdentity = "active directory workload identity"; static SqlAuthenticationProviderManager() { @@ -52,6 +53,7 @@ static SqlAuthenticationProviderManager() Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDefault, activeDirectoryAuthProvider); + Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, activeDirectoryAuthProvider); } public static readonly SqlAuthenticationProviderManager Instance; @@ -231,6 +233,8 @@ private static SqlAuthenticationMethod AuthenticationEnumFromString(string authe return SqlAuthenticationMethod.ActiveDirectoryMSI; case ActiveDirectoryDefault: return SqlAuthenticationMethod.ActiveDirectoryDefault; + case ActiveDirectoryWorkloadIdentity: + return SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; default: throw SQL.UnsupportedAuthentication(authentication); } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs index a329d626b0..b6104b9075 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -405,6 +405,11 @@ public SqlConnection(string connectionString, SqlCredential credential) : this() throw SQL.SettingCredentialWithNonInteractiveArgument(DbConnectionStringBuilderUtil.ActiveDirectoryDefaultString); } + if (UsesActiveDirectoryWorkloadIdentity(connectionOptions)) + { + throw SQL.SettingCredentialWithNonInteractiveArgument(DbConnectionStringBuilderUtil.ActiveDirectoryWorkloadIdentityString); + } + Credential = credential; } // else @@ -627,6 +632,11 @@ private bool UsesActiveDirectoryDefault(SqlConnectionString opt) return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault; } + private bool UsesActiveDirectoryWorkloadIdentity(SqlConnectionString opt) + { + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; + } + private bool UsesAuthentication(SqlConnectionString opt) { return opt != null && opt.Authentication != SqlAuthenticationMethod.NotSpecified; @@ -834,6 +844,10 @@ override public string ConnectionString { throw SQL.SettingNonInteractiveWithCredential(DbConnectionStringBuilderUtil.ActiveDirectoryDefaultString); } + else if (UsesActiveDirectoryWorkloadIdentity(connectionOptions)) + { + throw SQL.SettingNonInteractiveWithCredential(DbConnectionStringBuilderUtil.ActiveDirectoryWorkloadIdentityString); + } CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions); } @@ -1183,6 +1197,10 @@ public SqlCredential Credential { throw SQL.SettingCredentialWithNonInteractiveInvalid(DbConnectionStringBuilderUtil.ActiveDirectoryDefaultString); } + else if (UsesActiveDirectoryWorkloadIdentity(connectionOptions)) + { + throw SQL.SettingCredentialWithNonInteractiveInvalid(DbConnectionStringBuilderUtil.ActiveDirectoryWorkloadIdentityString); + } CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 068b37dc71..3d4d79ceae 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1595,6 +1595,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity // Since AD Integrated may be acting like Windows integrated, additionally check _fedAuthRequired || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired) || _accessTokenCallback != null) @@ -1991,7 +1992,8 @@ private bool ShouldDisableTnir(SqlConnectionString connectionOptions) connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI || - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault; + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault || + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; // Check if the user had explicitly specified the TNIR option in the connection string or the connection string builder. // If the user has specified the option in the connection string explicitly, then we shouldn't disable TNIR. @@ -2585,6 +2587,7 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired), "Credentials aren't provided for calling MSAL"); @@ -2820,6 +2823,7 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: case SqlAuthenticationMethod.ActiveDirectoryMSI: case SqlAuthenticationMethod.ActiveDirectoryDefault: + case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) { _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 9da85106e2..b5660ba694 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -594,6 +594,9 @@ internal void Connect(ServerInfo serverInfo, case SqlAuthenticationMethod.ActiveDirectoryDefault: SqlClientEventSource.Log.TryTraceEvent(" Active Directory Default authentication"); break; + case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: + SqlClientEventSource.Log.TryTraceEvent(" Active Directory Workload Identity authentication"); + break; case SqlAuthenticationMethod.SqlPassword: SqlClientEventSource.Log.TryTraceEvent(" SQL Password authentication"); break; @@ -8763,6 +8766,9 @@ internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionD case SqlAuthenticationMethod.ActiveDirectoryDefault: workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYDEFAULT; break; + case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: + workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYWORKLOADIDENTITY; + break; default: if (_connHandler._accessTokenCallback != null) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/DbConnectionStringCommon.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/DbConnectionStringCommon.cs index 6bef73963b..a24e139968 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/DbConnectionStringCommon.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/DbConnectionStringCommon.cs @@ -347,6 +347,7 @@ internal static ApplicationIntent ConvertToApplicationIntent(string keyword, obj internal const string ActiveDirectoryManagedIdentityString = "Active Directory Managed Identity"; internal const string ActiveDirectoryMSIString = "Active Directory MSI"; internal const string ActiveDirectoryDefaultString = "Active Directory Default"; + internal const string ActiveDirectoryWorkloadIdentityString = "Active Directory Workload Identity"; const string SqlCertificateString = "Sql Certificate"; #if DEBUG @@ -361,7 +362,8 @@ internal static ApplicationIntent ConvertToApplicationIntent(string keyword, obj "ActiveDirectoryDeviceCodeFlow", "ActiveDirectoryManagedIdentity", "ActiveDirectoryMSI", - "ActiveDirectoryDefault" + "ActiveDirectoryDefault", + "ActiveDirectoryWorkloadIdentity", }; private static bool IsValidAuthenticationMethodEnum() @@ -451,6 +453,12 @@ internal static bool TryConvertToAuthenticationType(string value, out SqlAuthent isSuccess = true; } #endif + else if (StringComparer.InvariantCultureIgnoreCase.Equals(value, ActiveDirectoryWorkloadIdentityString) + || StringComparer.InvariantCultureIgnoreCase.Equals(value, Convert.ToString(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, CultureInfo.InvariantCulture))) + { + result = SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; + isSuccess = true; + } else { result = DbConnectionStringDefaults.Authentication; @@ -529,6 +537,7 @@ internal static bool IsValidAuthenticationTypeValue(SqlAuthenticationMethod valu #if ADONET_CERT_AUTH && NETFRAMEWORK || value == SqlAuthenticationMethod.SqlCertificate #endif + || value == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity || value == SqlAuthenticationMethod.NotSpecified; } @@ -550,6 +559,7 @@ internal static string AuthenticationTypeToString(SqlAuthenticationMethod value) #if ADONET_CERT_AUTH && NETFRAMEWORK SqlAuthenticationMethod.SqlCertificate => SqlCertificateString, #endif + SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity => ActiveDirectoryWorkloadIdentityString, _ => null }; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs index 60151f1be1..01f265cf50 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs @@ -84,7 +84,8 @@ public override bool IsSupported(SqlAuthenticationMethod authentication) || authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || authentication == SqlAuthenticationMethod.ActiveDirectoryMSI - || authentication == SqlAuthenticationMethod.ActiveDirectoryDefault; + || authentication == SqlAuthenticationMethod.ActiveDirectoryDefault + || authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; } /// @@ -117,7 +118,7 @@ public override void BeforeUnload(SqlAuthenticationMethod authentication) public override async Task AcquireTokenAsync(SqlAuthenticationParameters parameters) { - CancellationTokenSource cts = new CancellationTokenSource(); + using CancellationTokenSource cts = new(); // Use Connection timeout value to cancel token acquire request after certain period of time. cts.CancelAfter(parameters.ConnectionTimeout * 1000); // Convert to milliseconds @@ -162,6 +163,7 @@ public override async Task AcquireTokenAsync(SqlAuthenti { defaultAzureCredentialOptions.ManagedIdentityClientId = clientId; defaultAzureCredentialOptions.SharedTokenCacheUsername = clientId; + defaultAzureCredentialOptions.WorkloadIdentityClientId = clientId; } AccessToken accessToken = await new DefaultAzureCredential(defaultAzureCredentialOptions).GetTokenAsync(tokenRequestContext, cts.Token).ConfigureAwait(false); SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Default auth mode. Expiry Time: {0}", accessToken.ExpiresOn); @@ -177,7 +179,6 @@ public override async Task AcquireTokenAsync(SqlAuthenti return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); } - AuthenticationResult result = null; if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal) { AccessToken accessToken = await new ClientSecretCredential(audience, parameters.UserId, parameters.Password, tokenCredentialOptions).GetTokenAsync(tokenRequestContext, cts.Token).ConfigureAwait(false); @@ -185,6 +186,25 @@ public override async Task AcquireTokenAsync(SqlAuthenti return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); } + if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity) + { + // The WorkloadIdentityCredentialOptions object initialization populates its instance members + // from the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, + // and AZURE_ADDITIONALLY_ALLOWED_TENANTS. AZURE_CLIENT_ID may be overridden by the User Id. + WorkloadIdentityCredentialOptions options = new() { AuthorityHost = new Uri(authority) }; + + if (clientId is not null) + { + options.ClientId = clientId; + } + + // If either tenant id, client id, or the token file path are not specified when fetching the token, + // a CredentialUnavailableException will be thrown instead + AccessToken accessToken = await new WorkloadIdentityCredential(options).GetTokenAsync(tokenRequestContext, cts.Token).ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Workload Identity auth mode. Expiry Time: {0}", accessToken.ExpiresOn); + return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); + } + /* * Today, MSAL.NET uses another redirect URI by default in desktop applications that run on Windows * (urn:ietf:wg:oauth:2.0:oob). In the future, we'll want to change this default, so we recommend @@ -209,6 +229,7 @@ public override async Task AcquireTokenAsync(SqlAuthenti #endif ); + AuthenticationResult result = null; IPublicClientApplication app = GetPublicClientAppInstance(pcaKey); if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryIntegrated) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionString.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionString.cs index 65d8dcc9cf..690a4e810a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionString.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionString.cs @@ -626,6 +626,12 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G { throw SQL.NonInteractiveWithPassword(DbConnectionStringBuilderUtil.ActiveDirectoryDefaultString); } + + if (Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity && _hasPasswordKeyword) + { + throw SQL.NonInteractiveWithPassword(DbConnectionStringBuilderUtil.ActiveDirectoryWorkloadIdentityString); + } + #if ADONET_CERT_AUTH && NETFRAMEWORK if (!DbConnectionStringBuilderUtil.IsValidCertificateValue(_certificate)) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index 2e727be277..c483d8a0f8 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -280,6 +280,7 @@ public enum FedAuthLibrary : byte public const byte MSALWORKFLOW_ACTIVEDIRECTORYMANAGEDIDENTITY = 0x03; // Using the Interactive byte as that's supported for Identity based authentication public const byte MSALWORKFLOW_ACTIVEDIRECTORYDEFAULT = 0x03; // Using the Interactive byte as that is the closest we have to non-password based authentication modes public const byte MSALWORKFLOW_ACTIVEDIRECTORYTOKENCREDENTIAL = 0x03; // Using the Interactive byte as that is the closest we have to non-password based authentication modes + public const byte MSALWORKFLOW_ACTIVEDIRECTORYWORKLOADIDENTITY = 0x03; // Using the Interactive byte as that's supported for Identity based authentication public enum ActiveDirectoryWorkflow : byte { @@ -290,6 +291,7 @@ public enum ActiveDirectoryWorkflow : byte DeviceCodeFlow = MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW, ManagedIdentity = MSALWORKFLOW_ACTIVEDIRECTORYMANAGEDIDENTITY, Default = MSALWORKFLOW_ACTIVEDIRECTORYDEFAULT, + WorkloadIdentity = MSALWORKFLOW_ACTIVEDIRECTORYWORKLOADIDENTITY, } // The string used for username in the error message when Authentication = Active Directory Integrated with FedAuth is used, if authentication fails. @@ -1212,8 +1214,12 @@ public enum SqlAuthenticationMethod /// ActiveDirectoryDefault, + + /// + ActiveDirectoryWorkloadIdentity, + #if ADONET_CERT_AUTH && NETFRAMEWORK - SqlCertificate + SqlCertificate, #endif } // This enum indicates the state of TransparentNetworkIPResolution diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj index 78d93ab108..a771a6a9a2 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj @@ -31,6 +31,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs new file mode 100644 index 0000000000..d41f4b40d1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit; + +namespace Microsoft.Data.SqlClient.Tests +{ + public class SqlAuthenticationProviderTest + { + [Theory] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryIntegrated)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryPassword)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryInteractive)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryMSI)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryDefault)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity)] + public void DefaultAuthenticationProviders(SqlAuthenticationMethod method) + { + Assert.IsType(SqlAuthenticationProvider.GetProvider(method)); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs index 70c94eb928..36e1d48697 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs @@ -241,6 +241,29 @@ public void ConnectionTestInvalidCredentialCombination() } } + [Theory] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryIntegrated)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryInteractive)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryMSI)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryDefault)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity)] + public void ConnectionTestInvalidCredentialAndAuthentication(SqlAuthenticationMethod authentication) + { + var connectionString = $"Authentication={authentication}"; + + using var testPassword = new SecureString(); + testPassword.MakeReadOnly(); + var credential = new SqlCredential(string.Empty, testPassword); + + Assert.Throws(() => new SqlConnection(connectionString, credential)); + + // Attempt to set the credential after creation + using var connection = new SqlConnection(connectionString); + Assert.Throws(() => connection.Credential = credential); + } + [Fact] public void ConnectionTestValidCredentialCombination() { diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs index b3a090f58f..d1c3fd2d34 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs @@ -39,6 +39,8 @@ public partial class SqlConnectionStringBuilderTest [InlineData("Authentication = ActiveDirectoryMSI ")] [InlineData("Authentication = Active Directory Default ")] [InlineData("Authentication = ActiveDirectoryDefault ")] + [InlineData("Authentication = Active Directory Workload Identity ")] + [InlineData("Authentication = ActiveDirectoryWorkloadIdentity ")] [InlineData("Command Timeout = 5")] [InlineData("Command Timeout = 15")] [InlineData("Command Timeout = 0")] diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs index 5d7cae1f1e..7ef0ed4bcc 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs @@ -453,6 +453,27 @@ public static void ActiveDirectoryManagedIdentityWithCredentialsMustFail() Assert.Contains(expectedMessage, e.Message); } + [ConditionalFact(nameof(IsAADConnStringsSetup))] + public static void ActiveDirectoryWorkloadIdentityWithCredentialsMustFail() + { + // connection fails with expected error message. + string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStrWithNoCred = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, credKeys) + + "Authentication=Active Directory Workload Identity;"; + + SecureString str = new SecureString(); + foreach (char c in "hello") + { + str.AppendChar(c); + } + str.MakeReadOnly(); + SqlCredential credential = new SqlCredential("someuser", str); + InvalidOperationException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred, credential)); + + string expectedMessage = "Cannot set the Credential property if 'Authentication=Active Directory Workload Identity' has been specified in the connection string."; + Assert.Contains(expectedMessage, e.Message); + } + [ConditionalFact(nameof(IsAADConnStringsSetup), nameof(IsManagedIdentitySetup))] public static void ActiveDirectoryManagedIdentityWithPasswordMustFail() {