Skip to content

Commit 55bb471

Browse files
committed
Implement refresh token logic
1 parent 8d4c0c4 commit 55bb471

File tree

1 file changed

+78
-0
lines changed

1 file changed

+78
-0
lines changed

src/ModelContextProtocol/Auth/OAuthService.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)