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