this repo has no description
1#![allow(unused_imports)] 2#![allow(unused_variables)] 3mod common; 4mod helpers; 5use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 6use bspds::oauth::dpop::{DPoPVerifier, DPoPJwk, compute_jwk_thumbprint}; 7use chrono::Utc; 8use common::{base_url, client}; 9use helpers::verify_new_account; 10use reqwest::{redirect, StatusCode}; 11use serde_json::{json, Value}; 12use sha2::{Digest, Sha256}; 13use wiremock::{Mock, MockServer, ResponseTemplate}; 14use wiremock::matchers::{method, path}; 15fn no_redirect_client() -> reqwest::Client { 16 reqwest::Client::builder() 17 .redirect(redirect::Policy::none()) 18 .build() 19 .unwrap() 20} 21fn generate_pkce() -> (String, String) { 22 let verifier_bytes: [u8; 32] = rand::random(); 23 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes); 24 let mut hasher = Sha256::new(); 25 hasher.update(code_verifier.as_bytes()); 26 let hash = hasher.finalize(); 27 let code_challenge = URL_SAFE_NO_PAD.encode(&hash); 28 (code_verifier, code_challenge) 29} 30async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer { 31 let mock_server = MockServer::start().await; 32 let client_id = mock_server.uri(); 33 let metadata = json!({ 34 "client_id": client_id, 35 "client_name": "Security Test Client", 36 "redirect_uris": [redirect_uri], 37 "grant_types": ["authorization_code", "refresh_token"], 38 "response_types": ["code"], 39 "token_endpoint_auth_method": "none", 40 "dpop_bound_access_tokens": false 41 }); 42 Mock::given(method("GET")) 43 .and(path("/")) 44 .respond_with(ResponseTemplate::new(200).set_body_json(metadata)) 45 .mount(&mock_server) 46 .await; 47 mock_server 48} 49async fn get_oauth_tokens( 50 http_client: &reqwest::Client, 51 url: &str, 52) -> (String, String, String) { 53 let ts = Utc::now().timestamp_millis(); 54 let handle = format!("sec-test-{}", ts); 55 let email = format!("sec-test-{}@example.com", ts); 56 let password = "security-test-password"; 57 http_client 58 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 59 .json(&json!({ 60 "handle": handle, 61 "email": email, 62 "password": password 63 })) 64 .send() 65 .await 66 .unwrap(); 67 let redirect_uri = "https://example.com/sec-callback"; 68 let mock_client = setup_mock_client_metadata(redirect_uri).await; 69 let client_id = mock_client.uri(); 70 let (code_verifier, code_challenge) = generate_pkce(); 71 let par_body: Value = http_client 72 .post(format!("{}/oauth/par", url)) 73 .form(&[ 74 ("response_type", "code"), 75 ("client_id", &client_id), 76 ("redirect_uri", redirect_uri), 77 ("code_challenge", &code_challenge), 78 ("code_challenge_method", "S256"), 79 ]) 80 .send() 81 .await 82 .unwrap() 83 .json() 84 .await 85 .unwrap(); 86 let request_uri = par_body["request_uri"].as_str().unwrap(); 87 let auth_client = no_redirect_client(); 88 let auth_res = auth_client 89 .post(format!("{}/oauth/authorize", url)) 90 .form(&[ 91 ("request_uri", request_uri), 92 ("username", &handle), 93 ("password", password), 94 ("remember_device", "false"), 95 ]) 96 .send() 97 .await 98 .unwrap(); 99 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 100 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 101 let token_body: Value = http_client 102 .post(format!("{}/oauth/token", url)) 103 .form(&[ 104 ("grant_type", "authorization_code"), 105 ("code", code), 106 ("redirect_uri", redirect_uri), 107 ("code_verifier", &code_verifier), 108 ("client_id", &client_id), 109 ]) 110 .send() 111 .await 112 .unwrap() 113 .json() 114 .await 115 .unwrap(); 116 let access_token = token_body["access_token"].as_str().unwrap().to_string(); 117 let refresh_token = token_body["refresh_token"].as_str().unwrap().to_string(); 118 (access_token, refresh_token, client_id) 119} 120#[tokio::test] 121async fn test_security_forged_token_signature_rejected() { 122 let url = base_url().await; 123 let http_client = client(); 124 let (access_token, _, _) = get_oauth_tokens(&http_client, url).await; 125 let parts: Vec<&str> = access_token.split('.').collect(); 126 assert_eq!(parts.len(), 3, "Token should have 3 parts"); 127 let forged_signature = URL_SAFE_NO_PAD.encode(&[0u8; 32]); 128 let forged_token = format!("{}.{}.{}", parts[0], parts[1], forged_signature); 129 let res = http_client 130 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 131 .header("Authorization", format!("Bearer {}", forged_token)) 132 .send() 133 .await 134 .unwrap(); 135 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Forged signature should be rejected"); 136} 137#[tokio::test] 138async fn test_security_modified_payload_rejected() { 139 let url = base_url().await; 140 let http_client = client(); 141 let (access_token, _, _) = get_oauth_tokens(&http_client, url).await; 142 let parts: Vec<&str> = access_token.split('.').collect(); 143 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap(); 144 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap(); 145 payload["sub"] = json!("did:plc:attacker"); 146 let modified_payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 147 let modified_token = format!("{}.{}.{}", parts[0], modified_payload, parts[2]); 148 let res = http_client 149 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 150 .header("Authorization", format!("Bearer {}", modified_token)) 151 .send() 152 .await 153 .unwrap(); 154 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Modified payload should be rejected"); 155} 156#[tokio::test] 157async fn test_security_algorithm_none_attack_rejected() { 158 let url = base_url().await; 159 let http_client = client(); 160 let header = json!({ 161 "alg": "none", 162 "typ": "at+jwt" 163 }); 164 let payload = json!({ 165 "iss": "https://test.pds", 166 "sub": "did:plc:attacker", 167 "aud": "https://test.pds", 168 "iat": Utc::now().timestamp(), 169 "exp": Utc::now().timestamp() + 3600, 170 "jti": "fake-token-id", 171 "scope": "atproto" 172 }); 173 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 174 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 175 let malicious_token = format!("{}.{}.", header_b64, payload_b64); 176 let res = http_client 177 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 178 .header("Authorization", format!("Bearer {}", malicious_token)) 179 .send() 180 .await 181 .unwrap(); 182 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Algorithm 'none' attack should be rejected"); 183} 184#[tokio::test] 185async fn test_security_algorithm_substitution_attack_rejected() { 186 let url = base_url().await; 187 let http_client = client(); 188 let header = json!({ 189 "alg": "RS256", 190 "typ": "at+jwt" 191 }); 192 let payload = json!({ 193 "iss": "https://test.pds", 194 "sub": "did:plc:attacker", 195 "aud": "https://test.pds", 196 "iat": Utc::now().timestamp(), 197 "exp": Utc::now().timestamp() + 3600, 198 "jti": "fake-token-id" 199 }); 200 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 201 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 202 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 64]); 203 let malicious_token = format!("{}.{}.{}", header_b64, payload_b64, fake_sig); 204 let res = http_client 205 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 206 .header("Authorization", format!("Bearer {}", malicious_token)) 207 .send() 208 .await 209 .unwrap(); 210 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Algorithm substitution attack should be rejected"); 211} 212#[tokio::test] 213async fn test_security_expired_token_rejected() { 214 let url = base_url().await; 215 let http_client = client(); 216 let header = json!({ 217 "alg": "HS256", 218 "typ": "at+jwt" 219 }); 220 let payload = json!({ 221 "iss": "https://test.pds", 222 "sub": "did:plc:test", 223 "aud": "https://test.pds", 224 "iat": Utc::now().timestamp() - 7200, 225 "exp": Utc::now().timestamp() - 3600, 226 "jti": "expired-token-id" 227 }); 228 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 229 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 230 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 32]); 231 let expired_token = format!("{}.{}.{}", header_b64, payload_b64, fake_sig); 232 let res = http_client 233 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 234 .header("Authorization", format!("Bearer {}", expired_token)) 235 .send() 236 .await 237 .unwrap(); 238 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Expired token should be rejected"); 239} 240#[tokio::test] 241async fn test_security_pkce_plain_method_rejected() { 242 let url = base_url().await; 243 let http_client = client(); 244 let redirect_uri = "https://example.com/pkce-plain-callback"; 245 let mock_client = setup_mock_client_metadata(redirect_uri).await; 246 let client_id = mock_client.uri(); 247 let res = http_client 248 .post(format!("{}/oauth/par", url)) 249 .form(&[ 250 ("response_type", "code"), 251 ("client_id", &client_id), 252 ("redirect_uri", redirect_uri), 253 ("code_challenge", "plain-text-challenge"), 254 ("code_challenge_method", "plain"), 255 ]) 256 .send() 257 .await 258 .unwrap(); 259 assert_eq!(res.status(), StatusCode::BAD_REQUEST, "PKCE plain method should be rejected"); 260 let body: Value = res.json().await.unwrap(); 261 assert_eq!(body["error"], "invalid_request"); 262 assert!( 263 body["error_description"].as_str().unwrap().to_lowercase().contains("s256"), 264 "Error should mention S256 requirement" 265 ); 266} 267#[tokio::test] 268async fn test_security_pkce_missing_challenge_rejected() { 269 let url = base_url().await; 270 let http_client = client(); 271 let redirect_uri = "https://example.com/no-pkce-callback"; 272 let mock_client = setup_mock_client_metadata(redirect_uri).await; 273 let client_id = mock_client.uri(); 274 let res = http_client 275 .post(format!("{}/oauth/par", url)) 276 .form(&[ 277 ("response_type", "code"), 278 ("client_id", &client_id), 279 ("redirect_uri", redirect_uri), 280 ]) 281 .send() 282 .await 283 .unwrap(); 284 assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Missing PKCE challenge should be rejected"); 285} 286#[tokio::test] 287async fn test_security_pkce_wrong_verifier_rejected() { 288 let url = base_url().await; 289 let http_client = client(); 290 let ts = Utc::now().timestamp_millis(); 291 let handle = format!("pkce-attack-{}", ts); 292 let email = format!("pkce-attack-{}@example.com", ts); 293 let password = "pkce-attack-password"; 294 http_client 295 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 296 .json(&json!({ 297 "handle": handle, 298 "email": email, 299 "password": password 300 })) 301 .send() 302 .await 303 .unwrap(); 304 let redirect_uri = "https://example.com/pkce-attack-callback"; 305 let mock_client = setup_mock_client_metadata(redirect_uri).await; 306 let client_id = mock_client.uri(); 307 let (_, code_challenge) = generate_pkce(); 308 let (attacker_verifier, _) = generate_pkce(); 309 let par_body: Value = http_client 310 .post(format!("{}/oauth/par", url)) 311 .form(&[ 312 ("response_type", "code"), 313 ("client_id", &client_id), 314 ("redirect_uri", redirect_uri), 315 ("code_challenge", &code_challenge), 316 ("code_challenge_method", "S256"), 317 ]) 318 .send() 319 .await 320 .unwrap() 321 .json() 322 .await 323 .unwrap(); 324 let request_uri = par_body["request_uri"].as_str().unwrap(); 325 let auth_client = no_redirect_client(); 326 let auth_res = auth_client 327 .post(format!("{}/oauth/authorize", url)) 328 .form(&[ 329 ("request_uri", request_uri), 330 ("username", &handle), 331 ("password", password), 332 ("remember_device", "false"), 333 ]) 334 .send() 335 .await 336 .unwrap(); 337 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 338 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 339 let token_res = http_client 340 .post(format!("{}/oauth/token", url)) 341 .form(&[ 342 ("grant_type", "authorization_code"), 343 ("code", code), 344 ("redirect_uri", redirect_uri), 345 ("code_verifier", &attacker_verifier), 346 ("client_id", &client_id), 347 ]) 348 .send() 349 .await 350 .unwrap(); 351 assert_eq!(token_res.status(), StatusCode::BAD_REQUEST, "Wrong PKCE verifier should be rejected"); 352 let body: Value = token_res.json().await.unwrap(); 353 assert_eq!(body["error"], "invalid_grant"); 354} 355#[tokio::test] 356async fn test_security_authorization_code_replay_attack() { 357 let url = base_url().await; 358 let http_client = client(); 359 let ts = Utc::now().timestamp_millis(); 360 let handle = format!("code-replay-{}", ts); 361 let email = format!("code-replay-{}@example.com", ts); 362 let password = "code-replay-password"; 363 http_client 364 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 365 .json(&json!({ 366 "handle": handle, 367 "email": email, 368 "password": password 369 })) 370 .send() 371 .await 372 .unwrap(); 373 let redirect_uri = "https://example.com/code-replay-callback"; 374 let mock_client = setup_mock_client_metadata(redirect_uri).await; 375 let client_id = mock_client.uri(); 376 let (code_verifier, code_challenge) = generate_pkce(); 377 let par_body: Value = http_client 378 .post(format!("{}/oauth/par", url)) 379 .form(&[ 380 ("response_type", "code"), 381 ("client_id", &client_id), 382 ("redirect_uri", redirect_uri), 383 ("code_challenge", &code_challenge), 384 ("code_challenge_method", "S256"), 385 ]) 386 .send() 387 .await 388 .unwrap() 389 .json() 390 .await 391 .unwrap(); 392 let request_uri = par_body["request_uri"].as_str().unwrap(); 393 let auth_client = no_redirect_client(); 394 let auth_res = auth_client 395 .post(format!("{}/oauth/authorize", url)) 396 .form(&[ 397 ("request_uri", request_uri), 398 ("username", &handle), 399 ("password", password), 400 ("remember_device", "false"), 401 ]) 402 .send() 403 .await 404 .unwrap(); 405 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 406 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 407 let stolen_code = code.to_string(); 408 let first_res = http_client 409 .post(format!("{}/oauth/token", url)) 410 .form(&[ 411 ("grant_type", "authorization_code"), 412 ("code", code), 413 ("redirect_uri", redirect_uri), 414 ("code_verifier", &code_verifier), 415 ("client_id", &client_id), 416 ]) 417 .send() 418 .await 419 .unwrap(); 420 assert_eq!(first_res.status(), StatusCode::OK, "First use should succeed"); 421 let replay_res = http_client 422 .post(format!("{}/oauth/token", url)) 423 .form(&[ 424 ("grant_type", "authorization_code"), 425 ("code", &stolen_code), 426 ("redirect_uri", redirect_uri), 427 ("code_verifier", &code_verifier), 428 ("client_id", &client_id), 429 ]) 430 .send() 431 .await 432 .unwrap(); 433 assert_eq!(replay_res.status(), StatusCode::BAD_REQUEST, "Replay attack should fail"); 434 let body: Value = replay_res.json().await.unwrap(); 435 assert_eq!(body["error"], "invalid_grant"); 436} 437#[tokio::test] 438async fn test_security_refresh_token_replay_attack() { 439 let url = base_url().await; 440 let http_client = client(); 441 let ts = Utc::now().timestamp_millis(); 442 let handle = format!("rt-replay-{}", ts); 443 let email = format!("rt-replay-{}@example.com", ts); 444 let password = "rt-replay-password"; 445 http_client 446 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 447 .json(&json!({ 448 "handle": handle, 449 "email": email, 450 "password": password 451 })) 452 .send() 453 .await 454 .unwrap(); 455 let redirect_uri = "https://example.com/rt-replay-callback"; 456 let mock_client = setup_mock_client_metadata(redirect_uri).await; 457 let client_id = mock_client.uri(); 458 let (code_verifier, code_challenge) = generate_pkce(); 459 let par_body: Value = http_client 460 .post(format!("{}/oauth/par", url)) 461 .form(&[ 462 ("response_type", "code"), 463 ("client_id", &client_id), 464 ("redirect_uri", redirect_uri), 465 ("code_challenge", &code_challenge), 466 ("code_challenge_method", "S256"), 467 ]) 468 .send() 469 .await 470 .unwrap() 471 .json() 472 .await 473 .unwrap(); 474 let request_uri = par_body["request_uri"].as_str().unwrap(); 475 let auth_client = no_redirect_client(); 476 let auth_res = auth_client 477 .post(format!("{}/oauth/authorize", url)) 478 .form(&[ 479 ("request_uri", request_uri), 480 ("username", &handle), 481 ("password", password), 482 ("remember_device", "false"), 483 ]) 484 .send() 485 .await 486 .unwrap(); 487 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 488 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 489 let token_body: Value = http_client 490 .post(format!("{}/oauth/token", url)) 491 .form(&[ 492 ("grant_type", "authorization_code"), 493 ("code", code), 494 ("redirect_uri", redirect_uri), 495 ("code_verifier", &code_verifier), 496 ("client_id", &client_id), 497 ]) 498 .send() 499 .await 500 .unwrap() 501 .json() 502 .await 503 .unwrap(); 504 let stolen_refresh_token = token_body["refresh_token"].as_str().unwrap().to_string(); 505 let first_refresh: Value = http_client 506 .post(format!("{}/oauth/token", url)) 507 .form(&[ 508 ("grant_type", "refresh_token"), 509 ("refresh_token", &stolen_refresh_token), 510 ("client_id", &client_id), 511 ]) 512 .send() 513 .await 514 .unwrap() 515 .json() 516 .await 517 .unwrap(); 518 assert!(first_refresh["access_token"].is_string(), "First refresh should succeed"); 519 let new_refresh_token = first_refresh["refresh_token"].as_str().unwrap(); 520 let replay_res = http_client 521 .post(format!("{}/oauth/token", url)) 522 .form(&[ 523 ("grant_type", "refresh_token"), 524 ("refresh_token", &stolen_refresh_token), 525 ("client_id", &client_id), 526 ]) 527 .send() 528 .await 529 .unwrap(); 530 assert_eq!(replay_res.status(), StatusCode::BAD_REQUEST, "Refresh token replay should fail"); 531 let body: Value = replay_res.json().await.unwrap(); 532 assert_eq!(body["error"], "invalid_grant"); 533 assert!( 534 body["error_description"].as_str().unwrap().to_lowercase().contains("reuse"), 535 "Error should mention token reuse" 536 ); 537 let family_revoked_res = http_client 538 .post(format!("{}/oauth/token", url)) 539 .form(&[ 540 ("grant_type", "refresh_token"), 541 ("refresh_token", new_refresh_token), 542 ("client_id", &client_id), 543 ]) 544 .send() 545 .await 546 .unwrap(); 547 assert_eq!( 548 family_revoked_res.status(), 549 StatusCode::BAD_REQUEST, 550 "Token family should be revoked after replay detection" 551 ); 552} 553#[tokio::test] 554async fn test_security_redirect_uri_manipulation() { 555 let url = base_url().await; 556 let http_client = client(); 557 let registered_redirect = "https://legitimate-app.com/callback"; 558 let attacker_redirect = "https://attacker.com/steal"; 559 let mock_client = setup_mock_client_metadata(registered_redirect).await; 560 let client_id = mock_client.uri(); 561 let (_, code_challenge) = generate_pkce(); 562 let res = http_client 563 .post(format!("{}/oauth/par", url)) 564 .form(&[ 565 ("response_type", "code"), 566 ("client_id", &client_id), 567 ("redirect_uri", attacker_redirect), 568 ("code_challenge", &code_challenge), 569 ("code_challenge_method", "S256"), 570 ]) 571 .send() 572 .await 573 .unwrap(); 574 assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Unregistered redirect_uri should be rejected"); 575} 576#[tokio::test] 577async fn test_security_deactivated_account_blocked() { 578 let url = base_url().await; 579 let http_client = client(); 580 let ts = Utc::now().timestamp_millis(); 581 let handle = format!("deact-sec-{}", ts); 582 let email = format!("deact-sec-{}@example.com", ts); 583 let password = "deact-sec-password"; 584 let create_res = http_client 585 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 586 .json(&json!({ 587 "handle": handle, 588 "email": email, 589 "password": password 590 })) 591 .send() 592 .await 593 .unwrap(); 594 assert_eq!(create_res.status(), StatusCode::OK); 595 let account: Value = create_res.json().await.unwrap(); 596 let did = account["did"].as_str().unwrap(); 597 let access_jwt = verify_new_account(&http_client, did).await; 598 let deact_res = http_client 599 .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url)) 600 .header("Authorization", format!("Bearer {}", access_jwt)) 601 .json(&json!({})) 602 .send() 603 .await 604 .unwrap(); 605 assert_eq!(deact_res.status(), StatusCode::OK); 606 let redirect_uri = "https://example.com/deact-sec-callback"; 607 let mock_client = setup_mock_client_metadata(redirect_uri).await; 608 let client_id = mock_client.uri(); 609 let (_, code_challenge) = generate_pkce(); 610 let par_body: Value = http_client 611 .post(format!("{}/oauth/par", url)) 612 .form(&[ 613 ("response_type", "code"), 614 ("client_id", &client_id), 615 ("redirect_uri", redirect_uri), 616 ("code_challenge", &code_challenge), 617 ("code_challenge_method", "S256"), 618 ]) 619 .send() 620 .await 621 .unwrap() 622 .json() 623 .await 624 .unwrap(); 625 let request_uri = par_body["request_uri"].as_str().unwrap(); 626 let auth_res = http_client 627 .post(format!("{}/oauth/authorize", url)) 628 .header("Accept", "application/json") 629 .form(&[ 630 ("request_uri", request_uri), 631 ("username", &handle), 632 ("password", password), 633 ("remember_device", "false"), 634 ]) 635 .send() 636 .await 637 .unwrap(); 638 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN, "Deactivated account should be blocked from OAuth"); 639 let body: Value = auth_res.json().await.unwrap(); 640 assert_eq!(body["error"], "access_denied"); 641} 642#[tokio::test] 643async fn test_security_url_injection_in_state_parameter() { 644 let url = base_url().await; 645 let http_client = client(); 646 let ts = Utc::now().timestamp_millis(); 647 let handle = format!("inject-state-{}", ts); 648 let email = format!("inject-state-{}@example.com", ts); 649 let password = "inject-state-password"; 650 http_client 651 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 652 .json(&json!({ 653 "handle": handle, 654 "email": email, 655 "password": password 656 })) 657 .send() 658 .await 659 .unwrap(); 660 let redirect_uri = "https://example.com/inject-callback"; 661 let mock_client = setup_mock_client_metadata(redirect_uri).await; 662 let client_id = mock_client.uri(); 663 let (code_verifier, code_challenge) = generate_pkce(); 664 let malicious_state = "state&redirect_uri=https://attacker.com&extra="; 665 let par_body: Value = http_client 666 .post(format!("{}/oauth/par", url)) 667 .form(&[ 668 ("response_type", "code"), 669 ("client_id", &client_id), 670 ("redirect_uri", redirect_uri), 671 ("code_challenge", &code_challenge), 672 ("code_challenge_method", "S256"), 673 ("state", malicious_state), 674 ]) 675 .send() 676 .await 677 .unwrap() 678 .json() 679 .await 680 .unwrap(); 681 let request_uri = par_body["request_uri"].as_str().unwrap(); 682 let auth_client = no_redirect_client(); 683 let auth_res = auth_client 684 .post(format!("{}/oauth/authorize", url)) 685 .form(&[ 686 ("request_uri", request_uri), 687 ("username", &handle), 688 ("password", password), 689 ("remember_device", "false"), 690 ]) 691 .send() 692 .await 693 .unwrap(); 694 assert!(auth_res.status().is_redirection(), "Should redirect successfully"); 695 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 696 assert!( 697 location.starts_with(redirect_uri), 698 "Redirect should go to registered URI, not attacker URI. Got: {}", 699 location 700 ); 701 let redirect_uri_count = location.matches("redirect_uri=").count(); 702 assert!( 703 redirect_uri_count <= 1, 704 "State injection should not add extra redirect_uri parameters" 705 ); 706 assert!( 707 location.contains(&urlencoding::encode(malicious_state).to_string()) || 708 location.contains("state=state%26redirect_uri"), 709 "State parameter should be properly URL-encoded. Got: {}", 710 location 711 ); 712} 713#[tokio::test] 714async fn test_security_cross_client_token_theft() { 715 let url = base_url().await; 716 let http_client = client(); 717 let ts = Utc::now().timestamp_millis(); 718 let handle = format!("cross-client-{}", ts); 719 let email = format!("cross-client-{}@example.com", ts); 720 let password = "cross-client-password"; 721 http_client 722 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 723 .json(&json!({ 724 "handle": handle, 725 "email": email, 726 "password": password 727 })) 728 .send() 729 .await 730 .unwrap(); 731 let redirect_uri_a = "https://app-a.com/callback"; 732 let mock_client_a = setup_mock_client_metadata(redirect_uri_a).await; 733 let client_id_a = mock_client_a.uri(); 734 let redirect_uri_b = "https://app-b.com/callback"; 735 let mock_client_b = setup_mock_client_metadata(redirect_uri_b).await; 736 let client_id_b = mock_client_b.uri(); 737 let (code_verifier, code_challenge) = generate_pkce(); 738 let par_body: Value = http_client 739 .post(format!("{}/oauth/par", url)) 740 .form(&[ 741 ("response_type", "code"), 742 ("client_id", &client_id_a), 743 ("redirect_uri", redirect_uri_a), 744 ("code_challenge", &code_challenge), 745 ("code_challenge_method", "S256"), 746 ]) 747 .send() 748 .await 749 .unwrap() 750 .json() 751 .await 752 .unwrap(); 753 let request_uri = par_body["request_uri"].as_str().unwrap(); 754 let auth_client = no_redirect_client(); 755 let auth_res = auth_client 756 .post(format!("{}/oauth/authorize", url)) 757 .form(&[ 758 ("request_uri", request_uri), 759 ("username", &handle), 760 ("password", password), 761 ("remember_device", "false"), 762 ]) 763 .send() 764 .await 765 .unwrap(); 766 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 767 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 768 let token_res = http_client 769 .post(format!("{}/oauth/token", url)) 770 .form(&[ 771 ("grant_type", "authorization_code"), 772 ("code", code), 773 ("redirect_uri", redirect_uri_a), 774 ("code_verifier", &code_verifier), 775 ("client_id", &client_id_b), 776 ]) 777 .send() 778 .await 779 .unwrap(); 780 assert_eq!( 781 token_res.status(), 782 StatusCode::BAD_REQUEST, 783 "Cross-client code exchange must be explicitly rejected (defense-in-depth)" 784 ); 785 let body: Value = token_res.json().await.unwrap(); 786 assert_eq!(body["error"], "invalid_grant"); 787 assert!( 788 body["error_description"].as_str().unwrap().contains("client_id"), 789 "Error should mention client_id mismatch" 790 ); 791} 792#[test] 793fn test_security_dpop_nonce_tamper_detection() { 794 let secret = b"test-dpop-secret-32-bytes-long!!"; 795 let verifier = DPoPVerifier::new(secret); 796 let nonce = verifier.generate_nonce(); 797 let nonce_bytes = URL_SAFE_NO_PAD.decode(&nonce).unwrap(); 798 let mut tampered = nonce_bytes.clone(); 799 if !tampered.is_empty() { 800 tampered[0] ^= 0xFF; 801 } 802 let tampered_nonce = URL_SAFE_NO_PAD.encode(&tampered); 803 let result = verifier.validate_nonce(&tampered_nonce); 804 assert!(result.is_err(), "Tampered nonce should be rejected"); 805} 806#[test] 807fn test_security_dpop_nonce_cross_server_rejected() { 808 let secret1 = b"server-1-secret-32-bytes-long!!!"; 809 let secret2 = b"server-2-secret-32-bytes-long!!!"; 810 let verifier1 = DPoPVerifier::new(secret1); 811 let verifier2 = DPoPVerifier::new(secret2); 812 let nonce_from_server1 = verifier1.generate_nonce(); 813 let result = verifier2.validate_nonce(&nonce_from_server1); 814 assert!(result.is_err(), "Nonce from different server should be rejected"); 815} 816#[test] 817fn test_security_dpop_proof_signature_tampering() { 818 use p256::ecdsa::{SigningKey, Signature, signature::Signer}; 819 use p256::elliptic_curve::sec1::ToEncodedPoint; 820 let secret = b"test-dpop-secret-32-bytes-long!!"; 821 let verifier = DPoPVerifier::new(secret); 822 let signing_key = SigningKey::random(&mut rand::thread_rng()); 823 let verifying_key = signing_key.verifying_key(); 824 let point = verifying_key.to_encoded_point(false); 825 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap()); 826 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap()); 827 let header = json!({ 828 "typ": "dpop+jwt", 829 "alg": "ES256", 830 "jwk": { 831 "kty": "EC", 832 "crv": "P-256", 833 "x": x, 834 "y": y 835 } 836 }); 837 let payload = json!({ 838 "jti": format!("tamper-test-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)), 839 "htm": "POST", 840 "htu": "https://example.com/token", 841 "iat": Utc::now().timestamp() 842 }); 843 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 844 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 845 let signing_input = format!("{}.{}", header_b64, payload_b64); 846 let signature: Signature = signing_key.sign(signing_input.as_bytes()); 847 let mut sig_bytes = signature.to_bytes().to_vec(); 848 sig_bytes[0] ^= 0xFF; 849 let tampered_sig = URL_SAFE_NO_PAD.encode(&sig_bytes); 850 let tampered_proof = format!("{}.{}.{}", header_b64, payload_b64, tampered_sig); 851 let result = verifier.verify_proof(&tampered_proof, "POST", "https://example.com/token", None); 852 assert!(result.is_err(), "Tampered DPoP signature should be rejected"); 853} 854#[test] 855fn test_security_dpop_proof_key_substitution() { 856 use p256::ecdsa::{SigningKey, Signature, signature::Signer}; 857 use p256::elliptic_curve::sec1::ToEncodedPoint; 858 let secret = b"test-dpop-secret-32-bytes-long!!"; 859 let verifier = DPoPVerifier::new(secret); 860 let signing_key = SigningKey::random(&mut rand::thread_rng()); 861 let attacker_key = SigningKey::random(&mut rand::thread_rng()); 862 let attacker_verifying = attacker_key.verifying_key(); 863 let attacker_point = attacker_verifying.to_encoded_point(false); 864 let x = URL_SAFE_NO_PAD.encode(attacker_point.x().unwrap()); 865 let y = URL_SAFE_NO_PAD.encode(attacker_point.y().unwrap()); 866 let header = json!({ 867 "typ": "dpop+jwt", 868 "alg": "ES256", 869 "jwk": { 870 "kty": "EC", 871 "crv": "P-256", 872 "x": x, 873 "y": y 874 } 875 }); 876 let payload = json!({ 877 "jti": format!("key-sub-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)), 878 "htm": "POST", 879 "htu": "https://example.com/token", 880 "iat": Utc::now().timestamp() 881 }); 882 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 883 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 884 let signing_input = format!("{}.{}", header_b64, payload_b64); 885 let signature: Signature = signing_key.sign(signing_input.as_bytes()); 886 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes()); 887 let mismatched_proof = format!("{}.{}.{}", header_b64, payload_b64, signature_b64); 888 let result = verifier.verify_proof(&mismatched_proof, "POST", "https://example.com/token", None); 889 assert!(result.is_err(), "DPoP proof with mismatched key should be rejected"); 890} 891#[test] 892fn test_security_jwk_thumbprint_consistency() { 893 let jwk = DPoPJwk { 894 kty: "EC".to_string(), 895 crv: Some("P-256".to_string()), 896 x: Some("WbbXrPhtCg66wuF0NLhzXxF5PFzNZ7wNJm9M_1pCcXY".to_string()), 897 y: Some("DubR6_2kU1H5EYhbcNpYZGy1EY6GEKKxv6PYx8VW0rA".to_string()), 898 }; 899 let mut results = Vec::new(); 900 for _ in 0..100 { 901 results.push(compute_jwk_thumbprint(&jwk).unwrap()); 902 } 903 let first = &results[0]; 904 for (i, result) in results.iter().enumerate() { 905 assert_eq!(first, result, "Thumbprint should be deterministic, but iteration {} differs", i); 906 } 907} 908#[test] 909fn test_security_dpop_iat_clock_skew_limits() { 910 use p256::ecdsa::{SigningKey, Signature, signature::Signer}; 911 use p256::elliptic_curve::sec1::ToEncodedPoint; 912 let secret = b"test-dpop-secret-32-bytes-long!!"; 913 let verifier = DPoPVerifier::new(secret); 914 let test_offsets = vec![ 915 (-600, true), 916 (-301, true), 917 (-299, false), 918 (0, false), 919 (299, false), 920 (301, true), 921 (600, true), 922 ]; 923 for (offset_secs, should_fail) in test_offsets { 924 let signing_key = SigningKey::random(&mut rand::thread_rng()); 925 let verifying_key = signing_key.verifying_key(); 926 let point = verifying_key.to_encoded_point(false); 927 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap()); 928 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap()); 929 let header = json!({ 930 "typ": "dpop+jwt", 931 "alg": "ES256", 932 "jwk": { 933 "kty": "EC", 934 "crv": "P-256", 935 "x": x, 936 "y": y 937 } 938 }); 939 let payload = json!({ 940 "jti": format!("clock-{}-{}", offset_secs, Utc::now().timestamp_nanos_opt().unwrap_or(0)), 941 "htm": "POST", 942 "htu": "https://example.com/token", 943 "iat": Utc::now().timestamp() + offset_secs 944 }); 945 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 946 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 947 let signing_input = format!("{}.{}", header_b64, payload_b64); 948 let signature: Signature = signing_key.sign(signing_input.as_bytes()); 949 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes()); 950 let proof = format!("{}.{}.{}", header_b64, payload_b64, signature_b64); 951 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None); 952 if should_fail { 953 assert!(result.is_err(), "iat offset {} should be rejected", offset_secs); 954 } else { 955 assert!(result.is_ok(), "iat offset {} should be accepted", offset_secs); 956 } 957 } 958} 959#[test] 960fn test_security_dpop_method_case_insensitivity() { 961 use p256::ecdsa::{SigningKey, Signature, signature::Signer}; 962 use p256::elliptic_curve::sec1::ToEncodedPoint; 963 let secret = b"test-dpop-secret-32-bytes-long!!"; 964 let verifier = DPoPVerifier::new(secret); 965 let signing_key = SigningKey::random(&mut rand::thread_rng()); 966 let verifying_key = signing_key.verifying_key(); 967 let point = verifying_key.to_encoded_point(false); 968 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap()); 969 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap()); 970 let header = json!({ 971 "typ": "dpop+jwt", 972 "alg": "ES256", 973 "jwk": { 974 "kty": "EC", 975 "crv": "P-256", 976 "x": x, 977 "y": y 978 } 979 }); 980 let payload = json!({ 981 "jti": format!("case-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)), 982 "htm": "post", 983 "htu": "https://example.com/token", 984 "iat": Utc::now().timestamp() 985 }); 986 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 987 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 988 let signing_input = format!("{}.{}", header_b64, payload_b64); 989 let signature: Signature = signing_key.sign(signing_input.as_bytes()); 990 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes()); 991 let proof = format!("{}.{}.{}", header_b64, payload_b64, signature_b64); 992 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None); 993 assert!(result.is_ok(), "HTTP method comparison should be case-insensitive"); 994} 995#[tokio::test] 996async fn test_security_invalid_grant_type_rejected() { 997 let url = base_url().await; 998 let http_client = client(); 999 let grant_types = vec![ 1000 "client_credentials", 1001 "password", 1002 "implicit", 1003 "urn:ietf:params:oauth:grant-type:jwt-bearer", 1004 "urn:ietf:params:oauth:grant-type:device_code", 1005 "", 1006 "AUTHORIZATION_CODE", 1007 "Authorization_Code", 1008 ]; 1009 for grant_type in grant_types { 1010 let res = http_client 1011 .post(format!("{}/oauth/token", url)) 1012 .form(&[ 1013 ("grant_type", grant_type), 1014 ("client_id", "https://example.com"), 1015 ]) 1016 .send() 1017 .await 1018 .unwrap(); 1019 assert_eq!( 1020 res.status(), 1021 StatusCode::BAD_REQUEST, 1022 "Grant type '{}' should be rejected", 1023 grant_type 1024 ); 1025 } 1026} 1027#[tokio::test] 1028async fn test_security_token_with_wrong_typ_rejected() { 1029 let url = base_url().await; 1030 let http_client = client(); 1031 let wrong_types = vec![ 1032 "JWT", 1033 "jwt", 1034 "at+JWT", 1035 "access_token", 1036 "", 1037 ]; 1038 for typ in wrong_types { 1039 let header = json!({ 1040 "alg": "HS256", 1041 "typ": typ 1042 }); 1043 let payload = json!({ 1044 "iss": "https://test.pds", 1045 "sub": "did:plc:test", 1046 "aud": "https://test.pds", 1047 "iat": Utc::now().timestamp(), 1048 "exp": Utc::now().timestamp() + 3600, 1049 "jti": "wrong-typ-token" 1050 }); 1051 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 1052 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 1053 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 32]); 1054 let token = format!("{}.{}.{}", header_b64, payload_b64, fake_sig); 1055 let res = http_client 1056 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1057 .header("Authorization", format!("Bearer {}", token)) 1058 .send() 1059 .await 1060 .unwrap(); 1061 assert_eq!( 1062 res.status(), 1063 StatusCode::UNAUTHORIZED, 1064 "Token with typ='{}' should be rejected", 1065 typ 1066 ); 1067 } 1068} 1069#[tokio::test] 1070async fn test_security_missing_required_claims_rejected() { 1071 let url = base_url().await; 1072 let http_client = client(); 1073 let tokens_missing_claims = vec![ 1074 (json!({"iss": "x", "sub": "x", "aud": "x", "iat": 0}), "exp"), 1075 (json!({"iss": "x", "sub": "x", "aud": "x", "exp": 9999999999i64}), "iat"), 1076 (json!({"iss": "x", "aud": "x", "iat": 0, "exp": 9999999999i64}), "sub"), 1077 ]; 1078 for (payload, missing_claim) in tokens_missing_claims { 1079 let header = json!({ 1080 "alg": "HS256", 1081 "typ": "at+jwt" 1082 }); 1083 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 1084 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 1085 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 32]); 1086 let token = format!("{}.{}.{}", header_b64, payload_b64, fake_sig); 1087 let res = http_client 1088 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1089 .header("Authorization", format!("Bearer {}", token)) 1090 .send() 1091 .await 1092 .unwrap(); 1093 assert_eq!( 1094 res.status(), 1095 StatusCode::UNAUTHORIZED, 1096 "Token missing '{}' claim should be rejected", 1097 missing_claim 1098 ); 1099 } 1100} 1101#[tokio::test] 1102async fn test_security_malformed_tokens_rejected() { 1103 let url = base_url().await; 1104 let http_client = client(); 1105 let malformed_tokens = vec![ 1106 "", 1107 "not-a-token", 1108 "one.two", 1109 "one.two.three.four", 1110 "....", 1111 "eyJhbGciOiJIUzI1NiJ9", 1112 "eyJhbGciOiJIUzI1NiJ9.", 1113 "eyJhbGciOiJIUzI1NiJ9..", 1114 ".eyJzdWIiOiJ0ZXN0In0.", 1115 "!!invalid-base64!!.eyJzdWIiOiJ0ZXN0In0.sig", 1116 "eyJhbGciOiJIUzI1NiJ9.!!invalid!!.sig", 1117 ]; 1118 for token in malformed_tokens { 1119 let res = http_client 1120 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1121 .header("Authorization", format!("Bearer {}", token)) 1122 .send() 1123 .await 1124 .unwrap(); 1125 assert_eq!( 1126 res.status(), 1127 StatusCode::UNAUTHORIZED, 1128 "Malformed token '{}' should be rejected", 1129 if token.len() > 50 { &token[..50] } else { token } 1130 ); 1131 } 1132} 1133#[tokio::test] 1134async fn test_security_authorization_header_formats() { 1135 let url = base_url().await; 1136 let http_client = client(); 1137 let (access_token, _, _) = get_oauth_tokens(&http_client, url).await; 1138 let valid_case_variants = vec![ 1139 format!("bearer {}", access_token), 1140 format!("BEARER {}", access_token), 1141 format!("Bearer {}", access_token), 1142 ]; 1143 for auth_header in valid_case_variants { 1144 let res = http_client 1145 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1146 .header("Authorization", &auth_header) 1147 .send() 1148 .await 1149 .unwrap(); 1150 assert_eq!( 1151 res.status(), 1152 StatusCode::OK, 1153 "Auth header '{}...' should be accepted (RFC 7235 case-insensitivity)", 1154 if auth_header.len() > 30 { &auth_header[..30] } else { &auth_header } 1155 ); 1156 } 1157 let invalid_formats = vec![ 1158 format!("Basic {}", access_token), 1159 format!("Digest {}", access_token), 1160 access_token.clone(), 1161 format!("Bearer{}", access_token), 1162 ]; 1163 for auth_header in invalid_formats { 1164 let res = http_client 1165 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1166 .header("Authorization", &auth_header) 1167 .send() 1168 .await 1169 .unwrap(); 1170 assert_eq!( 1171 res.status(), 1172 StatusCode::UNAUTHORIZED, 1173 "Auth header '{}...' should be rejected", 1174 if auth_header.len() > 30 { &auth_header[..30] } else { &auth_header } 1175 ); 1176 } 1177} 1178#[tokio::test] 1179async fn test_security_no_authorization_header() { 1180 let url = base_url().await; 1181 let http_client = client(); 1182 let res = http_client 1183 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1184 .send() 1185 .await 1186 .unwrap(); 1187 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Missing auth header should return 401"); 1188} 1189#[tokio::test] 1190async fn test_security_empty_authorization_header() { 1191 let url = base_url().await; 1192 let http_client = client(); 1193 let res = http_client 1194 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1195 .header("Authorization", "") 1196 .send() 1197 .await 1198 .unwrap(); 1199 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Empty auth header should return 401"); 1200} 1201#[tokio::test] 1202async fn test_security_revoked_token_rejected() { 1203 let url = base_url().await; 1204 let http_client = client(); 1205 let (access_token, refresh_token, _) = get_oauth_tokens(&http_client, url).await; 1206 let revoke_res = http_client 1207 .post(format!("{}/oauth/revoke", url)) 1208 .form(&[("token", &refresh_token)]) 1209 .send() 1210 .await 1211 .unwrap(); 1212 assert_eq!(revoke_res.status(), StatusCode::OK); 1213 let introspect_res = http_client 1214 .post(format!("{}/oauth/introspect", url)) 1215 .form(&[("token", &access_token)]) 1216 .send() 1217 .await 1218 .unwrap(); 1219 let introspect_body: Value = introspect_res.json().await.unwrap(); 1220 assert_eq!(introspect_body["active"], false, "Revoked token should be inactive"); 1221} 1222#[tokio::test] 1223#[ignore = "rate limiting is disabled in test environment"] 1224async fn test_security_oauth_authorize_rate_limiting() { 1225 let url = base_url().await; 1226 let http_client = no_redirect_client(); 1227 let ts = Utc::now().timestamp_nanos_opt().unwrap_or(0); 1228 let unique_ip = format!("10.{}.{}.{}", (ts >> 16) & 0xFF, (ts >> 8) & 0xFF, ts & 0xFF); 1229 let redirect_uri = "https://example.com/rate-limit-callback"; 1230 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1231 let client_id = mock_client.uri(); 1232 let (_, code_challenge) = generate_pkce(); 1233 let client_for_par = client(); 1234 let par_body: Value = client_for_par 1235 .post(format!("{}/oauth/par", url)) 1236 .form(&[ 1237 ("response_type", "code"), 1238 ("client_id", &client_id), 1239 ("redirect_uri", redirect_uri), 1240 ("code_challenge", &code_challenge), 1241 ("code_challenge_method", "S256"), 1242 ]) 1243 .send() 1244 .await 1245 .unwrap() 1246 .json() 1247 .await 1248 .unwrap(); 1249 let request_uri = par_body["request_uri"].as_str().unwrap(); 1250 let mut rate_limited_count = 0; 1251 let mut other_count = 0; 1252 for _ in 0..15 { 1253 let res = http_client 1254 .post(format!("{}/oauth/authorize", url)) 1255 .header("X-Forwarded-For", &unique_ip) 1256 .form(&[ 1257 ("request_uri", request_uri), 1258 ("username", "nonexistent_user"), 1259 ("password", "wrong_password"), 1260 ("remember_device", "false"), 1261 ]) 1262 .send() 1263 .await 1264 .unwrap(); 1265 match res.status() { 1266 StatusCode::TOO_MANY_REQUESTS => rate_limited_count += 1, 1267 _ => other_count += 1, 1268 } 1269 } 1270 assert!( 1271 rate_limited_count > 0, 1272 "Expected at least one rate-limited response after 15 OAuth authorize attempts. Got {} other and {} rate limited.", 1273 other_count, 1274 rate_limited_count 1275 ); 1276} 1277fn create_dpop_proof( 1278 method: &str, 1279 uri: &str, 1280 nonce: Option<&str>, 1281 ath: Option<&str>, 1282 iat_offset_secs: i64, 1283) -> String { 1284 use p256::ecdsa::{SigningKey, Signature, signature::Signer}; 1285 let signing_key = SigningKey::random(&mut rand::thread_rng()); 1286 let verifying_key = signing_key.verifying_key(); 1287 let point = verifying_key.to_encoded_point(false); 1288 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap()); 1289 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap()); 1290 let jwk = json!({ 1291 "kty": "EC", 1292 "crv": "P-256", 1293 "x": x, 1294 "y": y 1295 }); 1296 let header = json!({ 1297 "typ": "dpop+jwt", 1298 "alg": "ES256", 1299 "jwk": jwk 1300 }); 1301 let mut payload = json!({ 1302 "jti": format!("unique-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)), 1303 "htm": method, 1304 "htu": uri, 1305 "iat": Utc::now().timestamp() + iat_offset_secs 1306 }); 1307 if let Some(n) = nonce { 1308 payload["nonce"] = json!(n); 1309 } 1310 if let Some(a) = ath { 1311 payload["ath"] = json!(a); 1312 } 1313 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 1314 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 1315 let signing_input = format!("{}.{}", header_b64, payload_b64); 1316 let signature: Signature = signing_key.sign(signing_input.as_bytes()); 1317 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes()); 1318 format!("{}.{}", signing_input, signature_b64) 1319} 1320#[test] 1321fn test_dpop_nonce_generation() { 1322 let secret = b"test-dpop-secret-32-bytes-long!!"; 1323 let verifier = DPoPVerifier::new(secret); 1324 let nonce1 = verifier.generate_nonce(); 1325 let nonce2 = verifier.generate_nonce(); 1326 assert!(!nonce1.is_empty()); 1327 assert!(!nonce2.is_empty()); 1328} 1329#[test] 1330fn test_dpop_nonce_validation_success() { 1331 let secret = b"test-dpop-secret-32-bytes-long!!"; 1332 let verifier = DPoPVerifier::new(secret); 1333 let nonce = verifier.generate_nonce(); 1334 let result = verifier.validate_nonce(&nonce); 1335 assert!(result.is_ok(), "Valid nonce should pass: {:?}", result); 1336} 1337#[test] 1338fn test_dpop_nonce_wrong_secret() { 1339 let secret1 = b"test-dpop-secret-32-bytes-long!!"; 1340 let secret2 = b"different-secret-32-bytes-long!!"; 1341 let verifier1 = DPoPVerifier::new(secret1); 1342 let verifier2 = DPoPVerifier::new(secret2); 1343 let nonce = verifier1.generate_nonce(); 1344 let result = verifier2.validate_nonce(&nonce); 1345 assert!(result.is_err(), "Nonce from different secret should fail"); 1346} 1347#[test] 1348fn test_dpop_nonce_invalid_format() { 1349 let secret = b"test-dpop-secret-32-bytes-long!!"; 1350 let verifier = DPoPVerifier::new(secret); 1351 assert!(verifier.validate_nonce("invalid").is_err()); 1352 assert!(verifier.validate_nonce("").is_err()); 1353 assert!(verifier.validate_nonce("!!!not-base64!!!").is_err()); 1354} 1355#[test] 1356fn test_jwk_thumbprint_ec_p256() { 1357 let jwk = DPoPJwk { 1358 kty: "EC".to_string(), 1359 crv: Some("P-256".to_string()), 1360 x: Some("WbbXrPhtCg66wuF0NLhzXxF5PFzNZ7wNJm9M_1pCcXY".to_string()), 1361 y: Some("DubR6_2kU1H5EYhbcNpYZGy1EY6GEKKxv6PYx8VW0rA".to_string()), 1362 }; 1363 let thumbprint = compute_jwk_thumbprint(&jwk); 1364 assert!(thumbprint.is_ok()); 1365 let tp = thumbprint.unwrap(); 1366 assert!(!tp.is_empty()); 1367 assert!(tp.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_')); 1368} 1369#[test] 1370fn test_jwk_thumbprint_ec_secp256k1() { 1371 let jwk = DPoPJwk { 1372 kty: "EC".to_string(), 1373 crv: Some("secp256k1".to_string()), 1374 x: Some("some_x_value".to_string()), 1375 y: Some("some_y_value".to_string()), 1376 }; 1377 let thumbprint = compute_jwk_thumbprint(&jwk); 1378 assert!(thumbprint.is_ok()); 1379} 1380#[test] 1381fn test_jwk_thumbprint_okp_ed25519() { 1382 let jwk = DPoPJwk { 1383 kty: "OKP".to_string(), 1384 crv: Some("Ed25519".to_string()), 1385 x: Some("some_x_value".to_string()), 1386 y: None, 1387 }; 1388 let thumbprint = compute_jwk_thumbprint(&jwk); 1389 assert!(thumbprint.is_ok()); 1390} 1391#[test] 1392fn test_jwk_thumbprint_missing_crv() { 1393 let jwk = DPoPJwk { 1394 kty: "EC".to_string(), 1395 crv: None, 1396 x: Some("x".to_string()), 1397 y: Some("y".to_string()), 1398 }; 1399 let thumbprint = compute_jwk_thumbprint(&jwk); 1400 assert!(thumbprint.is_err()); 1401} 1402#[test] 1403fn test_jwk_thumbprint_missing_x() { 1404 let jwk = DPoPJwk { 1405 kty: "EC".to_string(), 1406 crv: Some("P-256".to_string()), 1407 x: None, 1408 y: Some("y".to_string()), 1409 }; 1410 let thumbprint = compute_jwk_thumbprint(&jwk); 1411 assert!(thumbprint.is_err()); 1412} 1413#[test] 1414fn test_jwk_thumbprint_missing_y_for_ec() { 1415 let jwk = DPoPJwk { 1416 kty: "EC".to_string(), 1417 crv: Some("P-256".to_string()), 1418 x: Some("x".to_string()), 1419 y: None, 1420 }; 1421 let thumbprint = compute_jwk_thumbprint(&jwk); 1422 assert!(thumbprint.is_err()); 1423} 1424#[test] 1425fn test_jwk_thumbprint_unsupported_key_type() { 1426 let jwk = DPoPJwk { 1427 kty: "RSA".to_string(), 1428 crv: None, 1429 x: None, 1430 y: None, 1431 }; 1432 let thumbprint = compute_jwk_thumbprint(&jwk); 1433 assert!(thumbprint.is_err()); 1434} 1435#[test] 1436fn test_jwk_thumbprint_deterministic() { 1437 let jwk = DPoPJwk { 1438 kty: "EC".to_string(), 1439 crv: Some("P-256".to_string()), 1440 x: Some("WbbXrPhtCg66wuF0NLhzXxF5PFzNZ7wNJm9M_1pCcXY".to_string()), 1441 y: Some("DubR6_2kU1H5EYhbcNpYZGy1EY6GEKKxv6PYx8VW0rA".to_string()), 1442 }; 1443 let tp1 = compute_jwk_thumbprint(&jwk).unwrap(); 1444 let tp2 = compute_jwk_thumbprint(&jwk).unwrap(); 1445 assert_eq!(tp1, tp2, "Thumbprint should be deterministic"); 1446} 1447#[test] 1448fn test_dpop_proof_invalid_format() { 1449 let secret = b"test-dpop-secret-32-bytes-long!!"; 1450 let verifier = DPoPVerifier::new(secret); 1451 let result = verifier.verify_proof("not.enough.parts", "POST", "https://example.com", None); 1452 assert!(result.is_err()); 1453 let result = verifier.verify_proof("invalid", "POST", "https://example.com", None); 1454 assert!(result.is_err()); 1455} 1456#[test] 1457fn test_dpop_proof_invalid_typ() { 1458 let secret = b"test-dpop-secret-32-bytes-long!!"; 1459 let verifier = DPoPVerifier::new(secret); 1460 let header = json!({ 1461 "typ": "JWT", 1462 "alg": "ES256", 1463 "jwk": { 1464 "kty": "EC", 1465 "crv": "P-256", 1466 "x": "x", 1467 "y": "y" 1468 } 1469 }); 1470 let payload = json!({ 1471 "jti": "unique", 1472 "htm": "POST", 1473 "htu": "https://example.com", 1474 "iat": Utc::now().timestamp() 1475 }); 1476 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 1477 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 1478 let proof = format!("{}.{}.sig", header_b64, payload_b64); 1479 let result = verifier.verify_proof(&proof, "POST", "https://example.com", None); 1480 assert!(result.is_err()); 1481} 1482#[test] 1483fn test_dpop_proof_method_mismatch() { 1484 let secret = b"test-dpop-secret-32-bytes-long!!"; 1485 let verifier = DPoPVerifier::new(secret); 1486 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0); 1487 let result = verifier.verify_proof(&proof, "GET", "https://example.com/token", None); 1488 assert!(result.is_err()); 1489} 1490#[test] 1491fn test_dpop_proof_uri_mismatch() { 1492 let secret = b"test-dpop-secret-32-bytes-long!!"; 1493 let verifier = DPoPVerifier::new(secret); 1494 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0); 1495 let result = verifier.verify_proof(&proof, "POST", "https://other.com/token", None); 1496 assert!(result.is_err()); 1497} 1498#[test] 1499fn test_dpop_proof_iat_too_old() { 1500 let secret = b"test-dpop-secret-32-bytes-long!!"; 1501 let verifier = DPoPVerifier::new(secret); 1502 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, -600); 1503 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None); 1504 assert!(result.is_err()); 1505} 1506#[test] 1507fn test_dpop_proof_iat_future() { 1508 let secret = b"test-dpop-secret-32-bytes-long!!"; 1509 let verifier = DPoPVerifier::new(secret); 1510 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 600); 1511 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None); 1512 assert!(result.is_err()); 1513} 1514#[test] 1515fn test_dpop_proof_ath_mismatch() { 1516 let secret = b"test-dpop-secret-32-bytes-long!!"; 1517 let verifier = DPoPVerifier::new(secret); 1518 let proof = create_dpop_proof( 1519 "GET", 1520 "https://example.com/resource", 1521 None, 1522 Some("wrong_hash"), 1523 0, 1524 ); 1525 let result = verifier.verify_proof( 1526 &proof, 1527 "GET", 1528 "https://example.com/resource", 1529 Some("correct_hash"), 1530 ); 1531 assert!(result.is_err()); 1532} 1533#[test] 1534fn test_dpop_proof_missing_ath_when_required() { 1535 let secret = b"test-dpop-secret-32-bytes-long!!"; 1536 let verifier = DPoPVerifier::new(secret); 1537 let proof = create_dpop_proof("GET", "https://example.com/resource", None, None, 0); 1538 let result = verifier.verify_proof( 1539 &proof, 1540 "GET", 1541 "https://example.com/resource", 1542 Some("expected_hash"), 1543 ); 1544 assert!(result.is_err()); 1545} 1546#[test] 1547fn test_dpop_proof_uri_ignores_query_params() { 1548 let secret = b"test-dpop-secret-32-bytes-long!!"; 1549 let verifier = DPoPVerifier::new(secret); 1550 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0); 1551 let result = verifier.verify_proof( 1552 &proof, 1553 "POST", 1554 "https://example.com/token?foo=bar", 1555 None, 1556 ); 1557 assert!(result.is_ok(), "Query params should be ignored: {:?}", result); 1558}