From f2d7525755954df1282cfde2d2b1e5433a7b5e6e Mon Sep 17 00:00:00 2001 From: Filip Navara Date: Fri, 25 Aug 2023 23:43:48 +0200 Subject: [PATCH] Report UnknownCredentials status on Unix/Managed NegotiateAuthenticationPal implementation for NTLM w/ default credentials. 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. --- .../FunctionalTests/NtAuthTests.FakeServer.cs | 28 ++++++++ .../Net/NegotiateAuthenticationPal.Managed.cs | 2 +- .../NegotiateAuthenticationPal.ManagedNtlm.cs | 26 ++++--- ...egotiateAuthenticationPal.ManagedSpnego.cs | 11 +-- .../Net/NegotiateAuthenticationPal.Unix.cs | 72 ++++++++----------- .../NegotiateAuthenticationPal.Unsupported.cs | 9 ++- .../UnitTests/NegotiateAuthenticationTests.cs | 12 ++++ 7 files changed, 94 insertions(+), 66 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs index 780db637ba335e..e0e08021cd8669 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs @@ -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); + }); + } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Managed.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Managed.cs index 4e5e8906b795cc..6f4047e98684e8 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Managed.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Managed.cs @@ -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); diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs index 866a754af72922..95be2c7fc8984e 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs @@ -218,17 +218,9 @@ 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; @@ -236,6 +228,22 @@ public ManagedNtlmNegotiateAuthenticationPal(NegotiateAuthenticationClientOption 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 diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedSpnego.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedSpnego.cs index 9721833914b206..4b94b32b02a237 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedSpnego.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedSpnego.cs @@ -303,16 +303,7 @@ private IEnumerable> 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; diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unix.cs index 776a50901411b9..610a6939e8123b 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unix.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unix.cs @@ -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); @@ -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) { @@ -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) { @@ -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 { @@ -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)) { @@ -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); } } @@ -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); } } @@ -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) { diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unsupported.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unsupported.cs index 7e6498832f738c..85f3f01d55da87 100644 --- a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unsupported.cs +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unsupported.cs @@ -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; @@ -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() @@ -42,7 +45,7 @@ public override void Dispose() public override byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateAuthenticationStatusCode statusCode) { - statusCode = NegotiateAuthenticationStatusCode.Unsupported; + statusCode = _statusCode; return null; } diff --git a/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs b/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs index 3e569b1153e853..22c14cd0e2440d 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs +++ b/src/libraries/System.Net.Security/tests/UnitTests/NegotiateAuthenticationTests.cs @@ -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() {