Skip to content
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

Make handling of DefaultCredentials in NegotiateAuthentication/SocketsHttpHandler more consistent #91160

Merged
merged 1 commit into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -139,5 +139,33 @@ await server.AcceptConnectionAsync(async connection =>
}).ConfigureAwait(false);
});
}

[Fact]
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Windows, "DefaultCredentials are unsupported for NTLM on Unix / Managed implementation")]
public async Task DefaultHandler_FakeServer_DefaultCredentials()
{
await LoopbackServer.CreateClientAndServerAsync(
async uri =>
{
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
requestMessage.Version = new Version(1, 1);

HttpMessageHandler handler = new HttpClientHandler() { Credentials = CredentialCache.DefaultCredentials };
using (var client = new HttpClient(handler))
{
HttpResponseMessage response = await client.SendAsync(requestMessage);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
},
async server =>
{
await server.AcceptConnectionAsync(async connection =>
{
var authHeader = "WWW-Authenticate: NTLM\r\n";
await connection.SendResponseAsync(HttpStatusCode.Unauthorized, authHeader).ConfigureAwait(false);
connection.CompleteRequestProcessing();
}).ConfigureAwait(false);
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static NegotiateAuthenticationPal Create(NegotiateAuthenticationClientOpt
switch (clientOptions.Package)
{
case NegotiationInfoClass.NTLM:
return new ManagedNtlmNegotiateAuthenticationPal(clientOptions);
return ManagedNtlmNegotiateAuthenticationPal.Create(clientOptions);

case NegotiationInfoClass.Negotiate:
return new ManagedSpnegoNegotiateAuthenticationPal(clientOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,24 +218,32 @@ private unsafe struct NtChallengeResponse
public override IIdentity RemoteIdentity => throw new InvalidOperationException();
public override System.Security.Principal.TokenImpersonationLevel ImpersonationLevel => System.Security.Principal.TokenImpersonationLevel.Impersonation;

public ManagedNtlmNegotiateAuthenticationPal(NegotiateAuthenticationClientOptions clientOptions)
private ManagedNtlmNegotiateAuthenticationPal(NegotiateAuthenticationClientOptions clientOptions)
{
Debug.Assert(clientOptions.Package == NegotiationInfoClass.NTLM);

_credential = clientOptions.Credential;
if (string.IsNullOrWhiteSpace(_credential.UserName) || string.IsNullOrWhiteSpace(_credential.Password))
{
// NTLM authentication is not possible with default credentials which are no-op
throw new PlatformNotSupportedException(SR.net_ntlm_not_possible_default_cred);
}

_spn = clientOptions.TargetName;
_channelBinding = clientOptions.Binding;
_protectionLevel = clientOptions.RequiredProtectionLevel;

if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"package={clientOptions.Package}, spn={_spn}, requiredProtectionLevel={_protectionLevel}");
}

public static new NegotiateAuthenticationPal Create(NegotiateAuthenticationClientOptions clientOptions)
{
Debug.Assert(clientOptions.Package == NegotiationInfoClass.NTLM);

if (clientOptions.Credential == CredentialCache.DefaultNetworkCredentials ||
string.IsNullOrWhiteSpace(clientOptions.Credential.UserName) ||

Choose a reason for hiding this comment

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

I think this should be an && between username and password.

Copy link
Member Author

Choose a reason for hiding this comment

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

It was always || in all versions on .NET. It's not like empty user name with non-empty password, or vice versa, makes any sense.

Copy link

@dotMorten dotMorten Aug 26, 2023

Choose a reason for hiding this comment

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

it's not? I've seen services that only require password with empty username.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think that's valid in NTLM or Kerberos. It's valid in Basic authentication.

string.IsNullOrWhiteSpace(clientOptions.Credential.Password))
{
// NTLM authentication is not possible with default credentials which are no-op
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, SR.net_ntlm_not_possible_default_cred);
return new UnsupportedNegotiateAuthenticationPal(clientOptions, NegotiateAuthenticationStatusCode.UnknownCredentials);
}

return new ManagedNtlmNegotiateAuthenticationPal(clientOptions);
}

public override void Dispose()
{
// Dispose of the state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,16 +303,7 @@ private IEnumerable<KeyValuePair<string, string>> EnumerateMechanisms()
{
// Abandon the optimistic path and restart with a new mechanism
_optimisticMechanism?.Dispose();
_mechanism = NegotiateAuthenticationPal.Create(new NegotiateAuthenticationClientOptions
{
Package = requestedPackage,
Credential = _clientOptions.Credential,
TargetName = _clientOptions.TargetName,
Binding = _clientOptions.Binding,
RequiredProtectionLevel = _clientOptions.RequiredProtectionLevel,
RequireMutualAuthentication = _clientOptions.RequireMutualAuthentication,
AllowedImpersonationLevel = _clientOptions.AllowedImpersonationLevel,
});
_mechanism = CreateMechanismForPackage(requestedPackage);
}

_optimisticMechanism = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static NegotiateAuthenticationPal Create(NegotiateAuthenticationClientOpt
switch (clientOptions.Package)
{
case NegotiationInfoClass.NTLM:
return new ManagedNtlmNegotiateAuthenticationPal(clientOptions);
return ManagedNtlmNegotiateAuthenticationPal.Create(clientOptions);

case NegotiationInfoClass.Negotiate:
return new ManagedSpnegoNegotiateAuthenticationPal(clientOptions, supportKerberos: true);
Expand All @@ -42,13 +42,15 @@ public static NegotiateAuthenticationPal Create(NegotiateAuthenticationClientOpt
{
return new UnixNegotiateAuthenticationPal(clientOptions);
}
catch (Win32Exception)
catch (Interop.NetSecurityNative.GssApiException gex)
{
return new UnsupportedNegotiateAuthenticationPal(clientOptions);
}
catch (PlatformNotSupportedException)
{
return new UnsupportedNegotiateAuthenticationPal(clientOptions);
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, gex);
NegotiateAuthenticationStatusCode statusCode = UnixNegotiateAuthenticationPal.GetErrorCode(gex);
if (statusCode <= NegotiateAuthenticationStatusCode.GenericFailure)
{
statusCode = NegotiateAuthenticationStatusCode.Unsupported;
}
return new UnsupportedNegotiateAuthenticationPal(clientOptions, statusCode);
}
catch (EntryPointNotFoundException)
{
Expand All @@ -63,13 +65,15 @@ public static NegotiateAuthenticationPal Create(NegotiateAuthenticationServerOpt
{
return new UnixNegotiateAuthenticationPal(serverOptions);
}
catch (Win32Exception)
catch (Interop.NetSecurityNative.GssApiException gex)
{
return new UnsupportedNegotiateAuthenticationPal(serverOptions);
}
catch (PlatformNotSupportedException)
{
return new UnsupportedNegotiateAuthenticationPal(serverOptions);
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, gex);
NegotiateAuthenticationStatusCode statusCode = UnixNegotiateAuthenticationPal.GetErrorCode(gex);
if (statusCode <= NegotiateAuthenticationStatusCode.GenericFailure)
{
statusCode = NegotiateAuthenticationStatusCode.Unsupported;
}
return new UnsupportedNegotiateAuthenticationPal(serverOptions, statusCode);
}
catch (EntryPointNotFoundException)
{
Expand Down Expand Up @@ -184,22 +188,25 @@ public UnixNegotiateAuthenticationPal(NegotiateAuthenticationClientOptions clien

if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Peer SPN-> '{_spn}'");

if (clientOptions.Credential == CredentialCache.DefaultCredentials ||
if (clientOptions.Credential == CredentialCache.DefaultNetworkCredentials ||
string.IsNullOrWhiteSpace(clientOptions.Credential.UserName) ||
string.IsNullOrWhiteSpace(clientOptions.Credential.Password))
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "using DefaultCredentials");
_credentialsHandle = AcquireDefaultCredential();

if (_packageType == Interop.NetSecurityNative.PackageType.NTLM)
{
// NTLM authentication is not possible with default credentials which are no-op
throw new PlatformNotSupportedException(SR.net_ntlm_not_possible_default_cred);
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, SR.net_ntlm_not_possible_default_cred);
throw new Interop.NetSecurityNative.GssApiException(Interop.NetSecurityNative.Status.GSS_S_NO_CRED, 0, SR.net_ntlm_not_possible_default_cred);
}
if (string.IsNullOrEmpty(_spn))
{
throw new PlatformNotSupportedException(SR.net_nego_not_supported_empty_target_with_defaultcreds);
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, SR.net_nego_not_supported_empty_target_with_defaultcreds);
throw new Interop.NetSecurityNative.GssApiException(Interop.NetSecurityNative.Status.GSS_S_BAD_NAME, 0, SR.net_nego_not_supported_empty_target_with_defaultcreds);
}

_credentialsHandle = SafeGssCredHandle.Create(string.Empty, string.Empty, _packageType);
}
else
{
Expand Down Expand Up @@ -229,7 +236,7 @@ public UnixNegotiateAuthenticationPal(NegotiateAuthenticationServerOptions serve

if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Peer SPN-> '{_spn}'");

if (serverOptions.Credential == CredentialCache.DefaultCredentials ||
if (serverOptions.Credential == CredentialCache.DefaultNetworkCredentials ||
string.IsNullOrWhiteSpace(serverOptions.Credential.UserName) ||
string.IsNullOrWhiteSpace(serverOptions.Credential.Password))
{
Expand Down Expand Up @@ -462,24 +469,7 @@ private static Interop.NetSecurityNative.PackageType GetPackageType(string packa
else
{
// Native shim currently supports only NTLM, Negotiate and Kerberos
throw new PlatformNotSupportedException(SR.net_securitypackagesupport);
}
}

private SafeGssCredHandle AcquireDefaultCredential()
{
try
{
return SafeGssCredHandle.Create(string.Empty, string.Empty, _packageType);
}
catch (Exception ex)
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, ex);

// NOTE: We throw PlatformNotSupportedException which is caught in
// NegotiateAuthenticationPal.Create and transformed into instantiation of
// UnsupportedNegotiateAuthenticationPal.
throw new PlatformNotSupportedException(ex.Message, ex);
throw new Interop.NetSecurityNative.GssApiException(Interop.NetSecurityNative.Status.GSS_S_UNAVAILABLE, 0);
}
}

Expand Down Expand Up @@ -511,14 +501,10 @@ private SafeGssCredHandle AcquireCredentialsHandle(NetworkCredential credential)

return SafeGssCredHandle.Create(username, password, _packageType);
}
catch (Exception ex)
catch (Exception ex) when (ex is not Interop.NetSecurityNative.GssApiException)
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, ex);

// NOTE: We throw PlatformNotSupportedException which is caught in
// NegotiateAuthenticationPal.Create and transformed into instantiation of
// UnsupportedNegotiateAuthenticationPal.
throw new PlatformNotSupportedException(ex.Message, ex);
throw new Interop.NetSecurityNative.GssApiException(Interop.NetSecurityNative.Status.GSS_S_BAD_NAME, 0);
}
}

Expand Down Expand Up @@ -753,7 +739,7 @@ private NegotiateAuthenticationStatusCode AcceptSecurityContext(
}

// https://www.gnu.org/software/gss/reference/gss.pdf (page 25)
private static NegotiateAuthenticationStatusCode GetErrorCode(Interop.NetSecurityNative.GssApiException exception)
internal static NegotiateAuthenticationStatusCode GetErrorCode(Interop.NetSecurityNative.GssApiException exception)
{
switch (exception.MajorStatus)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ internal sealed class UnsupportedNegotiateAuthenticationPal : NegotiateAuthentic
{
private string _package;
private string? _targetName;
private NegotiateAuthenticationStatusCode _statusCode;

public override bool IsAuthenticated => false;
public override bool IsSigned => false;
Expand All @@ -25,15 +26,17 @@ internal sealed class UnsupportedNegotiateAuthenticationPal : NegotiateAuthentic
public override IIdentity RemoteIdentity => throw new InvalidOperationException();
public override System.Security.Principal.TokenImpersonationLevel ImpersonationLevel => System.Security.Principal.TokenImpersonationLevel.Impersonation;

public UnsupportedNegotiateAuthenticationPal(NegotiateAuthenticationClientOptions clientOptions)
public UnsupportedNegotiateAuthenticationPal(NegotiateAuthenticationClientOptions clientOptions, NegotiateAuthenticationStatusCode statusCode = NegotiateAuthenticationStatusCode.Unsupported)
{
_package = clientOptions.Package;
_targetName = clientOptions.TargetName;
_statusCode = statusCode;
}

public UnsupportedNegotiateAuthenticationPal(NegotiateAuthenticationServerOptions serverOptions)
public UnsupportedNegotiateAuthenticationPal(NegotiateAuthenticationServerOptions serverOptions, NegotiateAuthenticationStatusCode statusCode = NegotiateAuthenticationStatusCode.Unsupported)
{
_package = serverOptions.Package;
_statusCode = statusCode;
}

public override void Dispose()
Expand All @@ -42,7 +45,7 @@ public override void Dispose()

public override byte[]? GetOutgoingBlob(ReadOnlySpan<byte> incomingBlob, out NegotiateAuthenticationStatusCode statusCode)
{
statusCode = NegotiateAuthenticationStatusCode.Unsupported;
statusCode = _statusCode;
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ public void Package_Unsupported_NTLM()
Assert.Equal(NegotiateAuthenticationStatusCode.Unsupported, statusCode);
}

[Fact]
[SkipOnPlatform(TestPlatforms.Windows, "The test is specific to GSSAPI / Managed implementations of NegotiateAuthentication")]
public void DefaultNetworkCredentials_NTLM_DoesNotThrow()
{
NegotiateAuthenticationClientOptions clientOptions = new NegotiateAuthenticationClientOptions { Package = "NTLM", Credential = CredentialCache.DefaultNetworkCredentials, TargetName = "HTTP/foo" };
// Assert.DoesNotThrow
NegotiateAuthentication negotiateAuthentication = new NegotiateAuthentication(clientOptions);
NegotiateAuthenticationStatusCode statusCode;
negotiateAuthentication.GetOutgoingBlob((byte[]?)null, out statusCode);
Assert.Equal(NegotiateAuthenticationStatusCode.UnknownCredentials, statusCode);
}

[Fact]
public void NtlmProtocolExampleTest()
{
Expand Down