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