Skip to content

Commit a0275ea

Browse files
committed
Update the HttpClient configuration
1 parent 8903c42 commit a0275ea

9 files changed

Lines changed: 115 additions & 37 deletions

File tree

Directory.Packages.props

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,30 @@
66
</PropertyGroup>
77
<!-- Product dependencies netstandard -->
88
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
9-
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0-preview.5.24272.6" />
9+
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
1010
<PackageVersion Include="Microsoft.Bcl.Memory" Version="9.0.4" />
1111
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
12+
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
1213
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
1314
<PackageVersion Include="System.IO.Pipelines" Version="8.0.0" />
1415
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
1516
<PackageVersion Include="System.Threading.Channels" Version="8.0.0" />
1617
</ItemGroup>
1718
<!-- Product dependencies LTS -->
1819
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
20+
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.15" />
1921
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
22+
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
2023
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
2124
<PackageVersion Include="System.IO.Pipelines" Version="8.0.0" />
2225
</ItemGroup>
2326
<!-- Product dependencies .NET 9 -->
2427
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
25-
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0-preview.5.24306.11" />
28+
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
2629
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
30+
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.4" />
2731
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
28-
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="7.4.1" />
32+
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.9.0" />
2933
<PackageVersion Include="System.IO.Pipelines" Version="9.0.4" />
3034
</ItemGroup>
3135
<!-- Product dependencies shared -->

samples/ProtectedMCPClient/BasicOAuthAuthorizationProvider.cs

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,54 @@ namespace ProtectedMCPClient;
1414
/// caching or any advanced token protection - it acquires a token and server metadata and holds it
1515
/// in memory as-is. This is NOT PRODUCTION READY and MUST NOT BE USED IN PRODUCTION.
1616
/// </summary>
17-
/// <remarks>
18-
/// Initializes a new instance of the <see cref="BasicOAuthAuthorizationProvider"/> class.
19-
/// </remarks>
20-
public class BasicOAuthAuthorizationProvider(
21-
Uri serverUrl,
22-
string clientId = "demo-client",
23-
string clientSecret = "",
24-
Uri? redirectUri = null,
25-
IEnumerable<string>? scopes = null) : IMcpAuthorizationProvider
17+
public class BasicOAuthAuthorizationProvider : ITokenProvider
2618
{
27-
private readonly Uri _serverUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl));
28-
private readonly Uri _redirectUri = redirectUri ?? new Uri("http://localhost:8080/callback");
29-
private readonly List<string> _scopes = scopes?.ToList() ?? [];
30-
private readonly HttpClient _httpClient = new();
19+
private readonly Uri _serverUrl;
20+
private readonly Uri _redirectUri;
21+
private readonly List<string> _scopes;
22+
private readonly string _clientId;
23+
private readonly string _clientSecret;
24+
private readonly HttpClient _httpClient;
25+
private readonly AuthorizationHelpers _authorizationHelpers;
26+
27+
// Client name for IHttpClientFactory used by the BasicOAuthAuthorizationProvider
28+
public const string HttpClientName = "ProtectedMCPClient.OAuth";
3129

3230
private TokenContainer? _token;
3331
private AuthorizationServerMetadata? _authServerMetadata;
3432

33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="BasicOAuthAuthorizationProvider"/> class.
35+
/// </summary>
36+
/// <param name="serverUrl">The MCP server URL.</param>
37+
/// <param name="httpClientFactory">The HTTP client factory to use for creating HTTP clients.</param>
38+
/// <param name="authorizationHelpers">The authorization helpers.</param>
39+
/// <param name="clientId">OAuth client ID.</param>
40+
/// <param name="clientSecret">OAuth client secret.</param>
41+
/// <param name="redirectUri">OAuth redirect URI.</param>
42+
/// <param name="scopes">OAuth scopes.</param>
43+
public BasicOAuthAuthorizationProvider(
44+
Uri serverUrl,
45+
IHttpClientFactory httpClientFactory,
46+
AuthorizationHelpers authorizationHelpers,
47+
string clientId = "demo-client",
48+
string clientSecret = "",
49+
Uri? redirectUri = null,
50+
IEnumerable<string>? scopes = null)
51+
{
52+
_serverUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl));
53+
if (httpClientFactory == null) throw new ArgumentNullException(nameof(httpClientFactory));
54+
_authorizationHelpers = authorizationHelpers ?? throw new ArgumentNullException(nameof(authorizationHelpers));
55+
56+
// Get the HttpClient once during construction instead of for each request
57+
_httpClient = httpClientFactory.CreateClient(HttpClientName);
58+
59+
_redirectUri = redirectUri ?? new Uri("http://localhost:8080/callback");
60+
_scopes = scopes?.ToList() ?? [];
61+
_clientId = clientId;
62+
_clientSecret = clientSecret;
63+
}
64+
3565
/// <inheritdoc />
3666
public IEnumerable<string> SupportedSchemes => new[] { "Bearer" };
3767

@@ -61,8 +91,8 @@ public class BasicOAuthAuthorizationProvider(
6191

6292
try
6393
{
64-
// Get the metadata from the challenge
65-
var resourceMetadata = await AuthorizationHelpers.ExtractProtectedResourceMetadata(
94+
// Get the metadata from the challenge using the instance-based AuthorizationHelpers
95+
var resourceMetadata = await _authorizationHelpers.ExtractProtectedResourceMetadata(
6696
response, _serverUrl, cancellationToken);
6797

6898
if (resourceMetadata?.AuthorizationServers?.Count > 0)
@@ -152,7 +182,7 @@ public class BasicOAuthAuthorizationProvider(
152182
{
153183
["grant_type"] = "refresh_token",
154184
["refresh_token"] = refreshToken,
155-
["client_id"] = clientId
185+
["client_id"] = _clientId
156186
});
157187

158188
try
@@ -162,9 +192,9 @@ public class BasicOAuthAuthorizationProvider(
162192
Content = requestContent
163193
};
164194

165-
if (!string.IsNullOrEmpty(clientSecret))
195+
if (!string.IsNullOrEmpty(_clientSecret))
166196
{
167-
var authValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}"));
197+
var authValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_clientId}:{_clientSecret}"));
168198
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", authValue);
169199
}
170200

@@ -213,7 +243,7 @@ public class BasicOAuthAuthorizationProvider(
213243
private Uri BuildAuthorizationUrl(AuthorizationServerMetadata authServerMetadata, string codeChallenge)
214244
{
215245
var queryParams = HttpUtility.ParseQueryString(string.Empty);
216-
queryParams["client_id"] = clientId;
246+
queryParams["client_id"] = _clientId;
217247
queryParams["redirect_uri"] = _redirectUri.ToString();
218248
queryParams["response_type"] = "code";
219249
queryParams["code_challenge"] = codeChallenge;
@@ -286,7 +316,7 @@ private Uri BuildAuthorizationUrl(AuthorizationServerMetadata authServerMetadata
286316
["grant_type"] = "authorization_code",
287317
["code"] = authorizationCode,
288318
["redirect_uri"] = _redirectUri.ToString(),
289-
["client_id"] = clientId,
319+
["client_id"] = _clientId,
290320
["code_verifier"] = codeVerifier
291321
});
292322

@@ -297,9 +327,9 @@ private Uri BuildAuthorizationUrl(AuthorizationServerMetadata authServerMetadata
297327
Content = requestContent
298328
};
299329

300-
if (!string.IsNullOrEmpty(clientSecret))
330+
if (!string.IsNullOrEmpty(_clientSecret))
301331
{
302-
var authValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}"));
332+
var authValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_clientId}:{_clientSecret}"));
303333
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", authValue);
304334
}
305335

samples/ProtectedMCPClient/Program.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using ModelContextProtocol.Authentication;
13
using ModelContextProtocol.Client;
24
using ModelContextProtocol.Protocol.Transport;
35

@@ -12,8 +14,33 @@ static async Task Main(string[] args)
1214

1315
var serverUrl = "http://localhost:7071/sse";
1416

17+
var services = new ServiceCollection();
18+
services.AddHttpClient();
19+
20+
var sharedHandler = new SocketsHttpHandler
21+
{
22+
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
23+
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1)
24+
};
25+
26+
services.AddHttpClient(BasicOAuthAuthorizationProvider.HttpClientName)
27+
.ConfigurePrimaryHttpMessageHandler(() => sharedHandler);
28+
29+
services.AddHttpClient(AuthorizationHelpers.HttpClientName)
30+
.ConfigurePrimaryHttpMessageHandler(() => sharedHandler);
31+
32+
services.AddTransient<AuthorizationHelpers>();
33+
34+
var serviceProvider = services.BuildServiceProvider();
35+
36+
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
37+
var authorizationHelpers = serviceProvider.GetRequiredService<AuthorizationHelpers>();
38+
39+
// Create the token provider with proper dependencies
1540
var tokenProvider = new BasicOAuthAuthorizationProvider(
16-
new Uri(serverUrl),
41+
new Uri(serverUrl),
42+
httpClientFactory,
43+
authorizationHelpers,
1744
clientId: "6ad97b5f-7a7b-413f-8603-7a3517d4adb8",
1845
redirectUri: new Uri("http://localhost:1179/callback"),
1946
scopes: ["api://167b4284-3f92-4436-92ed-38b38f83ae08/weather.read"]

samples/ProtectedMCPServer/ProtectedMCPServer.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
<ItemGroup>
1010
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
1111
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
12-
<PackageReference Include="Microsoft.IdentityModel.Tokens" />
1312
</ItemGroup>
1413

1514
</Project>

src/ModelContextProtocol/Authentication/AuthorizationDelegatingHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ namespace ModelContextProtocol.Authentication;
99
/// </summary>
1010
public class AuthorizationDelegatingHandler : DelegatingHandler
1111
{
12-
private readonly IMcpAuthorizationProvider _authorizationProvider;
12+
private readonly ITokenProvider _authorizationProvider;
1313
private string _currentScheme;
1414
private static readonly char[] SchemeSplitDelimiters = { ' ', ',' };
1515

1616
/// <summary>
1717
/// Initializes a new instance of the <see cref="AuthorizationDelegatingHandler"/> class.
1818
/// </summary>
1919
/// <param name="authorizationProvider">The provider that supplies authentication tokens.</param>
20-
public AuthorizationDelegatingHandler(IMcpAuthorizationProvider authorizationProvider)
20+
public AuthorizationDelegatingHandler(ITokenProvider authorizationProvider)
2121
{
2222
Throw.IfNull(authorizationProvider);
2323

src/ModelContextProtocol/Authentication/AuthorizationHelpers.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using ModelContextProtocol.Types.Authentication;
2+
using ModelContextProtocol.Utils;
23
using ModelContextProtocol.Utils.Json;
34
using System.Text.Json;
45

@@ -7,23 +8,39 @@ namespace ModelContextProtocol.Authentication;
78
/// <summary>
89
/// Provides utility methods for handling authentication in MCP clients.
910
/// </summary>
10-
public static class AuthorizationHelpers
11+
public class AuthorizationHelpers
1112
{
13+
private readonly HttpClient _httpClient;
14+
15+
/// <summary>
16+
/// Client name for IHttpClientFactory used by the AuthorizationHelpers.
17+
/// </summary>
18+
public const string HttpClientName = "ModelContextProtocol.Authentication";
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="AuthorizationHelpers"/> class.
22+
/// </summary>
23+
/// <param name="httpClientFactory">The HTTP client factory to use for creating HTTP clients.</param>
24+
public AuthorizationHelpers(IHttpClientFactory httpClientFactory)
25+
{
26+
Throw.IfNull(httpClientFactory);
27+
_httpClient = httpClientFactory.CreateClient(HttpClientName);
28+
}
29+
1230
/// <summary>
1331
/// Fetches the protected resource metadata from the provided URL.
1432
/// </summary>
1533
/// <param name="metadataUrl">The URL to fetch the metadata from.</param>
1634
/// <param name="cancellationToken">A token to cancel the operation.</param>
1735
/// <returns>The fetched ProtectedResourceMetadata, or null if it couldn't be fetched.</returns>
18-
private static async Task<ProtectedResourceMetadata?> FetchProtectedResourceMetadataAsync(
36+
private async Task<ProtectedResourceMetadata?> FetchProtectedResourceMetadataAsync(
1937
Uri metadataUrl,
2038
CancellationToken cancellationToken = default)
2139
{
22-
using var httpClient = new HttpClient();
2340
try
2441
{
2542
var request = new HttpRequestMessage(HttpMethod.Get, metadataUrl);
26-
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
43+
var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
2744
response.EnsureSuccessStatusCode();
2845

2946
var content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
@@ -69,7 +86,7 @@ private static bool VerifyResourceMatch(ProtectedResourceMetadata protectedResou
6986
/// <returns>The resource metadata if the resource matches the server, otherwise throws an exception.</returns>
7087
/// <exception cref="InvalidOperationException">Thrown when the response is not a 401, lacks a WWW-Authenticate header,
7188
/// lacks a resource_metadata parameter, the metadata can't be fetched, or the resource URI doesn't match the server URL.</exception>
72-
public static async Task<ProtectedResourceMetadata> ExtractProtectedResourceMetadata(
89+
public async Task<ProtectedResourceMetadata> ExtractProtectedResourceMetadata(
7390
HttpResponseMessage response,
7491
Uri serverUrl,
7592
CancellationToken cancellationToken = default)
@@ -154,7 +171,7 @@ public static async Task<ProtectedResourceMetadata> ExtractProtectedResourceMeta
154171
return new KeyValuePair<string, string>(key, value);
155172
})
156173
.Where(kvp => !string.IsNullOrEmpty(kvp.Key))
157-
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
174+
.ToDictionary();
158175

159176
if (paramDict.TryGetValue(parameterName, out var value))
160177
{

src/ModelContextProtocol/Authentication/ITokenProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace ModelContextProtocol.Authentication;
44
/// Defines an interface for providing authentication for requests.
55
/// This is the main extensibility point for authentication in MCP clients.
66
/// </summary>
7-
public interface IMcpAuthorizationProvider
7+
public interface ITokenProvider
88
{
99
/// <summary>
1010
/// Gets the collection of authentication schemes supported by this provider.

src/ModelContextProtocol/ModelContextProtocol.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
3737
<PackageReference Include="Microsoft.Extensions.AI" />
3838
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
39+
<PackageReference Include="Microsoft.Extensions.Http" />
3940
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
4041
<PackageReference Include="System.Net.ServerSentEvents" />
4142
</ItemGroup>

src/ModelContextProtocol/Protocol/Transport/SseClientTransport.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public SseClientTransport(SseClientTransportOptions transportOptions, HttpClient
5858
/// <param name="transportOptions">Configuration options for the transport.</param>
5959
/// <param name="authorizationProvider">The authorization provider to use for authentication.</param>
6060
/// <param name="loggerFactory">Logger factory for creating loggers used for diagnostic output during transport operations.</param>
61-
public SseClientTransport(SseClientTransportOptions transportOptions, IMcpAuthorizationProvider authorizationProvider, ILoggerFactory? loggerFactory = null)
61+
public SseClientTransport(SseClientTransportOptions transportOptions, ITokenProvider authorizationProvider, ILoggerFactory? loggerFactory = null)
6262
{
6363
Throw.IfNull(transportOptions);
6464
Throw.IfNull(authorizationProvider);

0 commit comments

Comments
 (0)