fix(oauth): keep using valid MCP access tokens without a refresh token (#11324)

This commit is contained in:
Alex Hancock
2026-08-25 14:21:44 +00:00
committed by GitHub
parent ff1193d8c3
commit eb188752d2
+62 -1
View File
@@ -17,7 +17,7 @@ use serde::Deserialize;
use std::collections::BTreeSet;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::{oneshot, Mutex};
use tracing::warn;
@@ -202,6 +202,26 @@ fn resolve_refreshed_granted_scopes(
token_scopes.unwrap_or_else(|| previous_granted_scopes.to_vec())
}
const REFRESH_BUFFER_SECS: u64 = 30;
fn access_token_needs_refresh(stored_credentials: &StoredCredentials) -> bool {
let Some(token_response) = stored_credentials.token_response.as_ref() else {
return true;
};
let (Some(expires_in), Some(received_at)) = (
token_response.expires_in(),
stored_credentials.token_received_at,
) else {
return false;
};
let elapsed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
.saturating_sub(received_at);
expires_in.as_secs().saturating_sub(elapsed) < REFRESH_BUFFER_SECS
}
fn configure_static_client(
auth_manager: &mut AuthorizationManager,
static_client: Option<&StaticOAuthClientConfig>,
@@ -331,6 +351,9 @@ pub async fn oauth_flow_with_challenge(
// client_id alone; a confidential client must present its secret
// at the token endpoint for the refresh to succeed.
configure_static_client(&mut auth_manager, static_client, mcp_server_url)?;
if !access_token_needs_refresh(stored_credentials) {
return Ok(auth_manager);
}
match auth_manager.refresh_token().await {
Ok(mut token_response) => {
let restored_omitted_scopes =
@@ -797,6 +820,44 @@ mod tests {
assert_eq!(request.scopes, vec!["scope.read", "scope.write"]);
}
#[test]
fn long_lived_token_without_refresh_token_is_not_refreshed() {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let token_response: OAuthTokenResponse = serde_json::from_value(serde_json::json!({
"access_token": "mcp_long_lived",
"token_type": "bearer",
"expires_in": 31_536_000,
}))
.unwrap();
let stored = StoredCredentials::new(
"client-id".to_string(),
Some(token_response.clone()),
vec![],
Some(now),
);
assert!(token_response.refresh_token().is_none());
assert!(
!access_token_needs_refresh(&stored),
"a token valid for another year must be used as-is instead of forcing browser re-auth"
);
let expired = StoredCredentials::new(
"client-id".to_string(),
Some(token_response),
vec![],
Some(now - 31_536_000),
);
assert!(
access_token_needs_refresh(&expired),
"an expired token must still take the refresh path"
);
}
#[tokio::test]
async fn wait_for_callback_times_out_with_authorization_url() {
let (_sender, receiver) = oneshot::channel();