mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
test(iam): verify JWKS rotation refresh (#6559)
This commit is contained in:
+100
-4
@@ -2273,6 +2273,7 @@ pub(crate) fn test_config(id: &str) -> OidcProviderConfig {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use jsonwebtoken::{Algorithm, EncodingKey, Header};
|
||||
use rustfs_utils::egress::OutboundDnsPolicyRejection;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
@@ -2641,12 +2642,15 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn start_mock_oidc_discovery_server<F>(
|
||||
fn start_mock_oidc_discovery_server_with_jwks<F, J>(
|
||||
build_discovery_issuer: F,
|
||||
max_requests: usize,
|
||||
signing_alg: &'static str,
|
||||
jwks_response: J,
|
||||
) -> Option<(String, std::thread::JoinHandle<()>)>
|
||||
where
|
||||
F: Fn(&str) -> (String, String, String) + Send + 'static,
|
||||
J: Fn(usize) -> String + Send + 'static,
|
||||
{
|
||||
use std::io::Write;
|
||||
use std::net::{Shutdown, TcpListener};
|
||||
@@ -2674,10 +2678,9 @@ mod tests {
|
||||
"response_types_supported": ["code"],
|
||||
"response_modes_supported": ["query"],
|
||||
"subject_types_supported": ["public"],
|
||||
"id_token_signing_alg_values_supported": ["RS256"],
|
||||
"id_token_signing_alg_values_supported": [signing_alg],
|
||||
})
|
||||
.to_string();
|
||||
let jwks_body = r#"{"keys":[]}"#;
|
||||
let (ready_tx, ready_rx) = mpsc::channel();
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
@@ -2687,6 +2690,7 @@ mod tests {
|
||||
let _ = ready_tx.send(());
|
||||
|
||||
let mut seen = 0usize;
|
||||
let mut jwks_fetches = 0usize;
|
||||
let start = Instant::now();
|
||||
let mut last_completed = Instant::now();
|
||||
|
||||
@@ -2719,7 +2723,11 @@ mod tests {
|
||||
.expect("failed to set discovery mock read timeout");
|
||||
|
||||
let path = read_mock_oidc_request_path(&mut stream);
|
||||
let response = mock_oidc_response(&path, &discovery_body, &expected_jwks_path, jwks_body);
|
||||
let jwks_body = jwks_response(jwks_fetches);
|
||||
if path == expected_jwks_path {
|
||||
jwks_fetches += 1;
|
||||
}
|
||||
let response = mock_oidc_response(&path, &discovery_body, &expected_jwks_path, &jwks_body);
|
||||
let _ = stream.write_all(response.as_bytes());
|
||||
let _ = stream.flush();
|
||||
let _ = stream.shutdown(Shutdown::Both);
|
||||
@@ -2737,6 +2745,94 @@ mod tests {
|
||||
Some((base, handle))
|
||||
}
|
||||
|
||||
fn start_mock_oidc_discovery_server<F>(
|
||||
build_discovery_issuer: F,
|
||||
max_requests: usize,
|
||||
) -> Option<(String, std::thread::JoinHandle<()>)>
|
||||
where
|
||||
F: Fn(&str) -> (String, String, String) + Send + 'static,
|
||||
{
|
||||
start_mock_oidc_discovery_server_with_jwks(build_discovery_issuer, max_requests, "RS256", |_| {
|
||||
r#"{"keys":[]}"#.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn oidc_es256_key_and_jwk(kid: &str) -> (EncodingKey, serde_json::Value) {
|
||||
let certified =
|
||||
rcgen::generate_simple_self_signed(vec![format!("{kid}.invalid")]).expect("OIDC signing key should generate");
|
||||
let encoding_key =
|
||||
EncodingKey::from_ec_pem(certified.signing_key.serialize_pem().as_bytes()).expect("OIDC signing key should encode");
|
||||
let mut jwk = jsonwebtoken::jwk::Jwk::from_encoding_key(&encoding_key, Algorithm::ES256)
|
||||
.expect("OIDC public JWK should derive from signing key");
|
||||
jwk.common.key_id = Some(kid.to_string());
|
||||
jwk.common.public_key_use = Some(jsonwebtoken::jwk::PublicKeyUse::Signature);
|
||||
(encoding_key, serde_json::to_value(jwk).expect("OIDC JWK should serialize"))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn web_identity_verification_refreshes_rotated_jwks() {
|
||||
let (_, initial_jwk) = oidc_es256_key_and_jwk("initial");
|
||||
let (rotated_key, rotated_jwk) = oidc_es256_key_and_jwk("rotated");
|
||||
let initial_jwks = serde_json::json!({ "keys": [initial_jwk] }).to_string();
|
||||
let rotated_jwks = serde_json::json!({ "keys": [rotated_jwk] }).to_string();
|
||||
let Some((base, handle)) = start_mock_oidc_discovery_server_with_jwks(
|
||||
|base| (base.to_string(), format!("{base}/jwks"), "/jwks".to_string()),
|
||||
4,
|
||||
"ES256",
|
||||
move |fetch| {
|
||||
if fetch == 0 {
|
||||
initial_jwks.clone()
|
||||
} else {
|
||||
rotated_jwks.clone()
|
||||
}
|
||||
},
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let config = build_mocked_oidc_provider_config("rotating", &base);
|
||||
let policy = OutboundPolicy::from_allowed_origins(&base).expect("loopback origin should be allowed");
|
||||
let http_client = ReqwestHttpClient::with_policy(policy);
|
||||
let state = OidcSys::discover_provider(&config, &http_client)
|
||||
.await
|
||||
.expect("initial OIDC discovery should succeed");
|
||||
let sys = OidcSys {
|
||||
configs: HashMap::from([(config.id.clone(), config.clone())]),
|
||||
provider_states: RwLock::new(HashMap::from([(config.id.clone(), state)])),
|
||||
state_store: OidcStateStore::new(),
|
||||
http_client,
|
||||
};
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("system clock should be after Unix epoch")
|
||||
.as_secs();
|
||||
let mut header = Header::new(Algorithm::ES256);
|
||||
header.kid = Some("rotated".to_string());
|
||||
let token = jsonwebtoken::encode(
|
||||
&header,
|
||||
&serde_json::json!({
|
||||
"iss": base,
|
||||
"sub": "rotated-user",
|
||||
"aud": config.client_id,
|
||||
"iat": now,
|
||||
"exp": now + 300,
|
||||
"groups": ["readwrite"],
|
||||
}),
|
||||
&rotated_key,
|
||||
)
|
||||
.expect("rotated OIDC token should sign");
|
||||
|
||||
let (claims, provider_id) = sys
|
||||
.verify_web_identity_token(&token)
|
||||
.await
|
||||
.expect("verification should refresh JWKS and accept the rotated key");
|
||||
assert_eq!(provider_id, "rotating");
|
||||
assert_eq!(claims.sub, "rotated-user");
|
||||
assert_eq!(claims.groups, vec!["readwrite"]);
|
||||
handle.join().expect("rotating JWKS mock server should exit cleanly");
|
||||
}
|
||||
|
||||
fn start_mock_oidc_tls_discovery_server<F>(
|
||||
build_discovery_issuer: F,
|
||||
max_requests: usize,
|
||||
|
||||
Reference in New Issue
Block a user