parent
dda84bc7fc
commit
6e25a9fc53
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue