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