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