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>
This commit is contained in:
parent
52e731bec6
commit
53c6daadd9
1 changed files with 67 additions and 3 deletions
|
|
@ -1192,7 +1192,8 @@ impl AuthorizationManager {
|
|||
drop(attempts);
|
||||
|
||||
let current_scopes = self.current_scopes.read().await.clone();
|
||||
let upgraded_scopes = Self::compute_scope_union(¤t_scopes, required_scope);
|
||||
let mut upgraded_scopes = Self::compute_scope_union(¤t_scopes, required_scope);
|
||||
self.add_offline_access_if_supported(&mut upgraded_scopes);
|
||||
|
||||
debug!(
|
||||
"Requesting scope upgrade: current={:?}, required={}, union={:?}",
|
||||
|
|
@ -1425,8 +1426,10 @@ impl AuthorizationManager {
|
|||
|
||||
let refresh_token_value = RefreshToken::new(refresh_token.secret().to_string());
|
||||
let mut refresh_request = oauth_client.exchange_refresh_token(&refresh_token_value);
|
||||
for scope in &stored_credentials.granted_scopes {
|
||||
refresh_request = refresh_request.add_scope(Scope::new(scope.clone()));
|
||||
let mut refresh_scopes = stored_credentials.granted_scopes;
|
||||
self.add_offline_access_if_supported(&mut refresh_scopes);
|
||||
for scope in refresh_scopes {
|
||||
refresh_request = refresh_request.add_scope(Scope::new(scope));
|
||||
}
|
||||
let token_result = refresh_request
|
||||
.request_async(&OAuthReqwestClient(self.http_client.clone()))
|
||||
|
|
@ -3912,6 +3915,29 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scope_upgrade_adds_offline_access_when_as_supports_it() {
|
||||
let mut mgr = manager_with_metadata(Some(AuthorizationMetadata {
|
||||
authorization_endpoint: "http://localhost/authorize".to_string(),
|
||||
token_endpoint: "http://localhost/token".to_string(),
|
||||
scopes_supported: Some(vec!["profile".to_string(), "offline_access".to_string()]),
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
mgr.configure_client_id("my-client").unwrap();
|
||||
*mgr.current_scopes.write().await = vec!["profile".to_string()];
|
||||
|
||||
let auth_url = mgr.request_scope_upgrade("email").await.unwrap();
|
||||
let parsed = Url::parse(&auth_url).unwrap();
|
||||
let scope = parsed
|
||||
.query_pairs()
|
||||
.find_map(|(key, value)| (key == "scope").then(|| value.into_owned()))
|
||||
.expect("scope should be present");
|
||||
let mut scope_parts: Vec<&str> = scope.split_whitespace().collect();
|
||||
scope_parts.sort_unstable();
|
||||
assert_eq!(scope_parts, vec!["email", "offline_access", "profile"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scope_upgrade_config_default_values() {
|
||||
let config = ScopeUpgradeConfig::default();
|
||||
|
|
@ -4461,6 +4487,44 @@ mod tests {
|
|||
assert_eq!(scope_parts, vec!["read", "write"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_token_adds_offline_access_when_as_supports_it() {
|
||||
let (base_url, captured) = start_token_server().await;
|
||||
|
||||
let mut manager = manager_with_metadata(Some(AuthorizationMetadata {
|
||||
authorization_endpoint: format!("{}/authorize", base_url),
|
||||
token_endpoint: format!("{}/token", base_url),
|
||||
scopes_supported: Some(vec!["read".to_string(), "offline_access".to_string()]),
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
manager.configure_client(test_client_config()).unwrap();
|
||||
|
||||
let stored = StoredCredentials {
|
||||
client_id: "my-client".to_string(),
|
||||
token_response: Some(make_token_response_with_refresh(
|
||||
"old-token",
|
||||
"my-refresh-token",
|
||||
)),
|
||||
granted_scopes: vec!["read".to_string()],
|
||||
token_received_at: Some(AuthorizationManager::now_epoch_secs()),
|
||||
};
|
||||
manager.credential_store.save(stored).await.unwrap();
|
||||
|
||||
manager.refresh_token().await.unwrap();
|
||||
|
||||
let body = captured.lock().unwrap().take().unwrap();
|
||||
let params: std::collections::HashMap<_, _> = url::form_urlencoded::parse(body.as_bytes())
|
||||
.into_owned()
|
||||
.collect();
|
||||
let scope = params
|
||||
.get("scope")
|
||||
.expect("scope should be present in refresh request");
|
||||
let mut scope_parts: Vec<&str> = scope.split_whitespace().collect();
|
||||
scope_parts.sort_unstable();
|
||||
assert_eq!(scope_parts, vec!["offline_access", "read"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_token_omits_scope_when_granted_scopes_is_empty() {
|
||||
let (base_url, captured) = start_token_server().await;
|
||||
|
|
|
|||
Loading…
Reference in a new issue