From 7a1f3ff62029ebfe30c5336edf26b7a2b73353e7 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Mon, 12 May 2025 17:36:36 -0700 Subject: [PATCH 1/5] Expose SSPI context provider as public Adds a property to SqlConnection to allow setting a provider Plumbs that property into the TdsParser so that it can be used if set Fixes #2253 --- .../SqlConnection.xml | 5 +++ .../SspiAuthenticationParameters.xml | 28 ++++++++++++++++ .../SspiContextProvider.xml | 20 +++++++++++ src/Microsoft.Data.SqlClient.sln | 19 +++++------ .../netcore/ref/Microsoft.Data.SqlClient.cs | 33 +++++++++++++++++++ .../Microsoft/Data/SqlClient/SqlConnection.cs | 26 +++++++++++---- .../Data/SqlClient/SqlConnectionFactory.cs | 4 +-- .../SqlClient/SqlInternalConnectionTds.cs | 6 ++-- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 2 +- .../netfx/ref/Microsoft.Data.SqlClient.cs | 33 +++++++++++++++++++ .../Microsoft/Data/SqlClient/SqlConnection.cs | 27 +++++++++++---- .../SqlClient/SqlInternalConnectionTds.cs | 5 ++- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 2 +- .../ConnectionPool/SqlConnectionPoolKey.cs | 21 ++++++++++-- .../SSPI/NativeSspiContextProvider.cs | 4 +-- .../SSPI/NegotiateSspiContextProvider.cs | 4 +-- .../SSPI/SspiAuthenticationParameters.cs | 9 ++++- .../SqlClient/SSPI/SspiContextProvider.cs | 23 ++++++++----- 18 files changed, 225 insertions(+), 46 deletions(-) create mode 100644 doc/snippets/Microsoft.Data.SqlClient/SspiAuthenticationParameters.xml create mode 100644 doc/snippets/Microsoft.Data.SqlClient/SspiContextProvider.xml diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml index 582afe1240..57b205f2a2 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml @@ -2086,6 +2086,11 @@ The following sample tries to open a connection to an invalid database to simula Returns 0 if the connection is inactive on the client side. + + + Gets or sets the instance for customizing the SSPI context. If not set, the default for the platform will be used. + + Indicates the state of the during the most recent network operation performed on the connection. diff --git a/doc/snippets/Microsoft.Data.SqlClient/SspiAuthenticationParameters.xml b/doc/snippets/Microsoft.Data.SqlClient/SspiAuthenticationParameters.xml new file mode 100644 index 0000000000..4623d86052 --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlClient/SspiAuthenticationParameters.xml @@ -0,0 +1,28 @@ + + + + + Provides parameters used during SSPI authentication. + + + Creates an instance of the SspiAuthenticationParameters. + The name of the server. + The resource (often the server service principal name). + + + Gets the resource (often the server service principal name). + + + Gets the server name. + + + Gets or sets the user id if available. + + + Gets or sets the database name if available. + + + Gets or sets the password if available. + + + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SspiContextProvider.xml b/doc/snippets/Microsoft.Data.SqlClient/SspiContextProvider.xml new file mode 100644 index 0000000000..2ea58cb80b --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlClient/SspiContextProvider.xml @@ -0,0 +1,20 @@ + + + + + Provides the ability to customize SSPI context generation. + + + Creates an instance of the SSPIContextProvider. + + + Generates an SSPI outgoing blob given the incoming blob. + Incoming blob + Outgoing blob + Gets the authentication parameters associated with this connection. + + true if the context was generated, otherwise false. + + + + diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index 9eacfc4de0..56d56b65e2 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -152,6 +152,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data.SqlClient", ..\doc\snippets\Microsoft.Data.SqlClient\SqlRowUpdatingEventArgs.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlRowUpdatingEventArgs.xml ..\doc\snippets\Microsoft.Data.SqlClient\SqlRowUpdatingEventHandler.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlRowUpdatingEventHandler.xml ..\doc\snippets\Microsoft.Data.SqlClient\SqlTransaction.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlTransaction.xml + ..\doc\snippets\Microsoft.Data.SqlClient\SspiAuthenticationParameters.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SspiAuthenticationParameters.xml + ..\doc\snippets\Microsoft.Data.SqlClient\SspiContextProvider.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SspiContextProvider.xml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data.SqlClient.DataClassification", "Microsoft.Data.SqlClient.DataClassification", "{5D1F0032-7B0D-4FB6-A969-FCFB25C9EA1D}" @@ -221,11 +223,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{4600328C-C13 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipelines", "pipelines", "{4CAE9195-4F1A-4D48-854C-1C9FBC512C66}" ProjectSection(SolutionItems) = preProject + ..\eng\pipelines\akv-official-pipeline.yml = ..\eng\pipelines\akv-official-pipeline.yml ..\eng\pipelines\dotnet-sqlclient-ci-core.yml = ..\eng\pipelines\dotnet-sqlclient-ci-core.yml ..\eng\pipelines\dotnet-sqlclient-ci-package-reference-pipeline.yml = ..\eng\pipelines\dotnet-sqlclient-ci-package-reference-pipeline.yml ..\eng\pipelines\dotnet-sqlclient-ci-project-reference-pipeline.yml = ..\eng\pipelines\dotnet-sqlclient-ci-project-reference-pipeline.yml ..\eng\pipelines\dotnet-sqlclient-signing-pipeline.yml = ..\eng\pipelines\dotnet-sqlclient-signing-pipeline.yml - ..\eng\pipelines\akv-official-pipeline.yml = ..\eng\pipelines\akv-official-pipeline.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{FD4D7A96-79B1-4F89-B64D-29FACCC9232F}" @@ -257,9 +259,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "steps", "steps", "{EABE3A3E ..\eng\pipelines\common\templates\steps\ci-project-build-step.yml = ..\eng\pipelines\common\templates\steps\ci-project-build-step.yml ..\eng\pipelines\common\templates\steps\code-analyze-step.yml = ..\eng\pipelines\common\templates\steps\code-analyze-step.yml ..\eng\pipelines\common\templates\steps\configure-sql-server-linux-step.yml = ..\eng\pipelines\common\templates\steps\configure-sql-server-linux-step.yml + ..\eng\pipelines\common\templates\steps\configure-sql-server-macos-step.yml = ..\eng\pipelines\common\templates\steps\configure-sql-server-macos-step.yml ..\eng\pipelines\common\templates\steps\configure-sql-server-step.yml = ..\eng\pipelines\common\templates\steps\configure-sql-server-step.yml ..\eng\pipelines\common\templates\steps\configure-sql-server-win-step.yml = ..\eng\pipelines\common\templates\steps\configure-sql-server-win-step.yml ..\eng\pipelines\common\templates\steps\copy-dlls-for-test-step.yml = ..\eng\pipelines\common\templates\steps\copy-dlls-for-test-step.yml + ..\eng\pipelines\common\templates\steps\ensure-dotnet-version.yml = ..\eng\pipelines\common\templates\steps\ensure-dotnet-version.yml ..\eng\pipelines\common\templates\steps\esrp-code-signing-step.yml = ..\eng\pipelines\common\templates\steps\esrp-code-signing-step.yml ..\eng\pipelines\common\templates\steps\generate-nuget-package-step.yml = ..\eng\pipelines\common\templates\steps\generate-nuget-package-step.yml ..\eng\pipelines\common\templates\steps\override-sni-version.yml = ..\eng\pipelines\common\templates\steps\override-sni-version.yml @@ -270,8 +274,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "steps", "steps", "{EABE3A3E ..\eng\pipelines\common\templates\steps\run-all-tests-step.yml = ..\eng\pipelines\common\templates\steps\run-all-tests-step.yml ..\eng\pipelines\common\templates\steps\update-config-file-step.yml = ..\eng\pipelines\common\templates\steps\update-config-file-step.yml ..\eng\pipelines\common\templates\steps\update-nuget-config-local-feed-step.yml = ..\eng\pipelines\common\templates\steps\update-nuget-config-local-feed-step.yml - ..\eng\pipelines\common\templates\steps\configure-sql-server-macos-step.yml = ..\eng\pipelines\common\templates\steps\configure-sql-server-macos-step.yml - ..\eng\pipelines\common\templates\steps\ensure-dotnet-version.yml = ..\eng\pipelines\common\templates\steps\ensure-dotnet-version.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "libraries", "libraries", "{75BAE755-3A1F-41F2-9176-9F8FF9FEE2DD}" @@ -288,24 +290,21 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "variables", "variables", "{ ProjectSection(SolutionItems) = preProject ..\eng\pipelines\variables\akv-official-variables.yml = ..\eng\pipelines\variables\akv-official-variables.yml ..\eng\pipelines\variables\common-variables.yml = ..\eng\pipelines\variables\common-variables.yml - ..\eng\pipelines\variables\onebranch-variables.yml = ..\eng\pipelines\variables\onebranch-variables.yml ..\eng\pipelines\variables\esrp-signing-variables.yml = ..\eng\pipelines\variables\esrp-signing-variables.yml + ..\eng\pipelines\variables\onebranch-variables.yml = ..\eng\pipelines\variables\onebranch-variables.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "jobs", "jobs", "{09352F1D-878F-4F55-8AA2-6E47F1AD37D5}" - ProjectSection(SolutionItems) = preProject - ..\eng\pipelines\jobs\ob-build-akv-official-job.yml = ..\eng\pipelines\jobs\build-akv-official-job.yml - EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "steps", "steps", "{AD738BD4-6A02-4B88-8F93-FBBBA49A74C8}" ProjectSection(SolutionItems) = preProject - ..\eng\pipelines\steps\script-output-environment-variables-step.yml = ..\eng\pipelines\steps\script-output-environment-variables-step.yml + ..\eng\pipelines\steps\compound-build-akv-step.yml = ..\eng\pipelines\steps\compound-build-akv-step.yml ..\eng\pipelines\steps\compound-esrp-code-signing-step.yml = ..\eng\pipelines\steps\compound-esrp-code-signing-step.yml + ..\eng\pipelines\steps\compound-extract-akv-apiscan-files-step.yml = ..\eng\pipelines\steps\compound-extract-akv-apiscan-files-step.yml ..\eng\pipelines\steps\compound-nuget-pack-step.yml = ..\eng\pipelines\steps\compound-nuget-pack-step.yml ..\eng\pipelines\steps\compound-publish-symbols-step.yml = ..\eng\pipelines\steps\compound-publish-symbols-step.yml ..\eng\pipelines\steps\roslyn-analyzers-akv-step.yml = ..\eng\pipelines\steps\roslyn-analyzers-akv-step.yml - ..\eng\pipelines\steps\compound-build-akv-step.yml = ..\eng\pipelines\steps\compound-build-akv-step.yml - ..\eng\pipelines\steps\compound-extract-akv-apiscan-files-step.yml = ..\eng\pipelines\steps\compound-extract-akv-apiscan-files-step.yml + ..\eng\pipelines\steps\script-output-environment-variables-step.yml = ..\eng\pipelines\steps\script-output-environment-variables-step.yml EndProjectSection EndProject Global diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index bdbee92399..ff6016c15c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -905,6 +905,8 @@ public void RegisterColumnEncryptionKeyStoreProvidersOnConnection(System.Collect [System.ComponentModel.BrowsableAttribute(false)] [System.ComponentModel.DesignerSerializationVisibilityAttribute(0)] public Microsoft.Data.SqlClient.SqlCredential Credential { get { throw null; } set { } } + /// + public SspiContextProvider SspiContextProvider { get { throw null; } set { } } /// [System.ComponentModel.DesignerSerializationVisibilityAttribute(0)] public override string Database { get { throw null; } } @@ -1940,6 +1942,37 @@ public sealed class SqlConfigurableRetryFactory /// public static SqlRetryLogicBaseProvider CreateNoneRetryProvider() { throw null; } } + /// + public abstract class SspiContextProvider + { + /// + protected abstract bool GenerateContext(System.ReadOnlySpan incomingBlob, System.Buffers.IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams); + } + /// + public sealed class SspiAuthenticationParameters + { + /// + public SspiAuthenticationParameters(string serverName, string resource) + { + ServerName = serverName; + Resource = resource; + } + + /// + public string Resource { get; } + + /// + public string ServerName { get; } + + /// + public string UserId { get; set; } + + /// + public string DatabaseName { get; set; } + + /// + public string Password { get; set; } + } } namespace Microsoft.Data.SqlClient.Diagnostics { 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 52112bfe74..0501e7c8d7 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 @@ -90,6 +90,7 @@ private static readonly Dictionary private IReadOnlyDictionary _customColumnEncryptionKeyStoreProviders; private Func> _accessTokenCallback; + private SspiContextProvider _sspiContextProvider; internal bool HasColumnEncryptionKeyStoreProvidersRegistered => _customColumnEncryptionKeyStoreProviders is not null && _customColumnEncryptionKeyStoreProviders.Count > 0; @@ -647,7 +648,7 @@ public override string ConnectionString CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessTokenCallback(connectionOptions); } } - ConnectionString_Set(new SqlConnectionPoolKey(value, _credential, _accessToken, _accessTokenCallback)); + ConnectionString_Set(new SqlConnectionPoolKey(value, _credential, _accessToken, _accessTokenCallback, _sspiContextProvider)); _connectionString = value; // Change _connectionString value only after value is validated CacheConnectionStringProperties(); } @@ -707,7 +708,7 @@ public string AccessToken } // Need to call ConnectionString_Set to do proper pool group check - ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, credential: _credential, accessToken: value, accessTokenCallback: null)); + ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, credential: _credential, accessToken: value, accessTokenCallback: null, sspiContextProvider: _sspiContextProvider)); _accessToken = value; } } @@ -730,11 +731,22 @@ public Func + public SspiContextProvider SspiContextProvider + { + get { return _sspiContextProvider; } + set + { + ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, credential: _credential, accessToken: null, accessTokenCallback: _accessTokenCallback, sspiContextProvider: value)); + _sspiContextProvider = value; + } + } + /// [ResDescription(StringsHelper.ResourceNames.SqlConnection_Database)] [ResCategory(StringsHelper.ResourceNames.SqlConnection_DataSource)] @@ -1032,7 +1044,7 @@ public SqlCredential Credential _credential = value; // Need to call ConnectionString_Set to do proper pool group check - ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, accessToken: _accessToken, accessTokenCallback: _accessTokenCallback)); + ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, accessToken: _accessToken, accessTokenCallback: _accessTokenCallback, _sspiContextProvider)); } } @@ -2262,7 +2274,7 @@ public static void ChangePassword(string connectionString, string newPassword) throw ADP.InvalidArgumentLength(nameof(newPassword), TdsEnums.MAXLEN_NEWPASSWORD); } - SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null, accessTokenCallback: null); + SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null, accessTokenCallback: null, sspiContextProvider: null); SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key); if (connectionOptions.IntegratedSecurity) @@ -2311,7 +2323,7 @@ public static void ChangePassword(string connectionString, SqlCredential credent throw ADP.InvalidArgumentLength(nameof(newSecurePassword), TdsEnums.MAXLEN_NEWPASSWORD); } - SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null); + SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null, sspiContextProvider: null); SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key); @@ -2349,7 +2361,7 @@ private static void ChangePassword(string connectionString, SqlConnectionString { con?.Dispose(); } - SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null); + SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null, sspiContextProvider: null); SqlConnectionFactory.SingletonInstance.ClearPool(key); } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs index ca9bcc2cd9..dce8e3d34c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -96,7 +96,7 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt // This first connection is established to SqlExpress to get the instance name // of the UserInstance. SqlConnectionString sseopt = new SqlConnectionString(opt, opt.DataSource, userInstance: true, setEnlistValue: false); - sseConnection = new SqlInternalConnectionTds(identity, sseopt, key.Credential, null, "", null, false, applyTransientFaultHandling: applyTransientFaultHandling); + sseConnection = new SqlInternalConnectionTds(identity, sseopt, key.Credential, null, "", null, false, applyTransientFaultHandling: applyTransientFaultHandling, sspiContextProvider: key.SspiContextProvider); // NOTE: Retrieve here. This user instance name will be used below to connect to the Sql Express User Instance. instanceName = sseConnection.InstanceName; @@ -136,7 +136,7 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt opt = new SqlConnectionString(opt, instanceName, userInstance: false, setEnlistValue: null); poolGroupProviderInfo = null; // null so we do not pass to constructor below... } - return new SqlInternalConnectionTds(identity, opt, key.Credential, poolGroupProviderInfo, "", null, redirectedUserInstance, userOpt, recoverySessionData, applyTransientFaultHandling: applyTransientFaultHandling, key.AccessToken, pool, key.AccessTokenCallback); + return new SqlInternalConnectionTds(identity, opt, key.Credential, poolGroupProviderInfo, "", null, redirectedUserInstance, userOpt, recoverySessionData, applyTransientFaultHandling: applyTransientFaultHandling, key.AccessToken, pool, key.AccessTokenCallback, key.SspiContextProvider); } protected override DbConnectionOptions CreateConnectionOptions(string connectionString, DbConnectionOptions previous) 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 d128268185..57930fa29b 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 @@ -136,6 +136,7 @@ internal sealed class SqlInternalConnectionTds : SqlInternalConnection, IDisposa SqlFedAuthToken _fedAuthToken = null; internal byte[] _accessTokenInBytes; internal readonly Func> _accessTokenCallback; + internal readonly SspiContextProvider _sspiContextProvider; private readonly ActiveDirectoryAuthenticationTimeoutRetryHelper _activeDirectoryAuthTimeoutRetryHelper; @@ -454,8 +455,8 @@ internal SqlInternalConnectionTds( bool applyTransientFaultHandling = false, string accessToken = null, DbConnectionPool pool = null, - Func> accessTokenCallback = null) : base(connectionOptions) + Func> accessTokenCallback = null, + SspiContextProvider sspiContextProvider = null) : base(connectionOptions) { #if DEBUG @@ -489,6 +490,7 @@ internal SqlInternalConnectionTds( } _accessTokenCallback = accessTokenCallback; + _sspiContextProvider = sspiContextProvider; _activeDirectoryAuthTimeoutRetryHelper = new ActiveDirectoryAuthenticationTimeoutRetryHelper(); 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 d66984d8b7..341c07b877 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 @@ -413,7 +413,7 @@ internal void Connect(ServerInfo serverInfo, // AD Integrated behaves like Windows integrated when connecting to a non-fedAuth server if (integratedSecurity || authType == SqlAuthenticationMethod.ActiveDirectoryIntegrated) { - _authenticationProvider = _physicalStateObj.CreateSspiContextProvider(); + _authenticationProvider = Connection._sspiContextProvider ?? _physicalStateObj.CreateSspiContextProvider(); SqlClientEventSource.Log.TryTraceEvent("TdsParser.Connect | SEC | SSPI or Active Directory Authentication Library loaded for SQL Server based integrated authentication"); } diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index 3d39e0083e..8d3eb09c86 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -808,6 +808,8 @@ public SqlConnection(string connectionString, Microsoft.Data.SqlClient.SqlCreden [System.ComponentModel.BrowsableAttribute(false)] [System.ComponentModel.DesignerSerializationVisibilityAttribute(0)] public Microsoft.Data.SqlClient.SqlCredential Credential { get { throw null; } set { } } + /// + public SspiContextProvider SspiContextProvider { get { throw null; } set { } } /// [System.ComponentModel.DesignerSerializationVisibilityAttribute(0)] public override string Database { get { throw null; } } @@ -1955,6 +1957,37 @@ public sealed class SqlConfigurableRetryFactory /// public static SqlRetryLogicBaseProvider CreateNoneRetryProvider() { throw null; } } + /// + public abstract class SspiContextProvider + { + /// + protected abstract bool GenerateContext(System.ReadOnlySpan incomingBlob, System.Buffers.IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams); + } + /// + public sealed class SspiAuthenticationParameters + { + /// + public SspiAuthenticationParameters(string serverName, string resource) + { + ServerName = serverName; + Resource = resource; + } + + /// + public string Resource { get; } + + /// + public string ServerName { get; } + + /// + public string UserId { get; set; } + + /// + public string DatabaseName { get; set; } + + /// + public string Password { get; set; } + } } namespace Microsoft.Data.SqlClient.Server { 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 2be92a6732..fc8a17dd18 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 @@ -88,6 +88,8 @@ private static readonly Dictionary private Func> _accessTokenCallback; + private SspiContextProvider _sspiContextProvider; + internal bool HasColumnEncryptionKeyStoreProvidersRegistered => _customColumnEncryptionKeyStoreProviders is not null && _customColumnEncryptionKeyStoreProviders.Count > 0; @@ -603,6 +605,17 @@ internal int ConnectRetryInterval get => ((SqlConnectionString)ConnectionOptions).ConnectRetryInterval; } + /// + public SspiContextProvider SspiContextProvider + { + get { return _sspiContextProvider; } + set + { + ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, null, _accessTokenCallback, value)); + _sspiContextProvider = value; + } + } + /// [DefaultValue("")] #pragma warning disable 618 // ignore obsolete warning about RecommendedAsConfigurable to use SettingsBindableAttribute @@ -671,7 +684,7 @@ public override string ConnectionString CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessTokenCallback(connectionOptions); } } - ConnectionString_Set(new SqlConnectionPoolKey(value, _credential, _accessToken, _accessTokenCallback)); + ConnectionString_Set(new SqlConnectionPoolKey(value, _credential, _accessToken, _accessTokenCallback, _sspiContextProvider)); _connectionString = value; // Change _connectionString value only after value is validated CacheConnectionStringProperties(); } @@ -731,7 +744,7 @@ public string AccessToken _accessToken = value; // Need to call ConnectionString_Set to do proper pool group check - ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, _accessToken, null)); + ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, _accessToken, null, _sspiContextProvider)); } } @@ -753,7 +766,7 @@ public Func> _accessTokenCallback; + internal readonly SspiContextProvider _sspiContextProvider; private readonly ActiveDirectoryAuthenticationTimeoutRetryHelper _activeDirectoryAuthTimeoutRetryHelper; @@ -430,7 +431,8 @@ internal SqlInternalConnectionTds( string accessToken = null, bool applyTransientFaultHandling = false, Func> accessTokenCallback = null) : base(connectionOptions) + Task> accessTokenCallback = null, + SspiContextProvider sspiContextProvider = null) : base(connectionOptions) { #if DEBUG @@ -486,6 +488,7 @@ internal SqlInternalConnectionTds( } _accessTokenCallback = accessTokenCallback; + _sspiContextProvider = sspiContextProvider; _activeDirectoryAuthTimeoutRetryHelper = new ActiveDirectoryAuthenticationTimeoutRetryHelper(); 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 130094dc53..b190aaedeb 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 @@ -411,7 +411,7 @@ internal void Connect(ServerInfo serverInfo, // AD Integrated behaves like Windows integrated when connecting to a non-fedAuth server if (integratedSecurity || authType == SqlAuthenticationMethod.ActiveDirectoryIntegrated) { - _authenticationProvider = _physicalStateObj.CreateSspiContextProvider(); + _authenticationProvider = Connection._sspiContextProvider ?? _physicalStateObj.CreateSspiContextProvider(); if (!string.IsNullOrEmpty(serverInfo.ServerSPN)) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs index 207c0a8e1a..356ef9436c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs @@ -18,10 +18,12 @@ internal class SqlConnectionPoolKey : DbConnectionPoolKey private readonly SqlCredential _credential; private readonly string _accessToken; private Func> _accessTokenCallback; + private SspiContextProvider _sspiContextProvider; internal SqlCredential Credential => _credential; internal string AccessToken => _accessToken; internal Func> AccessTokenCallback => _accessTokenCallback; + internal SspiContextProvider SspiContextProvider => _sspiContextProvider; internal override string ConnectionString { @@ -33,12 +35,19 @@ internal override string ConnectionString } } - internal SqlConnectionPoolKey(string connectionString, SqlCredential credential, string accessToken, Func> accessTokenCallback) : base(connectionString) + internal SqlConnectionPoolKey( + string connectionString, + SqlCredential credential, + string accessToken, + Func> accessTokenCallback, + SspiContextProvider sspiContextProvider + ) : base(connectionString) { Debug.Assert(credential == null || accessToken == null || accessTokenCallback == null, "Credential, AccessToken, and Callback can't have a value at the same time."); _credential = credential; _accessToken = accessToken; _accessTokenCallback = accessTokenCallback; + _sspiContextProvider = sspiContextProvider; CalculateHashCode(); } @@ -47,6 +56,8 @@ private SqlConnectionPoolKey(SqlConnectionPoolKey key) : base(key) _credential = key.Credential; _accessToken = key.AccessToken; _accessTokenCallback = key._accessTokenCallback; + _sspiContextProvider = key._sspiContextProvider; + CalculateHashCode(); } @@ -61,7 +72,8 @@ public override bool Equals(object obj) && _credential == key._credential && ConnectionString == key.ConnectionString && _accessTokenCallback == key._accessTokenCallback - && string.CompareOrdinal(_accessToken, key._accessToken) == 0); + && string.CompareOrdinal(_accessToken, key._accessToken) == 0 + && _sspiContextProvider == key._sspiContextProvider); } public override int GetHashCode() @@ -94,6 +106,11 @@ private void CalculateHashCode() _hashValue = _hashValue * 17 + _accessTokenCallback.GetHashCode(); } } + + if (_sspiContextProvider != null) + { + _hashValue = _hashValue * 17 + _sspiContextProvider.GetHashCode(); + } } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSspiContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSspiContextProvider.cs index 5935b149c8..1cc4af3e9c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSspiContextProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSspiContextProvider.cs @@ -49,7 +49,7 @@ private void LoadSSPILibrary() } } - protected override bool GenerateSspiClientContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) + protected override bool GenerateContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) { #if NETFRAMEWORK SNIHandle handle = _physicalStateObj.Handle; @@ -62,7 +62,7 @@ protected override bool GenerateSspiClientContext(ReadOnlySpan incomingBlo var sendLength = s_maxSSPILength; var outBuff = outgoingBlobWriter.GetSpan((int)sendLength); - if (0 != SniNativeWrapper.SniSecGenClientContext(handle, incomingBlob, outBuff, ref sendLength, authParams.Resource)) + if (SniNativeWrapper.SniSecGenClientContext(handle, incomingBlob, outBuff, ref sendLength, authParams.Resource) != 0) { return false; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs index 5dc52010b3..a255712238 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs @@ -12,7 +12,7 @@ internal sealed class NegotiateSspiContextProvider : SspiContextProvider { private NegotiateAuthentication? _negotiateAuth = null; - protected override bool GenerateSspiClientContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) + protected override bool GenerateContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) { NegotiateAuthenticationStatusCode statusCode = NegotiateAuthenticationStatusCode.UnknownCredentials; @@ -21,7 +21,7 @@ protected override bool GenerateSspiClientContext(ReadOnlySpan incomingBlo // Log session id, status code and the actual SPN used in the negotiation SqlClientEventSource.Log.TryTraceEvent("{0}.{1} | Info | Session Id {2}, StatusCode={3}, SPN={4}", nameof(NegotiateSspiContextProvider), - nameof(GenerateSspiClientContext), _physicalStateObj.SessionId, statusCode, _negotiateAuth.TargetName); + nameof(GenerateContext), _physicalStateObj.SessionId, statusCode, _negotiateAuth.TargetName); if (statusCode == NegotiateAuthenticationStatusCode.Completed || statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiAuthenticationParameters.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiAuthenticationParameters.cs index dce0858360..ad6c92853f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiAuthenticationParameters.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiAuthenticationParameters.cs @@ -2,22 +2,29 @@ namespace Microsoft.Data.SqlClient { - internal sealed class SspiAuthenticationParameters + /// + public sealed class SspiAuthenticationParameters { + /// public SspiAuthenticationParameters(string serverName, string resource) { ServerName = serverName; Resource = resource; } + /// public string Resource { get; } + /// public string ServerName { get; } + /// public string? UserId { get; set; } + /// public string? DatabaseName { get; set; } + /// public string? Password { get; set; } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs index ff83422f10..7b339f285d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs @@ -6,12 +6,18 @@ namespace Microsoft.Data.SqlClient { - internal abstract class SspiContextProvider + /// + public abstract class SspiContextProvider { private TdsParser _parser = null!; private ServerInfo _serverInfo = null!; private protected TdsParserStateObject _physicalStateObj = null!; + /// + protected SspiContextProvider() + { + } + internal void Initialize(ServerInfo serverInfo, TdsParserStateObject physicalStateObj, TdsParser parser) { _parser = parser; @@ -25,13 +31,14 @@ private protected virtual void Initialize() { } - protected abstract bool GenerateSspiClientContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams); + /// + protected abstract bool GenerateContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams); internal void SSPIData(ReadOnlySpan receivedBuff, IBufferWriter outgoingBlobWriter, string serverSpn) { using var _ = TrySNIEventScope.Create(nameof(SspiContextProvider)); - if (!RunGenerateSspiClientContext(receivedBuff, outgoingBlobWriter, serverSpn)) + if (!RunGenerateContext(receivedBuff, outgoingBlobWriter, serverSpn)) { // If we've hit here, the SSPI context provider implementation failed to generate the SSPI context. SSPIError(SQLMessage.SSPIGenerateError(), TdsEnums.GEN_CLIENT_CONTEXT); @@ -44,7 +51,7 @@ internal void SSPIData(ReadOnlySpan receivedBuff, IBufferWriter outg foreach (var serverSpn in serverSpns) { - if (RunGenerateSspiClientContext(receivedBuff, outgoingBlobWriter, serverSpn)) + if (RunGenerateContext(receivedBuff, outgoingBlobWriter, serverSpn)) { return; } @@ -54,7 +61,7 @@ internal void SSPIData(ReadOnlySpan receivedBuff, IBufferWriter outg SSPIError(SQLMessage.SSPIGenerateError(), TdsEnums.GEN_CLIENT_CONTEXT); } - private bool RunGenerateSspiClientContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, string serverSpn) + private bool RunGenerateContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, string serverSpn) { var options = _parser.Connection.ConnectionOptions; var authParams = new SspiAuthenticationParameters(options.DataSource, serverSpn) @@ -66,9 +73,9 @@ private bool RunGenerateSspiClientContext(ReadOnlySpan incomingBlob, IBuff try { - SqlClientEventSource.Log.TryTraceEvent("{0}.{1} | Info | SPN={1}", GetType().FullName, nameof(GenerateSspiClientContext), serverSpn); + SqlClientEventSource.Log.TryTraceEvent("{0}.{1} | Info | SPN={1}", GetType().FullName, nameof(GenerateContext), serverSpn); - return GenerateSspiClientContext(incomingBlob, outgoingBlobWriter, authParams); + return GenerateContext(incomingBlob, outgoingBlobWriter, authParams); } catch (Exception e) { @@ -77,7 +84,7 @@ private bool RunGenerateSspiClientContext(ReadOnlySpan incomingBlob, IBuff } } - protected void SSPIError(string error, string procedure) + private protected void SSPIError(string error, string procedure) { Debug.Assert(!string.IsNullOrEmpty(procedure), "TdsParser.SSPIError called with an empty or null procedure string"); Debug.Assert(!string.IsNullOrEmpty(error), "TdsParser.SSPIError called with an empty or null error string"); From 8937cb780129c7792552278be7ef632093257c47 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Tue, 13 May 2025 07:53:49 -0700 Subject: [PATCH 2/5] add test --- .../ConnectionPool/SqlConnectionPoolKey.cs | 3 +- .../SQL/KerberosTests/KerberosTest.cs | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs index 356ef9436c..31da3521df 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs @@ -40,8 +40,7 @@ internal SqlConnectionPoolKey( SqlCredential credential, string accessToken, Func> accessTokenCallback, - SspiContextProvider sspiContextProvider - ) : base(connectionString) + SspiContextProvider sspiContextProvider) : base(connectionString) { Debug.Assert(credential == null || accessToken == null || accessTokenCallback == null, "Credential, AccessToken, and Callback can't have a value at the same time."); _credential = credential; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs index c6026088f4..a88ce085c4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Buffers; using System.Collections; using System.Collections.Generic; using Xunit; @@ -24,6 +26,43 @@ public void IsKerBerosSetupTestAsync(string connectionStr) Assert.True(reader.Read(), "Expected to receive one row data"); Assert.Equal("KERBEROS", reader.GetString(0)); } + + [PlatformSpecific(TestPlatforms.AnyUnix)] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsKerberosTest))] + [ClassData(typeof(ConnectionStringsProvider))] + public void CustomSspiContextGeneratorTest(string connectionStr) + { + KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword); + + using SqlConnection conn = new(connectionStr) + { + SspiContextProvider = new TestSspiContextProvider(), + }; + + try + { + conn.Open(); + using SqlCommand command = new("SELECT auth_scheme from sys.dm_exec_connections where session_id = @@spid", conn); + using SqlDataReader reader = command.ExecuteReader(); + + Assert.Fail("Expected to use custom SSPI context provider"); + } + catch (SspiTestException) + { + } + } + + private sealed class TestSspiContextProvider : SspiContextProvider + { + protected override bool GenerateContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) + { + throw new SspiTestException(); + } + } + + private sealed class SspiTestException : Exception + { + } } public class ConnectionStringsProvider : IEnumerable From 8b44bc9325b6fbff29048bdb04bd9c7e04cc8407 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Tue, 13 May 2025 08:12:11 -0700 Subject: [PATCH 3/5] validate auth params --- .../SQL/KerberosTests/KerberosTest.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs index a88ce085c4..f246cb5d2e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs @@ -47,8 +47,14 @@ public void CustomSspiContextGeneratorTest(string connectionStr) Assert.Fail("Expected to use custom SSPI context provider"); } - catch (SspiTestException) + catch (SspiTestException sspi) { + var builder = new SqlConnectionStringBuilder(connectionStr); + + Assert.Equal(sspi.AuthParams.ServerName, builder.DataSource); + Assert.Equal(sspi.AuthParams.DatabaseName, builder.InitialCatalog); + Assert.Equal(sspi.AuthParams.UserId, builder.UserID); + Assert.Equal(sspi.AuthParams.Password, builder.Password); } } @@ -56,12 +62,18 @@ private sealed class TestSspiContextProvider : SspiContextProvider { protected override bool GenerateContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) { - throw new SspiTestException(); + throw new SspiTestException(authParams); } } private sealed class SspiTestException : Exception { + public SspiTestException(SspiAuthenticationParameters authParams) + { + AuthParams = authParams; + } + + public SspiAuthenticationParameters AuthParams { get; } } } From 49a67a3a42fab6ffd66fb18d920f9258c29fbf91 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Tue, 13 May 2025 08:19:34 -0700 Subject: [PATCH 4/5] move to integrated auth tests --- .../IntegratedAuthenticationTest.cs | 53 +++++++++++++++++++ .../SQL/KerberosTests/KerberosTest.cs | 51 ------------------ 2 files changed, 53 insertions(+), 51 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/IntegratedAuthenticationTest/IntegratedAuthenticationTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/IntegratedAuthenticationTest/IntegratedAuthenticationTest.cs index e043b2253c..af650a408e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/IntegratedAuthenticationTest/IntegratedAuthenticationTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/IntegratedAuthenticationTest/IntegratedAuthenticationTest.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Buffers; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -56,6 +58,39 @@ public static void IntegratedAuthenticationTest_ServerSPN() TryOpenConnectionWithIntegratedAuthentication(builder.ConnectionString); } + [ConditionalFact(nameof(IsIntegratedSecurityEnvironmentSet), nameof(AreConnectionStringsSetup))] + public static void CustomSspiContextGeneratorTest() + { + SqlConnectionStringBuilder builder = new(DataTestUtility.TCPConnectionString); + builder.IntegratedSecurity = true; + Assert.True(DataTestUtility.ParseDataSource(builder.DataSource, out string hostname, out int port, out string instanceName)); + // Build the SPN for the server we are connecting to + builder.ServerSPN = $"MSSQLSvc/{DataTestUtility.GetMachineFQDN(hostname)}"; + if (!string.IsNullOrWhiteSpace(instanceName)) + { + builder.ServerSPN += ":" + instanceName; + } + + using SqlConnection conn = new(builder.ConnectionString) + { + SspiContextProvider = new TestSspiContextProvider(), + }; + + try + { + conn.Open(); + + Assert.Fail("Expected to use custom SSPI context provider"); + } + catch (SspiTestException sspi) + { + Assert.Equal(sspi.AuthParams.ServerName, builder.DataSource); + Assert.Equal(sspi.AuthParams.DatabaseName, builder.InitialCatalog); + Assert.Equal(sspi.AuthParams.UserId, builder.UserID); + Assert.Equal(sspi.AuthParams.Password, builder.Password); + } + } + private static void TryOpenConnectionWithIntegratedAuthentication(string connectionString) { using (SqlConnection connection = new SqlConnection(connectionString)) @@ -63,5 +98,23 @@ private static void TryOpenConnectionWithIntegratedAuthentication(string connect connection.Open(); } } + + private sealed class TestSspiContextProvider : SspiContextProvider + { + protected override bool GenerateContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) + { + throw new SspiTestException(authParams); + } + } + + private sealed class SspiTestException : Exception + { + public SspiTestException(SspiAuthenticationParameters authParams) + { + AuthParams = authParams; + } + + public SspiAuthenticationParameters AuthParams { get; } + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs index f246cb5d2e..c6026088f4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/KerberosTests/KerberosTest.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Buffers; using System.Collections; using System.Collections.Generic; using Xunit; @@ -26,55 +24,6 @@ public void IsKerBerosSetupTestAsync(string connectionStr) Assert.True(reader.Read(), "Expected to receive one row data"); Assert.Equal("KERBEROS", reader.GetString(0)); } - - [PlatformSpecific(TestPlatforms.AnyUnix)] - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsKerberosTest))] - [ClassData(typeof(ConnectionStringsProvider))] - public void CustomSspiContextGeneratorTest(string connectionStr) - { - KerberosTicketManagemnt.Init(DataTestUtility.KerberosDomainUser, DataTestUtility.KerberosDomainPassword); - - using SqlConnection conn = new(connectionStr) - { - SspiContextProvider = new TestSspiContextProvider(), - }; - - try - { - conn.Open(); - using SqlCommand command = new("SELECT auth_scheme from sys.dm_exec_connections where session_id = @@spid", conn); - using SqlDataReader reader = command.ExecuteReader(); - - Assert.Fail("Expected to use custom SSPI context provider"); - } - catch (SspiTestException sspi) - { - var builder = new SqlConnectionStringBuilder(connectionStr); - - Assert.Equal(sspi.AuthParams.ServerName, builder.DataSource); - Assert.Equal(sspi.AuthParams.DatabaseName, builder.InitialCatalog); - Assert.Equal(sspi.AuthParams.UserId, builder.UserID); - Assert.Equal(sspi.AuthParams.Password, builder.Password); - } - } - - private sealed class TestSspiContextProvider : SspiContextProvider - { - protected override bool GenerateContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) - { - throw new SspiTestException(authParams); - } - } - - private sealed class SspiTestException : Exception - { - public SspiTestException(SspiAuthenticationParameters authParams) - { - AuthParams = authParams; - } - - public SspiAuthenticationParameters AuthParams { get; } - } } public class ConnectionStringsProvider : IEnumerable From 45a3228ee0f7d5f1558fb295c1243cf08053a3bc Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Wed, 21 May 2025 09:12:01 -0700 Subject: [PATCH 5/5] per feedback --- doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml | 8 ++++++++ .../netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs | 8 ++++---- .../netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs | 8 ++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml index 57b205f2a2..eb3e146060 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml @@ -2090,6 +2090,14 @@ The following sample tries to open a connection to an invalid database to simula Gets or sets the instance for customizing the SSPI context. If not set, the default for the platform will be used. + + An instance. + + + + The SspiContextProvider is a part of the connection pool key. Care should be taken when using this property to ensure the implementation returns a stable identity per resource. + + 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 0501e7c8d7..29e479525d 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 @@ -708,7 +708,7 @@ public string AccessToken } // Need to call ConnectionString_Set to do proper pool group check - ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, credential: _credential, accessToken: value, accessTokenCallback: null, sspiContextProvider: _sspiContextProvider)); + ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, credential: _credential, accessToken: value, accessTokenCallback: null, sspiContextProvider: null)); _accessToken = value; } } @@ -731,7 +731,7 @@ public Func