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