Skip to content

Expose SspiAuthenticationParameters on SspiContextProvider #2454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private void LoadSSPILibrary()
}
}

protected override void GenerateSspiClientContext(ReadOnlySpan<byte> incomingBlob, IBufferWriter<byte> outgoingBlobWriter, ReadOnlySpan<string> serverSpns)
protected override bool GenerateSspiClientContext(ReadOnlySpan<byte> incomingBlob, IBufferWriter<byte> outgoingBlobWriter, SqlAuthenticationParameters authParams)
{
#if NETFRAMEWORK
SNIHandle handle = _physicalStateObj.Handle;
Expand All @@ -62,9 +62,9 @@ protected override void GenerateSspiClientContext(ReadOnlySpan<byte> incomingBlo
var sendLength = s_maxSSPILength;
var outBuff = outgoingBlobWriter.GetSpan((int)sendLength);

if (0 != SniNativeWrapper.SniSecGenClientContext(handle, incomingBlob, outBuff, ref sendLength, serverSpns[0]))
if (0 != SniNativeWrapper.SniSecGenClientContext(handle, incomingBlob, outBuff, ref sendLength, authParams.ServerName))
{
throw new InvalidOperationException(SQLMessage.SSPIGenerateError());
return false;
}

if (sendLength > int.MaxValue)
Expand All @@ -73,6 +73,8 @@ protected override void GenerateSspiClientContext(ReadOnlySpan<byte> incomingBlo
}

outgoingBlobWriter.Advance((int)sendLength);

return true;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#if NET
#if NET

using System;
using System.Net.Security;
using System.Buffers;
using System.Net.Security;

#nullable enable

Expand All @@ -12,33 +12,24 @@ internal sealed class NegotiateSSPIContextProvider : SSPIContextProvider
{
private NegotiateAuthentication? _negotiateAuth = null;

protected override void GenerateSspiClientContext(ReadOnlySpan<byte> incomingBlob, IBufferWriter<byte> outgoingBlobWriter, ReadOnlySpan<string> serverSpns)
protected override bool GenerateSspiClientContext(ReadOnlySpan<byte> incomingBlob, IBufferWriter<byte> outgoingBlobWriter, SqlAuthenticationParameters authParams)
{
NegotiateAuthenticationStatusCode statusCode = NegotiateAuthenticationStatusCode.UnknownCredentials;

for (int i = 0; i < serverSpns.Length; i++)
{
_negotiateAuth ??= new(new NegotiateAuthenticationClientOptions { Package = "Negotiate", TargetName = serverSpns[i] });
var sendBuff = _negotiateAuth.GetOutgoingBlob(incomingBlob, out statusCode)!;

// 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);
if (statusCode == NegotiateAuthenticationStatusCode.Completed || statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded)
{
outgoingBlobWriter.Write(sendBuff);
break; // Successful case, exit the loop with current SPN.
}
else
{
_negotiateAuth = null; // Reset _negotiateAuth to be generated again for next SPN.
}
}
_negotiateAuth ??= new(new NegotiateAuthenticationClientOptions { Package = "Negotiate", TargetName = authParams.ServerName });
var sendBuff = _negotiateAuth.GetOutgoingBlob(incomingBlob, out statusCode)!;

// 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);

if (statusCode is not NegotiateAuthenticationStatusCode.Completed and not NegotiateAuthenticationStatusCode.ContinueNeeded)
if (statusCode == NegotiateAuthenticationStatusCode.Completed || statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded)
{
throw new InvalidOperationException(SQLMessage.SSPIGenerateError() + Environment.NewLine + statusCode);
outgoingBlobWriter.Write(sendBuff);
return true;
}

return false;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,78 @@ private protected virtual void Initialize()
{
}

protected abstract void GenerateSspiClientContext(ReadOnlySpan<byte> incomingBlob, IBufferWriter<byte> outgoingBlobWriter, ReadOnlySpan<string> serverSpns);
protected abstract bool GenerateSspiClientContext(ReadOnlySpan<byte> incomingBlob, IBufferWriter<byte> outgoingBlobWriter, SqlAuthenticationParameters authParams);

internal void SSPIData(ReadOnlySpan<byte> receivedBuff, IBufferWriter<byte> outgoingBlobWriter, string serverSpn)
=> SSPIData(receivedBuff, outgoingBlobWriter, new[] { serverSpn });
{
using var _ = TrySNIEventScope.Create(nameof(SSPIContextProvider));

internal void SSPIData(ReadOnlySpan<byte> receivedBuff, IBufferWriter<byte> outgoingBlobWriter, string[] serverSpns)
if (!RunGenerateSspiClientContext(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);
}
}

internal void SSPIData(ReadOnlySpan<byte> receivedBuff, IBufferWriter<byte> outgoingBlobWriter, ReadOnlySpan<string> serverSpns)
{
using (TrySNIEventScope.Create(nameof(SSPIContextProvider)))
using var _ = TrySNIEventScope.Create(nameof(SSPIContextProvider));

foreach (var serverSpn in serverSpns)
{
try
{
GenerateSspiClientContext(receivedBuff, outgoingBlobWriter, serverSpns);
}
catch (Exception e)
if (RunGenerateSspiClientContext(receivedBuff, outgoingBlobWriter, serverSpn))
{
SSPIError(e.Message + Environment.NewLine + e.StackTrace, TdsEnums.GEN_CLIENT_CONTEXT);
return;
}
}

// If we've hit here, the SSPI context provider implementation failed to generate the SSPI context.
SSPIError(SQLMessage.SSPIGenerateError(), TdsEnums.GEN_CLIENT_CONTEXT);
}

private bool RunGenerateSspiClientContext(ReadOnlySpan<byte> incomingBlob, IBufferWriter<byte> outgoingBlobWriter, string serverSpn)
{
var authParams = CreateSqlAuthParams(_parser.Connection, serverSpn);

try
{
#if NET8_0_OR_GREATER
SqlClientEventSource.Log.TryTraceEvent("{0}.{1} | Info | Session Id {2}, SPN={3}", GetType().FullName,
nameof(GenerateSspiClientContext), _physicalStateObj.SessionId, serverSpn);
#else
SqlClientEventSource.Log.TryTraceEvent("{0}.{1} | Info | SPN={1}", GetType().FullName,
nameof(GenerateSspiClientContext), serverSpn);
#endif

return GenerateSspiClientContext(incomingBlob, outgoingBlobWriter, authParams);
}
catch (Exception e)
{
SSPIError(e.Message + Environment.NewLine + e.StackTrace, TdsEnums.GEN_CLIENT_CONTEXT);
return false;
}
}

private static SqlAuthenticationParameters CreateSqlAuthParams(SqlInternalConnectionTds connection, string serverSpn)
{
var auth = new SqlAuthenticationParameters.Builder(
authenticationMethod: connection.ConnectionOptions.Authentication,
resource: null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to think of the best way to populate this. In other usage, I see serverSpn getting set as the resource and dataSource getting set as the serverName. I think it makes sense to continue that pattern here given that serverName and server SPN may be different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var authParamsBuilder = new SqlAuthenticationParameters.Builder(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not sure how to feel about authority. In federated auth, it's assumed that authority will be present, but here we don't set a value.

Let me chat with the team to see if we'd prefer to create a new type to better represent this set of credentials.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I refactored the builder so that the same pattern can be had wherever it's used if we want to use the existing SqlAuthenticationParameters

authority: null,
serverName: serverSpn,
connection.ConnectionOptions.InitialCatalog);

if (connection.ConnectionOptions.UserID is { } userId)
{
auth.WithUserId(userId);
}

if (connection.ConnectionOptions.Password is { } password)
{
auth.WithPassword(password);
}

return auth;
}

protected void SSPIError(string error, string procedure)
Expand Down
Loading