Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate;
// For testing
internal interface INegotiateStateFactory
{
INegotiateState CreateInstance();
INegotiateState CreateInstance(ReadOnlyMemory<byte> channelBindingToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.InteropServices;
using System.Security.Authentication.ExtendedProtection;

namespace Microsoft.AspNetCore.Authentication.Negotiate;

internal sealed class NegotiateChannelBinding : ChannelBinding
{
public unsafe NegotiateChannelBinding(ReadOnlyMemory<byte> channelBindingToken)
{
// ITlsConnectionFeature exposes managed bytes, but NegotiateAuthentication requires
// a ChannelBinding handle that remains valid throughout the authentication exchange.
Size = channelBindingToken.Length;
SetHandle(Marshal.AllocHGlobal(Size));
using var pinnedToken = channelBindingToken.Pin();
Buffer.MemoryCopy(pinnedToken.Pointer, (void*)handle, Size, Size);
}

public override int Size { get; }

protected override bool ReleaseHandle()
{
Marshal.FreeHGlobal(handle);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Security;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Claims;
using System.Security.Principal;

Expand All @@ -10,11 +11,26 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate;
internal sealed class NegotiateState : INegotiateState
{
private static readonly NegotiateAuthenticationServerOptions _serverOptions = new();
Comment thread
DeagleGross marked this conversation as resolved.
private readonly ChannelBinding? _channelBinding;
private readonly NegotiateAuthentication _instance;

public NegotiateState()
public NegotiateState(ReadOnlyMemory<byte> channelBindingToken)
{
_instance = new NegotiateAuthentication(_serverOptions);
_channelBinding = channelBindingToken.IsEmpty ? null : new NegotiateChannelBinding(channelBindingToken);

try
{
var serverOptions = _channelBinding is null
? _serverOptions
: new NegotiateAuthenticationServerOptions { Binding = _channelBinding };
_instance = new NegotiateAuthentication(serverOptions);
}
catch
{
// NegotiateAuthentication construction can fail after the binding has been allocated.
_channelBinding?.Dispose();
throw;
}
}

public string? GetOutgoingBlob(string incomingBlob, out BlobErrorType status, out Exception? error)
Expand Down Expand Up @@ -65,7 +81,14 @@ public IIdentity GetIdentity()

public void Dispose()
{
_instance.Dispose();
try
{
_instance.Dispose();
}
finally
{
_channelBinding?.Dispose();
}
}

private static bool IsCredentialError(NegotiateAuthenticationStatusCode error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate;

internal sealed class NegotiateStateFactory : INegotiateStateFactory
{
public INegotiateState CreateInstance()
public INegotiateState CreateInstance(ReadOnlyMemory<byte> channelBindingToken)
{
return new NegotiateState();
return new NegotiateState(channelBindingToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;authentication;security</PackageTags>
<IsTrimmable>true</IsTrimmable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
Expand Down
16 changes: 15 additions & 1 deletion src/Security/Authentication/Negotiate/src/NegotiateHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

using System.Diagnostics;
using System.Linq;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Claims;
using System.Security.Principal;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
Expand Down Expand Up @@ -129,7 +131,7 @@ public async Task<bool> HandleRequestAsync()
persistence?.State = null;
}

_negotiateState ??= Options.StateFactory.CreateInstance();
_negotiateState ??= Options.StateFactory.CreateInstance(GetChannelBindingToken());

var outgoing = _negotiateState.GetOutgoingBlob(token, out var errorType, out var exception);
if (errorType != BlobErrorType.None)
Expand Down Expand Up @@ -408,6 +410,18 @@ private AuthPersistence EstablishConnectionPersistence(IDictionary<object, objec
?? throw new NotSupportedException($"Negotiate authentication requires a server that supports {nameof(IConnectionItemsFeature)} like Kestrel.");
}

private ReadOnlyMemory<byte> GetChannelBindingToken()
{
if (Request.IsHttps &&
Context.Features.Get<ITlsConnectionFeature>() is { } tlsConnectionFeature &&
tlsConnectionFeature.TryGetChannelBindingBytes(ChannelBindingKind.Endpoint, out var channelBindingToken))
{
return channelBindingToken;
}

return default;
}

private void RegisterForConnectionDispose(IDisposable authState)
{
var connectionCompleteFeature = Context.Features.Get<IConnectionCompleteFeature>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ public void OnCompleted(Func<object, Task> callback, object state)

private class TestNegotiateStateFactory : INegotiateStateFactory
{
public INegotiateState CreateInstance() => new TestNegotiateState();
public INegotiateState CreateInstance(ReadOnlyMemory<byte> channelBindingToken) => new TestNegotiateState();
}

private class TestNegotiateState : INegotiateState
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.InteropServices;

namespace Microsoft.AspNetCore.Authentication.Negotiate;

public class NegotiateChannelBindingTests
{
[Fact]
public void Constructor_CopiesChannelBindingToken()
{
var channelBindingToken = new byte[] { 0x01, 0x23, 0x45, 0x67 };

using var channelBinding = new NegotiateChannelBinding(channelBindingToken);
channelBindingToken[0] = 0xff;
var copiedToken = new byte[channelBinding.Size];
Marshal.Copy(channelBinding.DangerousGetHandle(), copiedToken, 0, copiedToken.Length);

Assert.Equal(new byte[] { 0x01, 0x23, 0x45, 0x67 }, copiedToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security.Authentication.ExtendedProtection;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Caching.Memory;
Expand Down Expand Up @@ -123,6 +126,74 @@ public async Task NtlmStage1And2Auth_Success(bool persistNtlm)
await NtlmStage1And2Auth(server, testConnection);
}

[Fact]
public async Task NtlmStage1And2Auth_HttpsEndpointChannelBinding_UsesSingleStateAndReadsChannelBindingOnce()
{
var expectedToken = new byte[] { 0x01, 0x23, 0x45, 0x67 };
var factory = new TestNegotiateStateFactory();
using var host = await CreateHostAsync(options => options.StateFactory = factory);
var server = host.GetTestServer();
var connection = new TestConnection
{
IsHttps = true,
HasTlsConnectionFeature = true,
ChannelBindingAvailable = true,
ChannelBindingToken = expectedToken,
};

await NtlmStage1Auth(server, connection);

Assert.Equal(ChannelBindingKind.Endpoint, Assert.Single(connection.RequestedKinds));
Assert.Equal(expectedToken, Assert.Single(factory.ChannelBindingTokens).ToArray());
Assert.Equal(1, factory.CreateCount);
Assert.Single(factory.CreatedStates);

connection.ChannelBindingAvailable = false;
connection.ChannelBindingToken = new byte[] { 0x89 };
await NtlmStage2Auth(server, connection);

Assert.Single(factory.ChannelBindingTokens);
Assert.Equal(1, factory.CreateCount);
Assert.Single(factory.CreatedStates);
Assert.Equal(1, connection.ChannelBindingReadCount);
Assert.Equal(expectedToken, factory.ChannelBindingTokens[0].ToArray());
}

[Theory]
[InlineData(true, false, false, 0)]
[InlineData(true, true, false, 1)]
[InlineData(false, true, true, 0)]
public async Task NtlmStage1Auth_NoUsableChannelBinding_CreatesStateWithEmptyToken(
bool isHttps,
bool hasTlsConnectionFeature,
bool channelBindingAvailable,
int expectedTlsReads)
{
var factory = new TestNegotiateStateFactory();
using var host = await CreateHostAsync(options => options.StateFactory = factory);
var server = host.GetTestServer();
var connection = new TestConnection
{
IsHttps = isHttps,
HasTlsConnectionFeature = hasTlsConnectionFeature,
ChannelBindingAvailable = channelBindingAvailable,
ChannelBindingToken = new byte[] { 0x01 },
};

await NtlmStage1Auth(server, connection);

Assert.Single(factory.ChannelBindingTokens);
Assert.True(factory.ChannelBindingTokens[0].IsEmpty);
Assert.Equal(1, factory.CreateCount);
Assert.Single(factory.CreatedStates);
Assert.Equal(expectedTlsReads, connection.ChannelBindingReadCount);
Assert.Equal(expectedTlsReads, connection.RequestedKinds.Count);
if (expectedTlsReads == 1)
{
Assert.Equal(ChannelBindingKind.Endpoint, connection.RequestedKinds[0]);
}
}

[Theory]
[InlineData(false)]
[InlineData(true)]
Expand Down Expand Up @@ -498,22 +569,59 @@ private static Task<HttpContext> SendAsync(TestServer server, string path, TestC
{
context.Features.Set<IConnectionItemsFeature>(connection);
context.Features.Set<IConnectionCompleteFeature>(connection);
if (connection.IsHttps)
{
context.Request.Scheme = "https";
}
if (connection.HasTlsConnectionFeature)
{
context.Features.Set<ITlsConnectionFeature>(connection);
}
}
});
}

private class TestConnection : IConnectionItemsFeature, IConnectionCompleteFeature
private class TestConnection : IConnectionItemsFeature, IConnectionCompleteFeature, ITlsConnectionFeature
{
public IDictionary<object, object> Items { get; set; } = new ConnectionItems();
public bool IsHttps { get; set; }
public bool HasTlsConnectionFeature { get; set; }
public bool ChannelBindingAvailable { get; set; }
public ReadOnlyMemory<byte> ChannelBindingToken { get; set; }
public int ChannelBindingReadCount { get; private set; }
public List<ChannelBindingKind> RequestedKinds { get; } = new();
public X509Certificate2 ClientCertificate { get; set; }

public void OnCompleted(Func<object, Task> callback, object state)
{
}

public Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken)
=> Task.FromResult(ClientCertificate);

public bool TryGetChannelBindingBytes(ChannelBindingKind kind, out ReadOnlyMemory<byte> channelBindingToken)
{
ChannelBindingReadCount++;
RequestedKinds.Add(kind);
channelBindingToken = ChannelBindingAvailable ? ChannelBindingToken : default;
return ChannelBindingAvailable;
}
}

private class TestNegotiateStateFactory : INegotiateStateFactory
{
public INegotiateState CreateInstance() => new TestNegotiateState();
public int CreateCount { get; private set; }
public List<ReadOnlyMemory<byte>> ChannelBindingTokens { get; } = new();
public List<TestNegotiateState> CreatedStates { get; } = new();

public INegotiateState CreateInstance(ReadOnlyMemory<byte> channelBindingToken)
{
CreateCount++;
ChannelBindingTokens.Add(channelBindingToken.ToArray());
var state = new TestNegotiateState();
CreatedStates.Add(state);
return state;
}
}

private class TestNegotiateState : INegotiateState
Expand Down
Loading