@@ -416,4 +416,82 @@ private async Task<OAuthToken> ExchangeAuthorizationCodeForTokenAsync(
416416
417417 return tokenResponse ;
418418 }
419+
420+ /// <summary>
421+ /// Refreshes an OAuth access token using a refresh token.
422+ /// </summary>
423+ /// <param name="tokenEndpoint">The token endpoint URI from the authorization server metadata.</param>
424+ /// <param name="clientId">The client ID to use for authentication.</param>
425+ /// <param name="clientSecret">The client secret to use for authentication, if available.</param>
426+ /// <param name="refreshToken">The refresh token to use for obtaining a new access token.</param>
427+ /// <param name="scopes">Optional scopes to request. If not provided, the server will use the same scopes as the original token.</param>
428+ /// <returns>A new OAuth token response containing a new access token and potentially a new refresh token.</returns>
429+ /// <exception cref="ArgumentNullException">Thrown when required parameters are null.</exception>
430+ /// <exception cref="InvalidOperationException">Thrown when the token refresh fails.</exception>
431+ public async Task < OAuthToken > RefreshAccessTokenAsync (
432+ Uri tokenEndpoint ,
433+ string clientId ,
434+ string ? clientSecret ,
435+ string refreshToken ,
436+ IEnumerable < string > ? scopes = null )
437+ {
438+ if ( tokenEndpoint == null ) throw new ArgumentNullException ( nameof ( tokenEndpoint ) ) ;
439+ if ( string . IsNullOrEmpty ( clientId ) ) throw new ArgumentNullException ( nameof ( clientId ) ) ;
440+ if ( string . IsNullOrEmpty ( refreshToken ) ) throw new ArgumentNullException ( nameof ( refreshToken ) ) ;
441+
442+ var tokenRequest = new Dictionary < string , string >
443+ {
444+ [ "grant_type" ] = "refresh_token" ,
445+ [ "refresh_token" ] = refreshToken ,
446+ [ "client_id" ] = clientId
447+ } ;
448+
449+ // Add scopes if provided
450+ if ( scopes != null )
451+ {
452+ tokenRequest [ "scope" ] = string . Join ( " " , scopes ) ;
453+ }
454+
455+ var requestContent = new FormUrlEncodedContent ( tokenRequest ) ;
456+
457+ HttpResponseMessage response ;
458+ if ( ! string . IsNullOrEmpty ( clientSecret ) )
459+ {
460+ // Add client authentication if secret is available
461+ var authValue = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( $ "{ clientId } :{ clientSecret } ") ) ;
462+ _httpClient . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Basic" , authValue ) ;
463+ response = await _httpClient . PostAsync ( tokenEndpoint , requestContent ) ;
464+ _httpClient . DefaultRequestHeaders . Authorization = null ;
465+ }
466+ else
467+ {
468+ response = await _httpClient . PostAsync ( tokenEndpoint , requestContent ) ;
469+ }
470+
471+ try
472+ {
473+ response . EnsureSuccessStatusCode ( ) ;
474+
475+ var json = await response . Content . ReadAsStringAsync ( ) ;
476+ var tokenResponse = JsonSerializer . Deserialize ( json , McpJsonUtilities . DefaultOptions . GetTypeInfo < OAuthToken > ( ) ) ;
477+ if ( tokenResponse == null )
478+ {
479+ throw new InvalidOperationException ( "Failed to parse token response." ) ;
480+ }
481+
482+ // Some authorization servers might not return a new refresh token
483+ // If no new refresh token is provided, keep the old one
484+ if ( string . IsNullOrEmpty ( tokenResponse . RefreshToken ) )
485+ {
486+ tokenResponse . RefreshToken = refreshToken ;
487+ }
488+
489+ return tokenResponse ;
490+ }
491+ catch ( HttpRequestException ex )
492+ {
493+ string errorContent = await response . Content . ReadAsStringAsync ( ) ;
494+ throw new InvalidOperationException ( $ "Failed to refresh access token: { ex . Message } . Response: { errorContent } ", ex ) ;
495+ }
496+ }
419497}
0 commit comments