diff --git a/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs index 1643abe614..2e62846f8f 100644 --- a/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs +++ b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs @@ -20,7 +20,17 @@ namespace NegotiateAuthSample options.FallbackPolicy = options.DefaultPolicy; }); services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) - .AddNegotiate(); + .AddNegotiate(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + // context.SkipHandler(); + return Task.CompletedTask; + } + }; + }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/src/Security/Authentication/Negotiate/src/Internal/INegotiateState.cs b/src/Security/Authentication/Negotiate/src/Internal/INegotiateState.cs index dbc215ef7d..d2eb518c5b 100644 --- a/src/Security/Authentication/Negotiate/src/Internal/INegotiateState.cs +++ b/src/Security/Authentication/Negotiate/src/Internal/INegotiateState.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate // For testing internal interface INegotiateState : IDisposable { - string GetOutgoingBlob(string incomingBlob); + string GetOutgoingBlob(string incomingBlob, out BlobErrorType status, out Exception error); bool IsCompleted { get; } @@ -17,4 +17,12 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate IIdentity GetIdentity(); } + + internal enum BlobErrorType + { + None, + CredentialError, + ClientError, + Other + } } diff --git a/src/Security/Authentication/Negotiate/src/Internal/NegotiateLoggingExtensions.cs b/src/Security/Authentication/Negotiate/src/Internal/NegotiateLoggingExtensions.cs index 11141bb123..b39ec8cf7b 100644 --- a/src/Security/Authentication/Negotiate/src/Internal/NegotiateLoggingExtensions.cs +++ b/src/Security/Authentication/Negotiate/src/Internal/NegotiateLoggingExtensions.cs @@ -12,6 +12,8 @@ namespace Microsoft.Extensions.Logging private static Action _enablingCredentialPersistence; private static Action _disablingCredentialPersistence; private static Action _exceptionProcessingAuth; + private static Action _credentialError; + private static Action _clientError; private static Action _challengeNegotiate; private static Action _reauthenticating; private static Action _deferring; @@ -50,6 +52,14 @@ namespace Microsoft.Extensions.Logging eventId: new EventId(8, "Deferring"), logLevel: LogLevel.Information, formatString: "Deferring to the server's implementation of Windows Authentication."); + _credentialError = LoggerMessage.Define( + eventId: new EventId(9, "CredentialError"), + logLevel: LogLevel.Debug, + formatString: "There was a problem with the users credentials."); + _clientError = LoggerMessage.Define( + eventId: new EventId(10, "ClientError"), + logLevel: LogLevel.Debug, + formatString: "The users authentication request was invalid."); } public static void IncompleteNegotiateChallenge(this ILogger logger) @@ -75,5 +85,11 @@ namespace Microsoft.Extensions.Logging public static void Deferring(this ILogger logger) => _deferring(logger, null); + + public static void CredentialError(this ILogger logger, Exception ex) + => _credentialError(logger, ex); + + public static void ClientError(this ILogger logger, Exception ex) + => _clientError(logger, ex); } } diff --git a/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateState.cs b/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateState.cs index 37a1dff6f0..fb7a6a3a9f 100644 --- a/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateState.cs +++ b/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateState.cs @@ -5,6 +5,8 @@ using System; using System.Linq; using System.Net; using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; using System.Security.Authentication; using System.Security.Principal; @@ -12,21 +14,30 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate { internal class ReflectedNegotiateState : INegotiateState { + // https://www.gnu.org/software/gss/reference/gss.pdf + private const uint GSS_S_NO_CRED = 7 << 16; + private static readonly ConstructorInfo _constructor; private static readonly MethodInfo _getOutgoingBlob; private static readonly MethodInfo _isCompleted; private static readonly MethodInfo _protocol; private static readonly MethodInfo _getIdentity; private static readonly MethodInfo _closeContext; + private static readonly FieldInfo _statusCode; + private static readonly FieldInfo _statusException; + private static readonly MethodInfo _getException; + private static readonly FieldInfo _gssMinorStatus; + private static readonly Type _gssExceptionType; private readonly object _instance; static ReflectedNegotiateState() { - var ntAuthType = typeof(AuthenticationException).Assembly.GetType("System.Net.NTAuthentication"); + var secAssembly = typeof(AuthenticationException).Assembly; + var ntAuthType = secAssembly.GetType("System.Net.NTAuthentication", throwOnError: true); _constructor = ntAuthType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).First(); _getOutgoingBlob = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => - info.Name.Equals("GetOutgoingBlob") && info.GetParameters().Count() == 2).Single(); + info.Name.Equals("GetOutgoingBlob") && info.GetParameters().Count() == 3).Single(); _isCompleted = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => info.Name.Equals("get_IsCompleted")).Single(); _protocol = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => @@ -34,9 +45,23 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate _closeContext = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => info.Name.Equals("CloseContext")).Single(); - var negoStreamPalType = typeof(AuthenticationException).Assembly.GetType("System.Net.Security.NegotiateStreamPal"); + var securityStatusType = secAssembly.GetType("System.Net.SecurityStatusPal", throwOnError: true); + _statusCode = securityStatusType.GetField("ErrorCode"); + _statusException = securityStatusType.GetField("Exception"); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var interopType = secAssembly.GetType("Interop", throwOnError: true); + var netNativeType = interopType.GetNestedType("NetSecurityNative", BindingFlags.NonPublic | BindingFlags.Static); + _gssExceptionType = netNativeType.GetNestedType("GssApiException", BindingFlags.NonPublic); + _gssMinorStatus = _gssExceptionType.GetField("_minorStatus", BindingFlags.Instance | BindingFlags.NonPublic); + } + + var negoStreamPalType = secAssembly.GetType("System.Net.Security.NegotiateStreamPal", throwOnError: true); _getIdentity = negoStreamPalType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(info => info.Name.Equals("GetIdentity")).Single(); + _getException = negoStreamPalType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(info => + info.Name.Equals("CreateExceptionFromError")).Single(); } public ReflectedNegotiateState() @@ -50,14 +75,15 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate // The client doesn't need the context once auth is complete, but the server does. // I'm not sure why it auto-closes for the client given that the client closes it just a few lines later. // https://github.com/dotnet/corefx/blob/a3ab91e10045bb298f48c1d1f9bd5b0782a8ac46/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs#L134 - public string GetOutgoingBlob(string incomingBlob) + public string GetOutgoingBlob(string incomingBlob, out BlobErrorType status, out Exception error) { byte[] decodedIncomingBlob = null; if (incomingBlob != null && incomingBlob.Length > 0) { decodedIncomingBlob = Convert.FromBase64String(incomingBlob); } - byte[] decodedOutgoingBlob = GetOutgoingBlob(decodedIncomingBlob, true); + + byte[] decodedOutgoingBlob = GetOutgoingBlob(decodedIncomingBlob, out status, out error); string outgoingBlob = null; if (decodedOutgoingBlob != null && decodedOutgoingBlob.Length > 0) @@ -68,9 +94,65 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate return outgoingBlob; } - private byte[] GetOutgoingBlob(byte[] incomingBlob, bool thrownOnError) + private byte[] GetOutgoingBlob(byte[] incomingBlob, out BlobErrorType status, out Exception error) { - return (byte[])_getOutgoingBlob.Invoke(_instance, new object[] { incomingBlob, thrownOnError }); + try + { + // byte[] GetOutgoingBlob(byte[] incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) + var parameters = new object[] { incomingBlob, false, null }; + var blob = (byte[])_getOutgoingBlob.Invoke(_instance, parameters); + + var securityStatus = parameters[2]; + // TODO: Update after corefx changes + error = (Exception)(_statusException.GetValue(securityStatus) + ?? _getException.Invoke(null, new[] { securityStatus })); + var errorCode = (SecurityStatusPalErrorCode)_statusCode.GetValue(securityStatus); + + // TODO: Remove after corefx changes + // The linux implementation always uses InternalError; + if (errorCode == SecurityStatusPalErrorCode.InternalError + && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + && _gssExceptionType.IsInstanceOfType(error)) + { + var majorStatus = (uint)error.HResult; + var minorStatus = (uint)_gssMinorStatus.GetValue(error); + + // Remap specific errors + if (majorStatus == GSS_S_NO_CRED && minorStatus == 0) + { + errorCode = SecurityStatusPalErrorCode.UnknownCredentials; + } + + error = new Exception($"An authentication exception occured (0x{majorStatus:X}/0x{minorStatus:X}).", error); + } + + if (errorCode == SecurityStatusPalErrorCode.OK + || errorCode == SecurityStatusPalErrorCode.ContinueNeeded + || errorCode == SecurityStatusPalErrorCode.CompleteNeeded) + { + status = BlobErrorType.None; + } + else if (IsCredentialError(errorCode)) + { + status = BlobErrorType.CredentialError; + } + else if (IsClientError(errorCode)) + { + status = BlobErrorType.ClientError; + } + else + { + status = BlobErrorType.Other; + } + + return blob; + } + catch (TargetInvocationException tex) + { + // Unwrap + ExceptionDispatchInfo.Capture(tex.InnerException).Throw(); + throw; + } } public bool IsCompleted @@ -92,5 +174,36 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate { _closeContext.Invoke(_instance, Array.Empty()); } + + private bool IsCredentialError(SecurityStatusPalErrorCode error) + { + return error == SecurityStatusPalErrorCode.LogonDenied || + error == SecurityStatusPalErrorCode.UnknownCredentials || + error == SecurityStatusPalErrorCode.NoImpersonation || + error == SecurityStatusPalErrorCode.NoAuthenticatingAuthority || + error == SecurityStatusPalErrorCode.UntrustedRoot || + error == SecurityStatusPalErrorCode.CertExpired || + error == SecurityStatusPalErrorCode.SmartcardLogonRequired || + error == SecurityStatusPalErrorCode.BadBinding; + } + + private bool IsClientError(SecurityStatusPalErrorCode error) + { + return error == SecurityStatusPalErrorCode.InvalidToken || + error == SecurityStatusPalErrorCode.CannotPack || + error == SecurityStatusPalErrorCode.QopNotSupported || + error == SecurityStatusPalErrorCode.NoCredentials || + error == SecurityStatusPalErrorCode.MessageAltered || + error == SecurityStatusPalErrorCode.OutOfSequence || + error == SecurityStatusPalErrorCode.IncompleteMessage || + error == SecurityStatusPalErrorCode.IncompleteCredentials || + error == SecurityStatusPalErrorCode.WrongPrincipal || + error == SecurityStatusPalErrorCode.TimeSkew || + error == SecurityStatusPalErrorCode.IllegalMessage || + error == SecurityStatusPalErrorCode.CertUnknown || + error == SecurityStatusPalErrorCode.AlgorithmMismatch || + error == SecurityStatusPalErrorCode.SecurityQosFailed || + error == SecurityStatusPalErrorCode.UnsupportedPreauth; + } } } diff --git a/src/Security/Authentication/Negotiate/src/Internal/SecurityStatusPalErrorCode.cs b/src/Security/Authentication/Negotiate/src/Internal/SecurityStatusPalErrorCode.cs new file mode 100644 index 0000000000..f89a996e7e --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Internal/SecurityStatusPalErrorCode.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + internal enum SecurityStatusPalErrorCode + { + NotSet = 0, + OK, + ContinueNeeded, + CompleteNeeded, + CompAndContinue, + ContextExpired, + CredentialsNeeded, + Renegotiate, + + // Errors + OutOfMemory, + InvalidHandle, + Unsupported, + TargetUnknown, + InternalError, + PackageNotFound, + NotOwner, + CannotInstall, + InvalidToken, + CannotPack, + QopNotSupported, + NoImpersonation, + LogonDenied, + UnknownCredentials, + NoCredentials, + MessageAltered, + OutOfSequence, + NoAuthenticatingAuthority, + IncompleteMessage, + IncompleteCredentials, + BufferNotEnough, + WrongPrincipal, + TimeSkew, + UntrustedRoot, + IllegalMessage, + CertUnknown, + CertExpired, + DecryptFailure, + AlgorithmMismatch, + SecurityQosFailed, + SmartcardLogonRequired, + UnsupportedPreauth, + BadBinding, + DowngradeDetected, + ApplicationProtocolMismatch + } +} diff --git a/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs b/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs index e49cd5cd97..c880fb1564 100644 --- a/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs +++ b/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs @@ -65,6 +65,8 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate /// True if a response was generated, false otherwise. public async Task HandleRequestAsync() { + AuthPersistence persistence = null; + bool authFailedEventCalled = false; try { if (_requestProcessed || Options.DeferToServer) @@ -86,7 +88,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate } var connectionItems = GetConnectionItems(); - var persistence = (AuthPersistence)connectionItems[AuthPersistenceKey]; + persistence = (AuthPersistence)connectionItems[AuthPersistenceKey]; _negotiateState = persistence?.State; var authorizationHeader = Request.Headers[HeaderNames.Authorization]; @@ -126,7 +128,40 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate _negotiateState ??= Options.StateFactory.CreateInstance(); - var outgoing = _negotiateState.GetOutgoingBlob(token); + var outgoing = _negotiateState.GetOutgoingBlob(token, out var errorType, out var exception); + Logger.LogInformation(errorType.ToString()); + if (errorType != BlobErrorType.None) + { + _negotiateState.Dispose(); + _negotiateState = null; + if (persistence?.State != null) + { + persistence.State.Dispose(); + persistence.State = null; + } + + if (errorType == BlobErrorType.CredentialError) + { + Logger.CredentialError(exception); + authFailedEventCalled = true; // Could throw, and we don't want to double trigger the event. + var result = await InvokeAuthenticateFailedEvent(exception); + return result ?? false; // Default to skipping the handler, let AuthZ generate a new 401 + } + else if (errorType == BlobErrorType.ClientError) + { + Logger.ClientError(exception); + authFailedEventCalled = true; // Could throw, and we don't want to double trigger the event. + var result = await InvokeAuthenticateFailedEvent(exception); + if (result.HasValue) + { + return result.Value; + } + Context.Response.StatusCode = StatusCodes.Status400BadRequest; + return true; // Default to terminating request + } + + throw exception; + } if (!_negotiateState.IsCompleted) { @@ -193,24 +228,26 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate } catch (Exception ex) { - Logger.ExceptionProcessingAuth(ex); - var errorContext = new AuthenticationFailedContext(Context, Scheme, Options) { Exception = ex }; - await Events.AuthenticationFailed(errorContext); - - if (errorContext.Result != null) + if (authFailedEventCalled) { - if (errorContext.Result.Handled) - { - return true; - } - else if (errorContext.Result.Skipped) - { - return false; - } - else if (errorContext.Result.Failure != null) - { - throw new Exception("An error was returned from the AuthenticationFailed event.", errorContext.Result.Failure); - } + throw; + } + + Logger.ExceptionProcessingAuth(ex); + + // Clear state so it's possible to retry on the same connection. + _negotiateState?.Dispose(); + _negotiateState = null; + if (persistence?.State != null) + { + persistence.State.Dispose(); + persistence.State = null; + } + + var result = await InvokeAuthenticateFailedEvent(ex); + if (result.HasValue) + { + return result.Value; } throw; @@ -219,6 +256,30 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate return false; } + private async Task InvokeAuthenticateFailedEvent(Exception ex) + { + var errorContext = new AuthenticationFailedContext(Context, Scheme, Options) { Exception = ex }; + await Events.AuthenticationFailed(errorContext); + + if (errorContext.Result != null) + { + if (errorContext.Result.Handled) + { + return true; + } + else if (errorContext.Result.Skipped) + { + return false; + } + else if (errorContext.Result.Failure != null) + { + throw new Exception("An error was returned from the AuthenticationFailed event.", errorContext.Result.Failure); + } + } + + return null; + } + /// /// Checks if the current request is authenticated and returns the user. /// diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs b/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs index dc76aacb6e..6c3baef320 100644 --- a/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs +++ b/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Reflection.Metadata; using System.Security.Principal; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -71,16 +72,16 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate } [Fact] - public async Task OnAuthenticationFailed_Fires() + public async Task OnAuthenticationFailed_FromException_Fires() { - var eventInvoked = false; + var eventInvoked = 0; using var host = await CreateHostAsync(options => { options.Events = new NegotiateEvents() { OnAuthenticationFailed = context => { - eventInvoked = true; + eventInvoked++; Assert.IsType(context.Exception); Assert.Equal("InvalidBlob", context.Exception.Message); return Task.CompletedTask; @@ -92,11 +93,11 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate var ex = await Assert.ThrowsAsync(() => SendAsync(server, "/404", new TestConnection(), "Negotiate InvalidBlob")); Assert.Equal("InvalidBlob", ex.Message); - Assert.True(eventInvoked); + Assert.Equal(1, eventInvoked); } [Fact] - public async Task OnAuthenticationFailed_Handled() + public async Task OnAuthenticationFailed_FromException_Handled() { using var host = await CreateHostAsync(options => { @@ -104,7 +105,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate { OnAuthenticationFailed = context => { - context.Response.StatusCode = StatusCodes.Status418ImATeapot; ; + context.Response.StatusCode = StatusCodes.Status418ImATeapot; context.Response.Headers[HeaderNames.WWWAuthenticate] = "Teapot"; context.HandleResponse(); return Task.CompletedTask; @@ -118,6 +119,157 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate Assert.Equal("Teapot", result.Response.Headers[HeaderNames.WWWAuthenticate]); } + [Fact] + public async Task OnAuthenticationFailed_FromOtherBlobError_Fires() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + Assert.IsType(context.Exception); + Assert.Equal("A test other error occured", context.Exception.Message); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var ex = await Assert.ThrowsAsync(() => + SendAsync(server, "/404", new TestConnection(), "Negotiate OtherError")); + Assert.Equal("A test other error occured", ex.Message); + Assert.Equal(1, eventInvoked); + } + + [Fact] + public async Task OnAuthenticationFailed_FromOtherBlobError_Handled() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + context.Response.StatusCode = StatusCodes.Status418ImATeapot; + context.Response.Headers[HeaderNames.WWWAuthenticate] = "Teapot"; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var result = await SendAsync(server, "/404", new TestConnection(), "Negotiate OtherError"); + Assert.Equal(StatusCodes.Status418ImATeapot, result.Response.StatusCode); + Assert.Equal("Teapot", result.Response.Headers[HeaderNames.WWWAuthenticate]); + Assert.Equal(1, eventInvoked); + } + + [Fact] + public async Task OnAuthenticationFailed_FromCredentialError_Fires() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + Assert.IsType(context.Exception); + Assert.Equal("A test credential error occured", context.Exception.Message); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var response = await SendAsync(server, "/418", new TestConnection(), "Negotiate CredentialError"); + Assert.Equal(StatusCodes.Status418ImATeapot, response.Response.StatusCode); + Assert.Equal(1, eventInvoked); + } + + [Fact] + public async Task OnAuthenticationFailed_FromCredentialError_Handled() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + context.Response.StatusCode = StatusCodes.Status418ImATeapot; + context.Response.Headers[HeaderNames.WWWAuthenticate] = "Teapot"; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var result = await SendAsync(server, "/404", new TestConnection(), "Negotiate CredentialError"); + Assert.Equal(StatusCodes.Status418ImATeapot, result.Response.StatusCode); + Assert.Equal("Teapot", result.Response.Headers[HeaderNames.WWWAuthenticate]); + Assert.Equal(1, eventInvoked); + } + + [Fact] + public async Task OnAuthenticationFailed_FromClientError_Fires() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + Assert.IsType(context.Exception); + Assert.Equal("A test client error occured", context.Exception.Message); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var response = await SendAsync(server, "/404", new TestConnection(), "Negotiate ClientError"); + Assert.Equal(StatusCodes.Status400BadRequest, response.Response.StatusCode); + Assert.Equal(1, eventInvoked); + } + + [Fact] + public async Task OnAuthenticationFailed_FromClientError_Handled() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + context.Response.StatusCode = StatusCodes.Status418ImATeapot; + context.Response.Headers[HeaderNames.WWWAuthenticate] = "Teapot"; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var result = await SendAsync(server, "/404", new TestConnection(), "Negotiate ClientError"); + Assert.Equal(StatusCodes.Status418ImATeapot, result.Response.StatusCode); + Assert.Equal("Teapot", result.Response.Headers[HeaderNames.WWWAuthenticate]); + Assert.Equal(1, eventInvoked); + } + [Fact] public async Task OnAuthenticated_FiresOncePerRequest() { @@ -278,6 +430,12 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate Assert.False(string.IsNullOrEmpty(name), "name"); await context.Response.WriteAsync(name); }); + + builder.Map("/418", context => + { + context.Response.StatusCode = StatusCodes.Status418ImATeapot; + return Task.CompletedTask; + }); } private static Task SendAsync(TestServer server, string path, TestConnection connection, string authorizationHeader = null) @@ -352,7 +510,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate return new GenericIdentity("name", _protocol); } - public string GetOutgoingBlob(string incomingBlob) + public string GetOutgoingBlob(string incomingBlob, out BlobErrorType errorType, out Exception ex) { if (IsDisposed) { @@ -362,6 +520,10 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate { throw new InvalidOperationException("Authentication is already complete."); } + + errorType = BlobErrorType.None; + ex = null; + switch (incomingBlob) { case "ClientNtlmBlob1": @@ -391,8 +553,22 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate Assert.Equal("Kerberos", _protocol); IsCompleted = true; return "ServerKerberosBlob2"; + case "CredentialError": + errorType = BlobErrorType.CredentialError; + ex = new Exception("A test credential error occured"); + return null; + case "ClientError": + errorType = BlobErrorType.ClientError; + ex = new Exception("A test client error occured"); + return null; + case "OtherError": + errorType = BlobErrorType.Other; + ex = new Exception("A test other error occured"); + return null; default: - throw new InvalidOperationException(incomingBlob); + errorType = BlobErrorType.Other; + ex = new InvalidOperationException(incomingBlob); + return null; } } } diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs b/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs index b57a8e996d..d696cd0afd 100644 --- a/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs +++ b/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs @@ -271,6 +271,39 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate Assert.False(result.Response.Headers.ContainsKey(HeaderNames.WWWAuthenticate)); } + [Fact] + public async Task CredentialError_401() + { + using var host = await CreateHostAsync(); + var server = host.GetTestServer(); + var testConnection = new TestConnection(); + var result = await SendAsync(server, "/Authenticate", testConnection, "Negotiate CredentialError"); + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + [Fact] + public async Task ClientError_400() + { + using var host = await CreateHostAsync(); + var server = host.GetTestServer(); + var testConnection = new TestConnection(); + var result = await SendAsync(server, "/404", testConnection, "Negotiate ClientError"); + Assert.Equal(StatusCodes.Status400BadRequest, result.Response.StatusCode); + Assert.DoesNotContain(HeaderNames.WWWAuthenticate, result.Response.Headers); + } + + [Fact] + public async Task OtherError_Throws() + { + using var host = await CreateHostAsync(); + var server = host.GetTestServer(); + var testConnection = new TestConnection(); + + var ex = await Assert.ThrowsAsync(() => SendAsync(server, "/404", testConnection, "Negotiate OtherError")); + Assert.Equal("A test other error occured", ex.Message); + } + // Single Stage private static async Task KerberosAuth(TestServer server, TestConnection testConnection) { @@ -474,7 +507,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate return new GenericIdentity("name", _protocol); } - public string GetOutgoingBlob(string incomingBlob) + public string GetOutgoingBlob(string incomingBlob, out BlobErrorType errorType, out Exception ex) { if (IsDisposed) { @@ -484,6 +517,10 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate { throw new InvalidOperationException("Authentication is already complete."); } + + errorType = BlobErrorType.None; + ex = null; + switch (incomingBlob) { case "ClientNtlmBlob1": @@ -513,8 +550,22 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate Assert.Equal("Kerberos", _protocol); IsCompleted = true; return "ServerKerberosBlob2"; + case "CredentialError": + errorType = BlobErrorType.CredentialError; + ex = new Exception("A test credential error occured"); + return null; + case "ClientError": + errorType = BlobErrorType.ClientError; + ex = new Exception("A test client error occured"); + return null; + case "OtherError": + errorType = BlobErrorType.Other; + ex = new Exception("A test other error occured"); + return null; default: - throw new InvalidOperationException(incomingBlob); + errorType = BlobErrorType.Other; + ex = new InvalidOperationException(incomingBlob); + return null; } } }