Skip to content

Commit a0fbec6

Browse files
committed
Simplify client logic
1 parent afd05af commit a0fbec6

File tree

7 files changed

+218
-190
lines changed

7 files changed

+218
-190
lines changed

samples/ProtectedMCPClient/Program.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,6 @@ static async Task Main(string[] args)
1616
// Create a standard HttpClient with authentication configured
1717
var serverUrl = "http://localhost:7071/sse"; // Default server URL
1818

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-
2819
// Allow the user to specify a different server URL
2920
Console.WriteLine($"Server URL (press Enter for default: {serverUrl}):");
3021
var userInput = Console.ReadLine();
@@ -34,7 +25,7 @@ static async Task Main(string[] args)
3425
}
3526

3627
// Create a single HttpClient with authentication configured
37-
var tokenProvider = new SimpleAccessTokenProvider(apiKey, new Uri(serverUrl));
28+
var tokenProvider = new SimpleAccessTokenProvider(new Uri(serverUrl));
3829
var httpClient = new HttpClient().UseAuthenticationProvider(tokenProvider);
3930

4031
Console.WriteLine();
Lines changed: 205 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,56 @@
11
using ModelContextProtocol.Authentication;
2+
using ModelContextProtocol.Types.Authentication;
23
using System.Collections.Concurrent;
4+
using System.Net.Http.Headers;
5+
using System.Security.Cryptography;
6+
using System.Text;
7+
using System.Text.Json;
8+
using System.Web;
39

410
namespace ProtectedMCPClient;
511

612
/// <summary>
713
/// A simple implementation of IAccessTokenProvider that uses a fixed API key.
814
/// This is just for demonstration purposes.
915
/// </summary>
10-
public class SimpleAccessTokenProvider : IAccessTokenProvider
16+
public partial class SimpleAccessTokenProvider : IAccessTokenProvider
1117
{
12-
private readonly string _apiKey;
13-
private readonly ConcurrentDictionary<string, string> _tokenCache = new();
18+
private readonly ConcurrentDictionary<string, TokenContainer> _tokenCache = new();
1419
private readonly Uri _serverUrl;
20+
private readonly string _clientId;
21+
private readonly string _clientSecret;
22+
private readonly Uri _redirectUri;
1523

16-
public SimpleAccessTokenProvider(string apiKey, Uri serverUrl)
24+
public SimpleAccessTokenProvider(Uri serverUrl, string clientId = "demo-client", string clientSecret = "", Uri? redirectUri = null)
1725
{
18-
_apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
1926
_serverUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl));
27+
_clientId = clientId;
28+
_clientSecret = clientSecret;
29+
_redirectUri = redirectUri ?? new Uri("http://localhost:8080/callback");
2030
}
2131

2232
/// <inheritdoc />
23-
public Task<string?> GetAuthenticationTokenAsync(Uri resourceUri, CancellationToken cancellationToken = default)
33+
public async Task<string?> GetAuthenticationTokenAsync(Uri resourceUri, CancellationToken cancellationToken = default)
2434
{
25-
Console.WriteLine("Tried to get a token!");
26-
// In a real implementation, you might use different tokens for different resources,
27-
// or refresh tokens when they're about to expire
28-
return Task.FromResult<string?>(_apiKey);
35+
Console.WriteLine($"Getting authentication token for {resourceUri}");
36+
37+
// Check if we have a valid cached token
38+
string resourceKey = resourceUri.ToString();
39+
if (_tokenCache.TryGetValue(resourceKey, out var tokenInfo))
40+
{
41+
Console.WriteLine("Using cached token");
42+
return tokenInfo.AccessToken;
43+
}
44+
45+
return string.Empty;
2946
}
3047

3148
/// <inheritdoc />
3249
public async Task<bool> HandleUnauthorizedResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default)
3350
{
3451
try
3552
{
36-
// Use the updated AuthenticationChallengeHandler to handle the 401 challenge
53+
// Use AuthenticationUtils to handle the 401 challenge
3754
var resourceMetadata = await AuthenticationUtils.ExtractProtectedResourceMetadata(
3855
response,
3956
_serverUrl,
@@ -42,30 +59,37 @@ public async Task<bool> HandleUnauthorizedResponseAsync(HttpResponseMessage resp
4259
// If we get here, the resource metadata is valid and matches our server
4360
Console.WriteLine($"Successfully validated resource metadata for: {resourceMetadata.Resource}");
4461

45-
// For a real implementation, you would:
46-
// 1. Use the metadata to get information about the authorization servers
47-
// 2. Obtain a new token from one of those authorization servers
48-
// 3. Store the new token for future requests
49-
50-
// Example of what a real implementation might do:
51-
/*
62+
// Follow the authorization flow as described in the specs
5263
if (resourceMetadata.AuthorizationServers?.Count > 0)
5364
{
65+
// Get the first authorization server
5466
var authServerUrl = resourceMetadata.AuthorizationServers[0];
67+
Console.WriteLine($"Using authorization server: {authServerUrl}");
68+
69+
// Fetch authorization server metadata
5570
var authServerMetadata = await AuthenticationUtils.FetchAuthorizationServerMetadataAsync(
5671
authServerUrl, cancellationToken);
5772

5873
if (authServerMetadata != null)
5974
{
60-
// Use auth server metadata to obtain a new token
61-
// Store the token in _tokenCache
62-
// Return true to indicate the unauthorized response was handled
63-
return true;
75+
// Perform the OAuth authorization code flow with PKCE
76+
var token = await PerformAuthorizationCodeFlowAsync(authServerMetadata, resourceMetadata, cancellationToken);
77+
78+
if (token != null)
79+
{
80+
// Store the token in the cache
81+
string resourceKey = resourceMetadata.Resource.ToString();
82+
_tokenCache[resourceKey] = token;
83+
Console.WriteLine("Successfully obtained a new token");
84+
return true;
85+
}
86+
}
87+
else
88+
{
89+
Console.WriteLine("Failed to fetch authorization server metadata");
6490
}
6591
}
66-
*/
6792

68-
// For now, we still return false since we're not actually refreshing the token
6993
Console.WriteLine("API key is valid, but might not have sufficient permissions.");
7094
return false;
7195
}
@@ -82,4 +106,161 @@ public async Task<bool> HandleUnauthorizedResponseAsync(HttpResponseMessage resp
82106
return false;
83107
}
84108
}
109+
110+
/// <summary>
111+
/// Performs the OAuth authorization code flow with PKCE.
112+
/// </summary>
113+
/// <param name="authServerMetadata">The authorization server metadata.</param>
114+
/// <param name="resourceMetadata">The protected resource metadata.</param>
115+
/// <param name="cancellationToken">A token to cancel the operation.</param>
116+
/// <returns>The token information if successful, otherwise null.</returns>
117+
private async Task<TokenContainer?> PerformAuthorizationCodeFlowAsync(
118+
AuthorizationServerMetadata authServerMetadata,
119+
ProtectedResourceMetadata resourceMetadata,
120+
CancellationToken cancellationToken)
121+
{
122+
// Generate PKCE code challenge
123+
var codeVerifier = GenerateCodeVerifier();
124+
var codeChallenge = GenerateCodeChallenge(codeVerifier);
125+
126+
// In a real client, you would redirect the user to the authorization endpoint
127+
// For this sample, we'll simulate the authorization code grant
128+
Console.WriteLine("In a real app, the user would be redirected to the authorization URL:");
129+
130+
// Build the authorization URL
131+
var authorizationUrl = BuildAuthorizationUrl(authServerMetadata, codeChallenge, resourceMetadata);
132+
Console.WriteLine($"Authorization URL: {authorizationUrl}");
133+
134+
// In a real app, you would wait for the redirect with the authorization code
135+
// For this sample, we'll simulate it
136+
Console.WriteLine("Simulating authorization code grant (in a real app, user would interact with the auth server)");
137+
138+
// Simulate getting an authorization code (this would come from the redirect in a real app)
139+
// NOTE: This is just for demonstration. In a real client, you'd parse the authorization code from the redirect
140+
string simulatedAuthCode = "simulated_auth_code_would_come_from_redirect";
141+
142+
// Exchange the authorization code for tokens
143+
return await ExchangeCodeForTokenAsync(authServerMetadata, simulatedAuthCode, codeVerifier, cancellationToken);
144+
}
145+
146+
/// <summary>
147+
/// Builds the authorization URL for the authorization code flow.
148+
/// </summary>
149+
private Uri BuildAuthorizationUrl(
150+
AuthorizationServerMetadata authServerMetadata,
151+
string codeChallenge,
152+
ProtectedResourceMetadata resourceMetadata)
153+
{
154+
var queryParams = HttpUtility.ParseQueryString(string.Empty);
155+
queryParams["client_id"] = _clientId;
156+
queryParams["redirect_uri"] = _redirectUri.ToString();
157+
queryParams["response_type"] = "code";
158+
queryParams["code_challenge"] = codeChallenge;
159+
queryParams["code_challenge_method"] = "S256";
160+
161+
// Add scopes if available from resource metadata
162+
if (resourceMetadata.ScopesSupported.Count > 0)
163+
{
164+
queryParams["scope"] = string.Join(" ", resourceMetadata.ScopesSupported);
165+
}
166+
167+
// Create the authorization URL
168+
var uriBuilder = new UriBuilder(authServerMetadata.AuthorizationEndpoint);
169+
uriBuilder.Query = queryParams.ToString();
170+
171+
return uriBuilder.Uri;
172+
}
173+
174+
/// <summary>
175+
/// Exchanges an authorization code for an access token.
176+
/// </summary>
177+
private async Task<TokenContainer?> ExchangeCodeForTokenAsync(
178+
AuthorizationServerMetadata authServerMetadata,
179+
string authorizationCode,
180+
string codeVerifier,
181+
CancellationToken cancellationToken)
182+
{
183+
using var httpClient = new HttpClient();
184+
185+
// Set up the request to the token endpoint
186+
var requestContent = new FormUrlEncodedContent(new Dictionary<string, string>
187+
{
188+
["grant_type"] = "authorization_code",
189+
["code"] = authorizationCode,
190+
["redirect_uri"] = _redirectUri.ToString(),
191+
["client_id"] = _clientId,
192+
["code_verifier"] = codeVerifier
193+
});
194+
195+
// Add client authentication if we have a client secret
196+
if (!string.IsNullOrEmpty(_clientSecret))
197+
{
198+
var authHeader = Convert.ToBase64String(
199+
Encoding.UTF8.GetBytes($"{_clientId}:{_clientSecret}"));
200+
httpClient.DefaultRequestHeaders.Authorization =
201+
new AuthenticationHeaderValue("Basic", authHeader);
202+
}
203+
204+
try
205+
{
206+
// Make the token request
207+
var response = await httpClient.PostAsync(
208+
authServerMetadata.TokenEndpoint,
209+
requestContent,
210+
cancellationToken);
211+
212+
if (response.IsSuccessStatusCode)
213+
{
214+
// Parse the token response
215+
var responseJson = await response.Content.ReadAsStringAsync(cancellationToken);
216+
var tokenResponse = JsonSerializer.Deserialize<TokenContainer>(
217+
responseJson,
218+
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
219+
220+
if (tokenResponse != null)
221+
{
222+
// There was a valid token response
223+
}
224+
}
225+
else
226+
{
227+
Console.WriteLine($"Token request failed: {response.StatusCode}");
228+
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
229+
Console.WriteLine($"Error: {errorContent}");
230+
}
231+
}
232+
catch (Exception ex)
233+
{
234+
Console.WriteLine($"Exception during token exchange: {ex.Message}");
235+
}
236+
237+
return null;
238+
}
239+
240+
/// <summary>
241+
/// Generates a random code verifier for PKCE.
242+
/// </summary>
243+
private string GenerateCodeVerifier()
244+
{
245+
var bytes = new byte[32];
246+
using var rng = RandomNumberGenerator.Create();
247+
rng.GetBytes(bytes);
248+
return Convert.ToBase64String(bytes)
249+
.TrimEnd('=')
250+
.Replace('+', '-')
251+
.Replace('/', '_');
252+
}
253+
254+
/// <summary>
255+
/// Generates a code challenge from a code verifier using SHA256.
256+
/// </summary>
257+
private string GenerateCodeChallenge(string codeVerifier)
258+
{
259+
using var sha256 = SHA256.Create();
260+
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
261+
return Convert.ToBase64String(challengeBytes)
262+
.TrimEnd('=')
263+
.Replace('+', '-')
264+
.Replace('/', '_');
265+
}
85266
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace ProtectedMCPClient;
2+
3+
/// <summary>
4+
/// Represents a token response from the OAuth server.
5+
/// </summary>
6+
internal class TokenContainer
7+
{
8+
public string AccessToken { get; set; } = string.Empty;
9+
public string? RefreshToken { get; set; }
10+
public int ExpiresIn { get; set; }
11+
public string TokenType { get; set; } = string.Empty;
12+
}

src/ModelContextProtocol/Authentication/IClientRegistrationProvider.cs

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)