Skip to content

Commit ceee919

Browse files
committed
Updating the approach for client auth
1 parent 003f5a0 commit ceee919

File tree

7 files changed

+625
-24
lines changed

7 files changed

+625
-24
lines changed

samples/ProtectedMCPClient/Program.cs

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,30 @@
1+
using ModelContextProtocol.Authentication;
12
using ModelContextProtocol.Client;
23
using ModelContextProtocol.Protocol.Transport;
4+
using System.Diagnostics;
35

46
namespace ProtectedMCPClient;
57

68
class Program
79
{
810
static async Task Main(string[] args)
911
{
10-
Console.WriteLine("MCP Secure Weather Client with OAuth Authentication");
11-
Console.WriteLine("==================================================");
12+
Console.WriteLine("MCP Secure Weather Client with Authentication");
13+
Console.WriteLine("==============================================");
1214
Console.WriteLine();
1315

14-
//// Create the authorization config with HTTP listener
15-
//var authConfig = new AuthorizationConfig
16-
//{
17-
// ClientId = "6ad97b5f-7a7b-413f-8603-7a3517d4adb8",
18-
// Scopes = ["api://167b4284-3f92-4436-92ed-38b38f83ae08/weather.read"]
19-
//}.UseHttpListener(hostname: "localhost", listenPort: 1170);
20-
21-
//// Create an HTTP client with OAuth handling
22-
//var oauthHandler = new OAuthDelegatingHandler(
23-
// redirectUri: authConfig.RedirectUri,
24-
// clientId: authConfig.ClientId,
25-
// clientName: authConfig.ClientName,
26-
// scopes: authConfig.Scopes,
27-
// authorizationHandler: authConfig.AuthorizationHandler)
28-
//{
29-
// // The OAuth handler needs an inner handler
30-
// InnerHandler = new HttpClientHandler()
31-
//};
32-
33-
var httpClient = new HttpClient();
16+
// Create a standard HttpClient with authentication configured
3417
var serverUrl = "http://localhost:7071/sse"; // Default server URL
3518

19+
// Ask for the API key
20+
Console.WriteLine("Enter your API key (or press Enter to use default):");
21+
var apiKey = Console.ReadLine();
22+
if (string.IsNullOrWhiteSpace(apiKey))
23+
{
24+
apiKey = "demo-api-key-12345"; // Default API key for demonstration
25+
Console.WriteLine($"Using default API key: {apiKey}");
26+
}
27+
3628
// Allow the user to specify a different server URL
3729
Console.WriteLine($"Server URL (press Enter for default: {serverUrl}):");
3830
var userInput = Console.ReadLine();
@@ -41,10 +33,14 @@ static async Task Main(string[] args)
4133
serverUrl = userInput;
4234
}
4335

36+
// Create a single HttpClient with authentication configured
37+
var tokenProvider = new SimpleAccessTokenProvider(apiKey, new Uri(serverUrl));
38+
var httpClient = new HttpClient().UseAuthenticationProvider(tokenProvider);
39+
4440
Console.WriteLine();
4541
Console.WriteLine($"Connecting to weather server at {serverUrl}...");
46-
Console.WriteLine("When prompted for authorization, a browser window will open automatically.");
47-
Console.WriteLine("Complete the authentication in the browser, and this application will continue automatically.");
42+
Console.WriteLine("When prompted for authorization, the challenge will be verified automatically.");
43+
Console.WriteLine("If required, you'll be guided through any necessary authentication steps.");
4844
Console.WriteLine();
4945

5046
try
@@ -82,13 +78,27 @@ static async Task Main(string[] args)
8278
Console.WriteLine();
8379
}
8480
}
81+
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
82+
{
83+
// Handle authentication failures specifically
84+
Console.WriteLine("Authentication failed. The server returned a 401 Unauthorized response.");
85+
Console.WriteLine($"Details: {ex.Message}");
86+
87+
// Additional handling for 401 - could add manual authentication retry here
88+
Console.WriteLine("You might need to provide a different API key or authentication credentials.");
89+
}
8590
catch (Exception ex)
8691
{
8792
Console.WriteLine($"Error: {ex.Message}");
8893
if (ex.InnerException != null)
8994
{
9095
Console.WriteLine($"Inner error: {ex.InnerException.Message}");
9196
}
97+
98+
// Print stack trace in debug builds
99+
#if DEBUG
100+
Console.WriteLine($"Stack trace: {ex.StackTrace}");
101+
#endif
92102
}
93103

94104
Console.WriteLine("Press any key to exit...");
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using ModelContextProtocol.Authentication;
2+
using System.Collections.Concurrent;
3+
4+
namespace ProtectedMCPClient;
5+
6+
/// <summary>
7+
/// A simple implementation of IAccessTokenProvider that uses a fixed API key.
8+
/// This is just for demonstration purposes.
9+
/// </summary>
10+
public class SimpleAccessTokenProvider : IAccessTokenProvider
11+
{
12+
private readonly string _apiKey;
13+
private readonly ConcurrentDictionary<string, string> _tokenCache = new();
14+
private readonly Uri _serverUrl;
15+
16+
public SimpleAccessTokenProvider(string apiKey, Uri serverUrl)
17+
{
18+
_apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
19+
_serverUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl));
20+
}
21+
22+
/// <inheritdoc />
23+
public Task<string?> GetAuthenticationTokenAsync(Uri resourceUri, CancellationToken cancellationToken = default)
24+
{
25+
// In a real implementation, you might use different tokens for different resources,
26+
// or refresh tokens when they're about to expire
27+
return Task.FromResult<string?>(_apiKey);
28+
}
29+
30+
/// <inheritdoc />
31+
public async Task<bool> HandleUnauthorizedResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default)
32+
{
33+
try
34+
{
35+
// Use the updated AuthenticationChallengeHandler to handle the 401 challenge
36+
var resourceMetadata = await AuthenticationUtils.HandleAuthenticationChallengeAsync(
37+
response,
38+
_serverUrl,
39+
cancellationToken);
40+
41+
// If we get here, the resource metadata is valid and matches our server
42+
Console.WriteLine($"Successfully validated resource metadata for: {resourceMetadata.Resource}");
43+
44+
// For a real implementation, you would:
45+
// 1. Use the metadata to get information about the authorization servers
46+
// 2. Obtain a new token from one of those authorization servers
47+
// 3. Store the new token for future requests
48+
49+
// Example of what a real implementation might do:
50+
/*
51+
if (resourceMetadata.AuthorizationServers?.Count > 0)
52+
{
53+
var authServerUrl = resourceMetadata.AuthorizationServers[0];
54+
var authServerMetadata = await AuthenticationUtils.FetchAuthorizationServerMetadataAsync(
55+
authServerUrl, cancellationToken);
56+
57+
if (authServerMetadata != null)
58+
{
59+
// Use auth server metadata to obtain a new token
60+
// Store the token in _tokenCache
61+
// Return true to indicate the unauthorized response was handled
62+
return true;
63+
}
64+
}
65+
*/
66+
67+
// For now, we still return false since we're not actually refreshing the token
68+
Console.WriteLine("API key is valid, but might not have sufficient permissions.");
69+
return false;
70+
}
71+
catch (InvalidOperationException ex)
72+
{
73+
// Log the specific error about why the challenge handling failed
74+
Console.WriteLine($"Authentication challenge failed: {ex.Message}");
75+
return false;
76+
}
77+
catch (Exception ex)
78+
{
79+
// Log any unexpected errors
80+
Console.WriteLine($"Unexpected error during authentication challenge: {ex.Message}");
81+
return false;
82+
}
83+
}
84+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System.Net.Http;
2+
using System.Net.Http.Headers;
3+
4+
namespace ModelContextProtocol.Authentication;
5+
6+
/// <summary>
7+
/// A delegating handler that adds authentication tokens to requests and handles 401 responses.
8+
/// </summary>
9+
internal class AuthenticationDelegatingHandler : DelegatingHandler
10+
{
11+
private readonly IAccessTokenProvider _tokenProvider;
12+
private readonly string _scheme;
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="AuthenticationDelegatingHandler"/> class.
16+
/// </summary>
17+
/// <param name="tokenProvider">The provider that supplies authentication tokens.</param>
18+
/// <param name="scheme">The authentication scheme to use, e.g., "Bearer".</param>
19+
public AuthenticationDelegatingHandler(IAccessTokenProvider tokenProvider, string scheme)
20+
{
21+
_tokenProvider = tokenProvider ?? throw new ArgumentNullException(nameof(tokenProvider));
22+
_scheme = scheme ?? throw new ArgumentNullException(nameof(scheme));
23+
}
24+
25+
/// <summary>
26+
/// Sends an HTTP request with authentication handling.
27+
/// </summary>
28+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
29+
{
30+
// Add the authentication token to the request if not already present
31+
if (request.Headers.Authorization == null)
32+
{
33+
await AddAuthenticationHeaderAsync(request, cancellationToken);
34+
}
35+
36+
// Send the request through the inner handler
37+
var response = await base.SendAsync(request, cancellationToken);
38+
39+
// Handle unauthorized responses
40+
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
41+
{
42+
// Try to handle the unauthorized response
43+
var handled = await _tokenProvider.HandleUnauthorizedResponseAsync(
44+
response,
45+
cancellationToken);
46+
47+
if (handled)
48+
{
49+
// If the unauthorized response was handled, retry the request
50+
var retryRequest = await CloneHttpRequestMessageAsync(request);
51+
52+
// Get a new token
53+
await AddAuthenticationHeaderAsync(retryRequest, cancellationToken);
54+
55+
// Send the retry request
56+
return await base.SendAsync(retryRequest, cancellationToken);
57+
}
58+
}
59+
60+
return response;
61+
}
62+
63+
/// <summary>
64+
/// Adds an authorization header to the request.
65+
/// </summary>
66+
private async Task AddAuthenticationHeaderAsync(HttpRequestMessage request, CancellationToken cancellationToken)
67+
{
68+
if (request.RequestUri != null)
69+
{
70+
var token = await _tokenProvider.GetAuthenticationTokenAsync(request.RequestUri, cancellationToken);
71+
if (!string.IsNullOrEmpty(token))
72+
{
73+
request.Headers.Authorization = new AuthenticationHeaderValue(_scheme, token);
74+
}
75+
}
76+
}
77+
78+
/// <summary>
79+
/// Creates a clone of the HTTP request message.
80+
/// </summary>
81+
private static async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpRequestMessage request)
82+
{
83+
var clone = new HttpRequestMessage(request.Method, request.RequestUri);
84+
85+
// Copy the request headers
86+
foreach (var header in request.Headers)
87+
{
88+
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
89+
}
90+
91+
// Copy the request content if present
92+
if (request.Content != null)
93+
{
94+
var contentBytes = await request.Content.ReadAsByteArrayAsync();
95+
var cloneContent = new ByteArrayContent(contentBytes);
96+
97+
// Copy the content headers
98+
foreach (var header in request.Content.Headers)
99+
{
100+
cloneContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
101+
}
102+
103+
clone.Content = cloneContent;
104+
}
105+
106+
// Copy the request properties
107+
#pragma warning disable CS0618 // Type or member is obsolete
108+
foreach (var property in request.Properties)
109+
{
110+
clone.Properties.Add(property);
111+
}
112+
#pragma warning restore CS0618 // Type or member is obsolete
113+
114+
// Copy the request version
115+
clone.Version = request.Version;
116+
117+
return clone;
118+
}
119+
}

0 commit comments

Comments
 (0)