11using ModelContextProtocol . Authentication ;
2+ using ModelContextProtocol . Types . Authentication ;
23using 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
410namespace 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}
0 commit comments