Granular error handling for Negotiate #12566 (#12958)

This commit is contained in:
Chris Ross 2019-08-09 11:28:39 -07:00 committed by GitHub
parent dda84bc7fc
commit 6e25a9fc53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 527 additions and 38 deletions

View File

@ -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)

View File

@ -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
}
}

View File

@ -12,6 +12,8 @@ namespace Microsoft.Extensions.Logging
private static Action<ILogger, Exception> _enablingCredentialPersistence;
private static Action<ILogger, string, Exception> _disablingCredentialPersistence;
private static Action<ILogger, Exception> _exceptionProcessingAuth;
private static Action<ILogger, Exception> _credentialError;
private static Action<ILogger, Exception> _clientError;
private static Action<ILogger, Exception> _challengeNegotiate;
private static Action<ILogger, Exception> _reauthenticating;
private static Action<ILogger, Exception> _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);
}
}

View File

@ -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<object>());
}
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;
}
}
}

View File

@ -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
}
}

View File

@ -65,6 +65,8 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
/// <returns>True if a response was generated, false otherwise.</returns>
public async Task<bool> 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<bool?> 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;
}
/// <summary>
/// Checks if the current request is authenticated and returns the user.
/// </summary>

View File

@ -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<InvalidOperationException>(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<InvalidOperationException>(() =>
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<Exception>(context.Exception);
Assert.Equal("A test other error occured", context.Exception.Message);
return Task.CompletedTask;
}
};
});
var server = host.GetTestServer();
var ex = await Assert.ThrowsAsync<Exception>(() =>
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<Exception>(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<Exception>(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<HttpContext> 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;
}
}
}

View File

@ -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<Exception>(() => 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;
}
}
}