Skip to content

Commit

Permalink
Report UnknownCredentials status on Unix/Managed NegotiateAuthenticat…
Browse files Browse the repository at this point in the history
…ionPal implementation for NTLM w/ default credentials. (#91160)

This was handled inconsistently between the managed NTLM implementation and the GSSAPI one.
Add test for the behavior.
Add test to ensure SocketsHttpHandler using CredentialCache.DefaultCredentials with NTLM doesn't throw PNSE exception and returns the Unauthorized HTTP response instead.
  • Loading branch information
filipnavara authored Aug 30, 2023
1 parent c4ebb99 commit b715349
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 66 deletions.
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) ||
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

0 comments on commit b715349

Please sign in to comment.