fix(auth): retain state until issuer validation (#1167)
This commit is contained in:
parent
d81bfa8854
commit
a50a73fda2
1 changed files with 74 additions and 3 deletions
|
|
@ -2018,11 +2018,13 @@ impl AuthorizationManager {
|
||||||
AuthError::InternalError("Authorization state not found".to_string())
|
AuthError::InternalError("Authorization state not found".to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Delete state after retrieval (one-time use)
|
|
||||||
self.state_store.delete(csrf_token).await?;
|
|
||||||
|
|
||||||
Self::validate_authorization_response_issuer(&stored_state, received_issuer)?;
|
Self::validate_authorization_response_issuer(&stored_state, received_issuer)?;
|
||||||
|
|
||||||
|
// Consume state only after the callback is bound to the expected issuer.
|
||||||
|
// A callback with the correct state but a forged or missing required `iss`
|
||||||
|
// must not discard the PKCE verifier needed by the legitimate callback.
|
||||||
|
self.state_store.delete(csrf_token).await?;
|
||||||
|
|
||||||
// capture requested scopes before the state is consumed
|
// capture requested scopes before the state is consumed
|
||||||
let requested_scopes = stored_state.requested_scopes.clone();
|
let requested_scopes = stored_state.requested_scopes.clone();
|
||||||
|
|
||||||
|
|
@ -6762,6 +6764,75 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::missing_required_issuer(None)]
|
||||||
|
#[case::mismatched_issuer(Some("https://evil.example.com"))]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn invalid_issuer_does_not_consume_authorization_state(
|
||||||
|
#[case] invalid_issuer: Option<&str>,
|
||||||
|
) {
|
||||||
|
let client = RecordingOAuthHttpClient::with_responses(vec![http_response(
|
||||||
|
200,
|
||||||
|
serde_json::json!({
|
||||||
|
"access_token": "access-token",
|
||||||
|
"token_type": "bearer",
|
||||||
|
"expires_in": 3600
|
||||||
|
}),
|
||||||
|
)]);
|
||||||
|
let mut manager = AuthorizationManager::new_with_oauth_http_client(
|
||||||
|
"https://mcp.example.com/mcp",
|
||||||
|
Arc::new(client.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
manager.set_metadata(AuthorizationMetadata {
|
||||||
|
authorization_endpoint: "https://auth.example.com/authorize".to_string(),
|
||||||
|
token_endpoint: "https://auth.example.com/token".to_string(),
|
||||||
|
issuer: Some("https://auth.example.com".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
manager.configure_client_id("test-client-id").unwrap();
|
||||||
|
|
||||||
|
let pkce = PkceCodeVerifier::new("verifier".to_string());
|
||||||
|
let csrf = CsrfToken::new("csrf".to_string());
|
||||||
|
let state = StoredAuthorizationState::new_with_expected_issuer(
|
||||||
|
&pkce,
|
||||||
|
&csrf,
|
||||||
|
Some("https://auth.example.com".to_string()),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
manager.state_store.save("csrf", state).await.unwrap();
|
||||||
|
|
||||||
|
manager
|
||||||
|
.exchange_code_for_token_with_issuer("forged-code", "csrf", invalid_issuer)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
manager.state_store.load("csrf").await.unwrap().is_some(),
|
||||||
|
"issuer validation failures must leave state available for the legitimate callback"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
client.requests().is_empty(),
|
||||||
|
"issuer validation failures must not reach the token endpoint"
|
||||||
|
);
|
||||||
|
|
||||||
|
manager
|
||||||
|
.exchange_code_for_token_with_issuer(
|
||||||
|
"legitimate-code",
|
||||||
|
"csrf",
|
||||||
|
Some("https://auth.example.com"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
manager.state_store.load("csrf").await.unwrap().is_none(),
|
||||||
|
"a valid callback must consume its one-time authorization state"
|
||||||
|
);
|
||||||
|
assert_eq!(client.requests().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
// -- scope management --
|
// -- scope management --
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue