Skip to content

Commit 53c6daa

Browse files
fix(auth): apply offline_access to reauth paths (#897)
* fix(auth): apply offline_access to reauth paths * Update crates/rmcp/src/transport/auth.rs Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com> --------- Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com>
1 parent 52e731b commit 53c6daa

1 file changed

Lines changed: 67 additions & 3 deletions

File tree

crates/rmcp/src/transport/auth.rs

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,7 +1192,8 @@ impl AuthorizationManager {
11921192
drop(attempts);
11931193

11941194
let current_scopes = self.current_scopes.read().await.clone();
1195-
let upgraded_scopes = Self::compute_scope_union(&current_scopes, required_scope);
1195+
let mut upgraded_scopes = Self::compute_scope_union(&current_scopes, required_scope);
1196+
self.add_offline_access_if_supported(&mut upgraded_scopes);
11961197

11971198
debug!(
11981199
"Requesting scope upgrade: current={:?}, required={}, union={:?}",
@@ -1425,8 +1426,10 @@ impl AuthorizationManager {
14251426

14261427
let refresh_token_value = RefreshToken::new(refresh_token.secret().to_string());
14271428
let mut refresh_request = oauth_client.exchange_refresh_token(&refresh_token_value);
1428-
for scope in &stored_credentials.granted_scopes {
1429-
refresh_request = refresh_request.add_scope(Scope::new(scope.clone()));
1429+
let mut refresh_scopes = stored_credentials.granted_scopes;
1430+
self.add_offline_access_if_supported(&mut refresh_scopes);
1431+
for scope in refresh_scopes {
1432+
refresh_request = refresh_request.add_scope(Scope::new(scope));
14301433
}
14311434
let token_result = refresh_request
14321435
.request_async(&OAuthReqwestClient(self.http_client.clone()))
@@ -3912,6 +3915,29 @@ mod tests {
39123915
);
39133916
}
39143917

3918+
#[tokio::test]
3919+
async fn scope_upgrade_adds_offline_access_when_as_supports_it() {
3920+
let mut mgr = manager_with_metadata(Some(AuthorizationMetadata {
3921+
authorization_endpoint: "http://localhost/authorize".to_string(),
3922+
token_endpoint: "http://localhost/token".to_string(),
3923+
scopes_supported: Some(vec!["profile".to_string(), "offline_access".to_string()]),
3924+
..Default::default()
3925+
}))
3926+
.await;
3927+
mgr.configure_client_id("my-client").unwrap();
3928+
*mgr.current_scopes.write().await = vec!["profile".to_string()];
3929+
3930+
let auth_url = mgr.request_scope_upgrade("email").await.unwrap();
3931+
let parsed = Url::parse(&auth_url).unwrap();
3932+
let scope = parsed
3933+
.query_pairs()
3934+
.find_map(|(key, value)| (key == "scope").then(|| value.into_owned()))
3935+
.expect("scope should be present");
3936+
let mut scope_parts: Vec<&str> = scope.split_whitespace().collect();
3937+
scope_parts.sort_unstable();
3938+
assert_eq!(scope_parts, vec!["email", "offline_access", "profile"]);
3939+
}
3940+
39153941
#[test]
39163942
fn scope_upgrade_config_default_values() {
39173943
let config = ScopeUpgradeConfig::default();
@@ -4461,6 +4487,44 @@ mod tests {
44614487
assert_eq!(scope_parts, vec!["read", "write"]);
44624488
}
44634489

4490+
#[tokio::test]
4491+
async fn refresh_token_adds_offline_access_when_as_supports_it() {
4492+
let (base_url, captured) = start_token_server().await;
4493+
4494+
let mut manager = manager_with_metadata(Some(AuthorizationMetadata {
4495+
authorization_endpoint: format!("{}/authorize", base_url),
4496+
token_endpoint: format!("{}/token", base_url),
4497+
scopes_supported: Some(vec!["read".to_string(), "offline_access".to_string()]),
4498+
..Default::default()
4499+
}))
4500+
.await;
4501+
manager.configure_client(test_client_config()).unwrap();
4502+
4503+
let stored = StoredCredentials {
4504+
client_id: "my-client".to_string(),
4505+
token_response: Some(make_token_response_with_refresh(
4506+
"old-token",
4507+
"my-refresh-token",
4508+
)),
4509+
granted_scopes: vec!["read".to_string()],
4510+
token_received_at: Some(AuthorizationManager::now_epoch_secs()),
4511+
};
4512+
manager.credential_store.save(stored).await.unwrap();
4513+
4514+
manager.refresh_token().await.unwrap();
4515+
4516+
let body = captured.lock().unwrap().take().unwrap();
4517+
let params: std::collections::HashMap<_, _> = url::form_urlencoded::parse(body.as_bytes())
4518+
.into_owned()
4519+
.collect();
4520+
let scope = params
4521+
.get("scope")
4522+
.expect("scope should be present in refresh request");
4523+
let mut scope_parts: Vec<&str> = scope.split_whitespace().collect();
4524+
scope_parts.sort_unstable();
4525+
assert_eq!(scope_parts, vec!["offline_access", "read"]);
4526+
}
4527+
44644528
#[tokio::test]
44654529
async fn refresh_token_omits_scope_when_granted_scopes_is_empty() {
44664530
let (base_url, captured) = start_token_server().await;

0 commit comments

Comments
 (0)