Authorization Support (Using ASP.NET Core Native AuthN/AuthZ Integration) (#377)

This commit is contained in:
Den Delimarsky 🌺 2025-07-03 14:39:28 -07:00 • committed by GitHub
commit cdf88e7f5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 4207 additions and 126 deletions

View file

@ -8,8 +8,6 @@ namespace ModelContextProtocol.AspNetCore.Tests.Utils;
public class KestrelInMemoryTest : LoggedTest
{
private readonly KestrelInMemoryTransport _inMemoryTransport = new();
public KestrelInMemoryTest(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
@ -17,17 +15,16 @@ public class KestrelInMemoryTest : LoggedTest
// or a helper that does the same every test. But clear out the existing socket transport to avoid potential port conflicts.
Builder = WebApplication.CreateSlimBuilder();
Builder.Services.RemoveAll<IConnectionListenerFactory>();
Builder.Services.AddSingleton<IConnectionListenerFactory>(_inMemoryTransport);
Builder.Services.AddSingleton<IConnectionListenerFactory>(KestrelInMemoryTransport);
Builder.Services.AddSingleton(XunitLoggerProvider);
HttpClient = new HttpClient(new SocketsHttpHandler
SocketsHttpHandler.ConnectCallback = (context, token) =>
{
ConnectCallback = (context, token) =>
{
var connection = _inMemoryTransport.CreateConnection();
return new(connection.ClientStream);
},
})
var connection = KestrelInMemoryTransport.CreateConnection(context.DnsEndPoint);
return new(connection.ClientStream);
};
HttpClient = new HttpClient(SocketsHttpHandler)
{
BaseAddress = new Uri("http://localhost:5000/"),
Timeout = TimeSpan.FromSeconds(10),
@ -38,6 +35,10 @@ public class KestrelInMemoryTest : LoggedTest
public HttpClient HttpClient { get; }
public SocketsHttpHandler SocketsHttpHandler { get; } = new();
public KestrelInMemoryTransport KestrelInMemoryTransport { get; } = new();
public override void Dispose()
{
HttpClient.Dispose();

View file

@ -1,50 +1,59 @@
using Microsoft.AspNetCore.Connections;
using System.Collections.Concurrent;
using System.Net;
using System.Threading.Channels;
namespace ModelContextProtocol.AspNetCore.Tests.Utils;
public sealed class KestrelInMemoryTransport : IConnectionListenerFactory, IConnectionListener
public sealed class KestrelInMemoryTransport : IConnectionListenerFactory
{
private readonly Channel<ConnectionContext> _acceptQueue = Channel.CreateUnbounded<ConnectionContext>();
private EndPoint? _endPoint;
// socket accept queues keyed by listen port.
private readonly ConcurrentDictionary<int, Channel<ConnectionContext>> _acceptQueues = [];
public EndPoint EndPoint => _endPoint ?? throw new InvalidOperationException("EndPoint is not set. Call BindAsync first.");
public KestrelInMemoryConnection CreateConnection()
public KestrelInMemoryConnection CreateConnection(EndPoint endpoint)
{
var connection = new KestrelInMemoryConnection();
_acceptQueue.Writer.TryWrite(connection);
GetAcceptQueue(endpoint).Writer.TryWrite(connection);
return connection;
}
public async ValueTask<ConnectionContext?> AcceptAsync(CancellationToken cancellationToken = default)
{
if (await _acceptQueue.Reader.WaitToReadAsync(cancellationToken))
public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) =>
new(new KestrelInMemoryListener(endpoint, GetAcceptQueue(endpoint)));
private Channel<ConnectionContext> GetAcceptQueue(EndPoint endpoint) =>
_acceptQueues.GetOrAdd(GetEndpointPort(endpoint), _ => Channel.CreateUnbounded<ConnectionContext>());
private static int GetEndpointPort(EndPoint endpoint) =>
endpoint switch
{
while (_acceptQueue.Reader.TryRead(out var item))
DnsEndPoint dnsEndpoint => dnsEndpoint.Port,
IPEndPoint ipEndpoint => ipEndpoint.Port,
_ => throw new InvalidOperationException($"Unexpected endpoint type: '{endpoint.GetType()}'"),
};
private sealed class KestrelInMemoryListener(EndPoint endpoint, Channel<ConnectionContext> acceptQueue) : IConnectionListener
{
public EndPoint EndPoint => endpoint;
public async ValueTask<ConnectionContext?> AcceptAsync(CancellationToken cancellationToken = default)
{
if (await acceptQueue.Reader.WaitToReadAsync(cancellationToken))
{
return item;
while (acceptQueue.Reader.TryRead(out var item))
{
return item;
}
}
return null;
}
return null;
}
public ValueTask UnbindAsync(CancellationToken cancellationToken = default)
{
acceptQueue.Writer.TryComplete();
return default;
}
public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default)
{
_endPoint = endpoint;
return new ValueTask<IConnectionListener>(this);
}
public ValueTask DisposeAsync()
{
return UnbindAsync(default);
}
public ValueTask UnbindAsync(CancellationToken cancellationToken = default)
{
_acceptQueue.Writer.TryComplete();
return default;
public ValueTask DisposeAsync() => UnbindAsync(CancellationToken.None);
}
}