this repo has no description
1mod common; 2mod helpers; 3 4use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 5use chrono::Utc; 6use common::{base_url, client, create_account_and_login}; 7use reqwest::{redirect, StatusCode}; 8use serde_json::{json, Value}; 9use sha2::{Digest, Sha256}; 10use wiremock::{Mock, MockServer, ResponseTemplate}; 11use wiremock::matchers::{method, path}; 12 13fn no_redirect_client() -> reqwest::Client { 14 reqwest::Client::builder() 15 .redirect(redirect::Policy::none()) 16 .build() 17 .unwrap() 18} 19 20fn generate_pkce() -> (String, String) { 21 let verifier_bytes: [u8; 32] = rand::random(); 22 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes); 23 24 let mut hasher = Sha256::new(); 25 hasher.update(code_verifier.as_bytes()); 26 let hash = hasher.finalize(); 27 let code_challenge = URL_SAFE_NO_PAD.encode(&hash); 28 29 (code_verifier, code_challenge) 30} 31 32async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer { 33 let mock_server = MockServer::start().await; 34 35 let client_id = mock_server.uri(); 36 let metadata = json!({ 37 "client_id": client_id, 38 "client_name": "Test OAuth 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 46 Mock::given(method("GET")) 47 .and(path("/")) 48 .respond_with(ResponseTemplate::new(200).set_body_json(metadata)) 49 .mount(&mock_server) 50 .await; 51 52 mock_server 53} 54 55#[allow(dead_code)] 56async fn setup_mock_dpop_client(redirect_uri: &str) -> MockServer { 57 let mock_server = MockServer::start().await; 58 59 let client_id = mock_server.uri(); 60 let metadata = json!({ 61 "client_id": client_id, 62 "client_name": "DPoP Test Client", 63 "redirect_uris": [redirect_uri], 64 "grant_types": ["authorization_code", "refresh_token"], 65 "response_types": ["code"], 66 "token_endpoint_auth_method": "none", 67 "dpop_bound_access_tokens": true 68 }); 69 70 Mock::given(method("GET")) 71 .and(path("/")) 72 .respond_with(ResponseTemplate::new(200).set_body_json(metadata)) 73 .mount(&mock_server) 74 .await; 75 76 mock_server 77} 78 79#[tokio::test] 80async fn test_oauth_protected_resource_metadata() { 81 let url = base_url().await; 82 let client = client(); 83 84 let res = client 85 .get(format!("{}/.well-known/oauth-protected-resource", url)) 86 .send() 87 .await 88 .expect("Failed to fetch protected resource metadata"); 89 90 assert_eq!(res.status(), StatusCode::OK); 91 92 let body: Value = res.json().await.expect("Invalid JSON"); 93 94 assert!(body["resource"].is_string()); 95 assert!(body["authorization_servers"].is_array()); 96 assert!(body["bearer_methods_supported"].is_array()); 97 98 let bearer_methods = body["bearer_methods_supported"].as_array().unwrap(); 99 assert!(bearer_methods.contains(&json!("header"))); 100} 101 102#[tokio::test] 103async fn test_oauth_authorization_server_metadata() { 104 let url = base_url().await; 105 let client = client(); 106 107 let res = client 108 .get(format!("{}/.well-known/oauth-authorization-server", url)) 109 .send() 110 .await 111 .expect("Failed to fetch authorization server metadata"); 112 113 assert_eq!(res.status(), StatusCode::OK); 114 115 let body: Value = res.json().await.expect("Invalid JSON"); 116 117 assert!(body["issuer"].is_string()); 118 assert!(body["authorization_endpoint"].is_string()); 119 assert!(body["token_endpoint"].is_string()); 120 assert!(body["jwks_uri"].is_string()); 121 122 let response_types = body["response_types_supported"].as_array().unwrap(); 123 assert!(response_types.contains(&json!("code"))); 124 125 let grant_types = body["grant_types_supported"].as_array().unwrap(); 126 assert!(grant_types.contains(&json!("authorization_code"))); 127 assert!(grant_types.contains(&json!("refresh_token"))); 128 129 let code_challenge_methods = body["code_challenge_methods_supported"].as_array().unwrap(); 130 assert!(code_challenge_methods.contains(&json!("S256"))); 131 132 assert_eq!(body["require_pushed_authorization_requests"], json!(true)); 133 134 let dpop_algs = body["dpop_signing_alg_values_supported"].as_array().unwrap(); 135 assert!(dpop_algs.contains(&json!("ES256"))); 136} 137 138#[tokio::test] 139async fn test_oauth_jwks_endpoint() { 140 let url = base_url().await; 141 let client = client(); 142 143 let res = client 144 .get(format!("{}/oauth/jwks", url)) 145 .send() 146 .await 147 .expect("Failed to fetch JWKS"); 148 149 assert_eq!(res.status(), StatusCode::OK); 150 151 let body: Value = res.json().await.expect("Invalid JSON"); 152 assert!(body["keys"].is_array()); 153} 154 155#[tokio::test] 156async fn test_par_success() { 157 let url = base_url().await; 158 let client = client(); 159 160 let redirect_uri = "https://example.com/callback"; 161 let mock_client = setup_mock_client_metadata(redirect_uri).await; 162 let client_id = mock_client.uri(); 163 164 let (_code_verifier, code_challenge) = generate_pkce(); 165 166 let res = client 167 .post(format!("{}/oauth/par", url)) 168 .form(&[ 169 ("response_type", "code"), 170 ("client_id", &client_id), 171 ("redirect_uri", redirect_uri), 172 ("code_challenge", &code_challenge), 173 ("code_challenge_method", "S256"), 174 ("scope", "atproto"), 175 ("state", "test-state-123"), 176 ]) 177 .send() 178 .await 179 .expect("Failed to send PAR request"); 180 181 assert_eq!(res.status(), StatusCode::OK, "PAR should succeed: {:?}", res.text().await); 182 183 let body: Value = client 184 .post(format!("{}/oauth/par", url)) 185 .form(&[ 186 ("response_type", "code"), 187 ("client_id", &client_id), 188 ("redirect_uri", redirect_uri), 189 ("code_challenge", &code_challenge), 190 ("code_challenge_method", "S256"), 191 ("scope", "atproto"), 192 ("state", "test-state-123"), 193 ]) 194 .send() 195 .await 196 .unwrap() 197 .json() 198 .await 199 .expect("Invalid JSON"); 200 201 assert!(body["request_uri"].is_string()); 202 assert!(body["expires_in"].is_number()); 203 204 let request_uri = body["request_uri"].as_str().unwrap(); 205 assert!(request_uri.starts_with("urn:ietf:params:oauth:request_uri:")); 206} 207 208#[tokio::test] 209async fn test_par_requires_pkce() { 210 let url = base_url().await; 211 let client = client(); 212 213 let redirect_uri = "https://example.com/callback"; 214 let mock_client = setup_mock_client_metadata(redirect_uri).await; 215 let client_id = mock_client.uri(); 216 217 let res = client 218 .post(format!("{}/oauth/par", url)) 219 .form(&[ 220 ("response_type", "code"), 221 ("client_id", &client_id), 222 ("redirect_uri", redirect_uri), 223 ("scope", "atproto"), 224 ]) 225 .send() 226 .await 227 .expect("Failed to send PAR request"); 228 229 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 230 231 let body: Value = res.json().await.expect("Invalid JSON"); 232 assert_eq!(body["error"], "invalid_request"); 233} 234 235#[tokio::test] 236async fn test_par_requires_s256() { 237 let url = base_url().await; 238 let client = client(); 239 240 let redirect_uri = "https://example.com/callback"; 241 let mock_client = setup_mock_client_metadata(redirect_uri).await; 242 let client_id = mock_client.uri(); 243 244 let res = client 245 .post(format!("{}/oauth/par", url)) 246 .form(&[ 247 ("response_type", "code"), 248 ("client_id", &client_id), 249 ("redirect_uri", redirect_uri), 250 ("code_challenge", "test-challenge"), 251 ("code_challenge_method", "plain"), 252 ]) 253 .send() 254 .await 255 .expect("Failed to send PAR request"); 256 257 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 258 259 let body: Value = res.json().await.expect("Invalid JSON"); 260 assert_eq!(body["error"], "invalid_request"); 261 assert!(body["error_description"].as_str().unwrap().contains("S256")); 262} 263 264#[tokio::test] 265async fn test_par_validates_redirect_uri() { 266 let url = base_url().await; 267 let client = client(); 268 269 let registered_redirect = "https://example.com/callback"; 270 let wrong_redirect = "https://evil.com/steal"; 271 let mock_client = setup_mock_client_metadata(registered_redirect).await; 272 let client_id = mock_client.uri(); 273 274 let (_, code_challenge) = generate_pkce(); 275 276 let res = client 277 .post(format!("{}/oauth/par", url)) 278 .form(&[ 279 ("response_type", "code"), 280 ("client_id", &client_id), 281 ("redirect_uri", wrong_redirect), 282 ("code_challenge", &code_challenge), 283 ("code_challenge_method", "S256"), 284 ]) 285 .send() 286 .await 287 .expect("Failed to send PAR request"); 288 289 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 290 291 let body: Value = res.json().await.expect("Invalid JSON"); 292 assert_eq!(body["error"], "invalid_request"); 293} 294 295#[tokio::test] 296async fn test_authorize_get_with_valid_request_uri() { 297 let url = base_url().await; 298 let client = client(); 299 300 let redirect_uri = "https://example.com/callback"; 301 let mock_client = setup_mock_client_metadata(redirect_uri).await; 302 let client_id = mock_client.uri(); 303 304 let (_, code_challenge) = generate_pkce(); 305 306 let par_res = client 307 .post(format!("{}/oauth/par", url)) 308 .form(&[ 309 ("response_type", "code"), 310 ("client_id", &client_id), 311 ("redirect_uri", redirect_uri), 312 ("code_challenge", &code_challenge), 313 ("code_challenge_method", "S256"), 314 ("scope", "atproto"), 315 ("state", "test-state"), 316 ]) 317 .send() 318 .await 319 .expect("PAR failed"); 320 321 let par_body: Value = par_res.json().await.expect("Invalid PAR JSON"); 322 let request_uri = par_body["request_uri"].as_str().unwrap(); 323 324 let auth_res = client 325 .get(format!("{}/oauth/authorize", url)) 326 .header("Accept", "application/json") 327 .query(&[("request_uri", request_uri)]) 328 .send() 329 .await 330 .expect("Authorize GET failed"); 331 332 assert_eq!(auth_res.status(), StatusCode::OK); 333 334 let auth_body: Value = auth_res.json().await.expect("Invalid auth JSON"); 335 assert_eq!(auth_body["client_id"], client_id); 336 assert_eq!(auth_body["redirect_uri"], redirect_uri); 337 assert_eq!(auth_body["scope"], "atproto"); 338 assert_eq!(auth_body["state"], "test-state"); 339} 340 341#[tokio::test] 342async fn test_authorize_rejects_invalid_request_uri() { 343 let url = base_url().await; 344 let client = client(); 345 346 let res = client 347 .get(format!("{}/oauth/authorize", url)) 348 .header("Accept", "application/json") 349 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:nonexistent")]) 350 .send() 351 .await 352 .expect("Request failed"); 353 354 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 355 356 let body: Value = res.json().await.expect("Invalid JSON"); 357 assert_eq!(body["error"], "invalid_request"); 358} 359 360#[tokio::test] 361async fn test_authorize_requires_request_uri() { 362 let url = base_url().await; 363 let client = client(); 364 365 let res = client 366 .get(format!("{}/oauth/authorize", url)) 367 .send() 368 .await 369 .expect("Request failed"); 370 371 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 372} 373 374#[tokio::test] 375async fn test_full_oauth_flow_without_dpop() { 376 let url = base_url().await; 377 let http_client = client(); 378 379 let (_, _user_did) = create_account_and_login(&http_client).await; 380 381 let ts = Utc::now().timestamp_millis(); 382 let handle = format!("oauth-test-{}", ts); 383 let email = format!("oauth-test-{}@example.com", ts); 384 let password = "oauth-test-password"; 385 386 let create_res = http_client 387 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 388 .json(&json!({ 389 "handle": handle, 390 "email": email, 391 "password": password 392 })) 393 .send() 394 .await 395 .expect("Account creation failed"); 396 397 assert_eq!(create_res.status(), StatusCode::OK); 398 let account: Value = create_res.json().await.unwrap(); 399 let user_did = account["did"].as_str().unwrap(); 400 401 let redirect_uri = "https://example.com/oauth/callback"; 402 let mock_client = setup_mock_client_metadata(redirect_uri).await; 403 let client_id = mock_client.uri(); 404 405 let (code_verifier, code_challenge) = generate_pkce(); 406 let state = format!("state-{}", ts); 407 408 let par_res = http_client 409 .post(format!("{}/oauth/par", url)) 410 .form(&[ 411 ("response_type", "code"), 412 ("client_id", &client_id), 413 ("redirect_uri", redirect_uri), 414 ("code_challenge", &code_challenge), 415 ("code_challenge_method", "S256"), 416 ("scope", "atproto"), 417 ("state", &state), 418 ]) 419 .send() 420 .await 421 .expect("PAR failed"); 422 423 let par_status = par_res.status(); 424 let par_text = par_res.text().await.unwrap_or_default(); 425 if par_status != StatusCode::OK { 426 panic!("PAR failed with status {}: {}", par_status, par_text); 427 } 428 let par_body: Value = serde_json::from_str(&par_text).unwrap(); 429 let request_uri = par_body["request_uri"].as_str().unwrap(); 430 431 let auth_client = no_redirect_client(); 432 let auth_res = auth_client 433 .post(format!("{}/oauth/authorize", url)) 434 .form(&[ 435 ("request_uri", request_uri), 436 ("username", &handle), 437 ("password", password), 438 ("remember_device", "false"), 439 ]) 440 .send() 441 .await 442 .expect("Authorize POST failed"); 443 444 let auth_status = auth_res.status(); 445 if auth_status != StatusCode::TEMPORARY_REDIRECT 446 && auth_status != StatusCode::SEE_OTHER 447 && auth_status != StatusCode::FOUND 448 { 449 let auth_text = auth_res.text().await.unwrap_or_default(); 450 panic!( 451 "Expected redirect, got {}: {}", 452 auth_status, auth_text 453 ); 454 } 455 456 let location = auth_res.headers().get("location") 457 .expect("No Location header") 458 .to_str() 459 .unwrap(); 460 461 assert!(location.starts_with(redirect_uri), "Redirect to wrong URI: {}", location); 462 assert!(location.contains("code="), "No code in redirect: {}", location); 463 assert!(location.contains(&format!("state={}", state)), "Wrong state in redirect"); 464 465 let code = location 466 .split("code=") 467 .nth(1) 468 .unwrap() 469 .split('&') 470 .next() 471 .unwrap(); 472 473 let token_res = http_client 474 .post(format!("{}/oauth/token", url)) 475 .form(&[ 476 ("grant_type", "authorization_code"), 477 ("code", code), 478 ("redirect_uri", redirect_uri), 479 ("code_verifier", &code_verifier), 480 ("client_id", &client_id), 481 ]) 482 .send() 483 .await 484 .expect("Token request failed"); 485 486 let token_status = token_res.status(); 487 let token_text = token_res.text().await.unwrap_or_default(); 488 if token_status != StatusCode::OK { 489 panic!("Token request failed with status {}: {}", token_status, token_text); 490 } 491 492 let token_body: Value = serde_json::from_str(&token_text).unwrap(); 493 494 assert!(token_body["access_token"].is_string()); 495 assert!(token_body["refresh_token"].is_string()); 496 assert_eq!(token_body["token_type"], "Bearer"); 497 assert!(token_body["expires_in"].is_number()); 498 assert_eq!(token_body["sub"], user_did); 499} 500 501#[tokio::test] 502async fn test_token_refresh_flow() { 503 let url = base_url().await; 504 let http_client = client(); 505 506 let ts = Utc::now().timestamp_millis(); 507 let handle = format!("refresh-test-{}", ts); 508 let email = format!("refresh-test-{}@example.com", ts); 509 let password = "refresh-test-password"; 510 511 http_client 512 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 513 .json(&json!({ 514 "handle": handle, 515 "email": email, 516 "password": password 517 })) 518 .send() 519 .await 520 .expect("Account creation failed"); 521 522 let redirect_uri = "https://example.com/refresh-callback"; 523 let mock_client = setup_mock_client_metadata(redirect_uri).await; 524 let client_id = mock_client.uri(); 525 526 let (code_verifier, code_challenge) = generate_pkce(); 527 528 let par_body: Value = http_client 529 .post(format!("{}/oauth/par", url)) 530 .form(&[ 531 ("response_type", "code"), 532 ("client_id", &client_id), 533 ("redirect_uri", redirect_uri), 534 ("code_challenge", &code_challenge), 535 ("code_challenge_method", "S256"), 536 ]) 537 .send() 538 .await 539 .unwrap() 540 .json() 541 .await 542 .unwrap(); 543 544 let request_uri = par_body["request_uri"].as_str().unwrap(); 545 546 let auth_client = no_redirect_client(); 547 let auth_res = auth_client 548 .post(format!("{}/oauth/authorize", url)) 549 .form(&[ 550 ("request_uri", request_uri), 551 ("username", &handle), 552 ("password", password), 553 ("remember_device", "false"), 554 ]) 555 .send() 556 .await 557 .unwrap(); 558 559 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 560 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 561 562 let token_body: Value = http_client 563 .post(format!("{}/oauth/token", url)) 564 .form(&[ 565 ("grant_type", "authorization_code"), 566 ("code", code), 567 ("redirect_uri", redirect_uri), 568 ("code_verifier", &code_verifier), 569 ("client_id", &client_id), 570 ]) 571 .send() 572 .await 573 .unwrap() 574 .json() 575 .await 576 .unwrap(); 577 578 let refresh_token = token_body["refresh_token"].as_str().unwrap(); 579 let original_access_token = token_body["access_token"].as_str().unwrap(); 580 581 let refresh_res = http_client 582 .post(format!("{}/oauth/token", url)) 583 .form(&[ 584 ("grant_type", "refresh_token"), 585 ("refresh_token", refresh_token), 586 ("client_id", &client_id), 587 ]) 588 .send() 589 .await 590 .expect("Refresh request failed"); 591 592 assert_eq!(refresh_res.status(), StatusCode::OK); 593 594 let refresh_body: Value = refresh_res.json().await.unwrap(); 595 596 assert!(refresh_body["access_token"].is_string()); 597 assert!(refresh_body["refresh_token"].is_string()); 598 599 let new_access_token = refresh_body["access_token"].as_str().unwrap(); 600 let new_refresh_token = refresh_body["refresh_token"].as_str().unwrap(); 601 602 assert_ne!(new_access_token, original_access_token, "Access token should rotate"); 603 assert_ne!(new_refresh_token, refresh_token, "Refresh token should rotate"); 604} 605 606#[tokio::test] 607async fn test_refresh_token_reuse_detection() { 608 let url = base_url().await; 609 let http_client = client(); 610 611 let ts = Utc::now().timestamp_millis(); 612 let handle = format!("reuse-test-{}", ts); 613 let email = format!("reuse-test-{}@example.com", ts); 614 let password = "reuse-test-password"; 615 616 http_client 617 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 618 .json(&json!({ 619 "handle": handle, 620 "email": email, 621 "password": password 622 })) 623 .send() 624 .await 625 .unwrap(); 626 627 let redirect_uri = "https://example.com/reuse-callback"; 628 let mock_client = setup_mock_client_metadata(redirect_uri).await; 629 let client_id = mock_client.uri(); 630 631 let (code_verifier, code_challenge) = generate_pkce(); 632 633 let par_body: Value = http_client 634 .post(format!("{}/oauth/par", url)) 635 .form(&[ 636 ("response_type", "code"), 637 ("client_id", &client_id), 638 ("redirect_uri", redirect_uri), 639 ("code_challenge", &code_challenge), 640 ("code_challenge_method", "S256"), 641 ]) 642 .send() 643 .await 644 .unwrap() 645 .json() 646 .await 647 .unwrap(); 648 649 let request_uri = par_body["request_uri"].as_str().unwrap(); 650 651 let auth_client = no_redirect_client(); 652 let auth_res = auth_client 653 .post(format!("{}/oauth/authorize", url)) 654 .form(&[ 655 ("request_uri", request_uri), 656 ("username", &handle), 657 ("password", password), 658 ("remember_device", "false"), 659 ]) 660 .send() 661 .await 662 .unwrap(); 663 664 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 665 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 666 667 let token_body: Value = http_client 668 .post(format!("{}/oauth/token", url)) 669 .form(&[ 670 ("grant_type", "authorization_code"), 671 ("code", code), 672 ("redirect_uri", redirect_uri), 673 ("code_verifier", &code_verifier), 674 ("client_id", &client_id), 675 ]) 676 .send() 677 .await 678 .unwrap() 679 .json() 680 .await 681 .unwrap(); 682 683 let original_refresh_token = token_body["refresh_token"].as_str().unwrap().to_string(); 684 685 let first_refresh: Value = http_client 686 .post(format!("{}/oauth/token", url)) 687 .form(&[ 688 ("grant_type", "refresh_token"), 689 ("refresh_token", &original_refresh_token), 690 ("client_id", &client_id), 691 ]) 692 .send() 693 .await 694 .unwrap() 695 .json() 696 .await 697 .unwrap(); 698 699 assert!(first_refresh["access_token"].is_string(), "First refresh should succeed"); 700 701 let reuse_res = http_client 702 .post(format!("{}/oauth/token", url)) 703 .form(&[ 704 ("grant_type", "refresh_token"), 705 ("refresh_token", &original_refresh_token), 706 ("client_id", &client_id), 707 ]) 708 .send() 709 .await 710 .unwrap(); 711 712 assert_eq!(reuse_res.status(), StatusCode::BAD_REQUEST, "Reuse should be rejected"); 713 714 let reuse_body: Value = reuse_res.json().await.unwrap(); 715 assert_eq!(reuse_body["error"], "invalid_grant"); 716 assert!( 717 reuse_body["error_description"].as_str().unwrap().to_lowercase().contains("reuse"), 718 "Error should mention reuse" 719 ); 720} 721 722#[tokio::test] 723async fn test_pkce_verification() { 724 let url = base_url().await; 725 let http_client = client(); 726 727 let ts = Utc::now().timestamp_millis(); 728 let handle = format!("pkce-test-{}", ts); 729 let email = format!("pkce-test-{}@example.com", ts); 730 let password = "pkce-test-password"; 731 732 http_client 733 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 734 .json(&json!({ 735 "handle": handle, 736 "email": email, 737 "password": password 738 })) 739 .send() 740 .await 741 .unwrap(); 742 743 let redirect_uri = "https://example.com/pkce-callback"; 744 let mock_client = setup_mock_client_metadata(redirect_uri).await; 745 let client_id = mock_client.uri(); 746 747 let (_, code_challenge) = generate_pkce(); 748 let wrong_verifier = "wrong-code-verifier-that-does-not-match"; 749 750 let par_body: Value = http_client 751 .post(format!("{}/oauth/par", url)) 752 .form(&[ 753 ("response_type", "code"), 754 ("client_id", &client_id), 755 ("redirect_uri", redirect_uri), 756 ("code_challenge", &code_challenge), 757 ("code_challenge_method", "S256"), 758 ]) 759 .send() 760 .await 761 .unwrap() 762 .json() 763 .await 764 .unwrap(); 765 766 let request_uri = par_body["request_uri"].as_str().unwrap(); 767 768 let auth_client = no_redirect_client(); 769 let auth_res = auth_client 770 .post(format!("{}/oauth/authorize", url)) 771 .form(&[ 772 ("request_uri", request_uri), 773 ("username", &handle), 774 ("password", password), 775 ("remember_device", "false"), 776 ]) 777 .send() 778 .await 779 .unwrap(); 780 781 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 782 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 783 784 let token_res = http_client 785 .post(format!("{}/oauth/token", url)) 786 .form(&[ 787 ("grant_type", "authorization_code"), 788 ("code", code), 789 ("redirect_uri", redirect_uri), 790 ("code_verifier", wrong_verifier), 791 ("client_id", &client_id), 792 ]) 793 .send() 794 .await 795 .unwrap(); 796 797 assert_eq!(token_res.status(), StatusCode::BAD_REQUEST); 798 799 let token_body: Value = token_res.json().await.unwrap(); 800 assert_eq!(token_body["error"], "invalid_grant"); 801 assert!(token_body["error_description"].as_str().unwrap().contains("PKCE")); 802} 803 804#[tokio::test] 805async fn test_authorization_code_cannot_be_reused() { 806 let url = base_url().await; 807 let http_client = client(); 808 809 let ts = Utc::now().timestamp_millis(); 810 let handle = format!("code-reuse-{}", ts); 811 let email = format!("code-reuse-{}@example.com", ts); 812 let password = "code-reuse-password"; 813 814 http_client 815 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 816 .json(&json!({ 817 "handle": handle, 818 "email": email, 819 "password": password 820 })) 821 .send() 822 .await 823 .unwrap(); 824 825 let redirect_uri = "https://example.com/code-reuse-callback"; 826 let mock_client = setup_mock_client_metadata(redirect_uri).await; 827 let client_id = mock_client.uri(); 828 829 let (code_verifier, code_challenge) = generate_pkce(); 830 831 let par_body: Value = http_client 832 .post(format!("{}/oauth/par", url)) 833 .form(&[ 834 ("response_type", "code"), 835 ("client_id", &client_id), 836 ("redirect_uri", redirect_uri), 837 ("code_challenge", &code_challenge), 838 ("code_challenge_method", "S256"), 839 ]) 840 .send() 841 .await 842 .unwrap() 843 .json() 844 .await 845 .unwrap(); 846 847 let request_uri = par_body["request_uri"].as_str().unwrap(); 848 849 let auth_client = no_redirect_client(); 850 let auth_res = auth_client 851 .post(format!("{}/oauth/authorize", url)) 852 .form(&[ 853 ("request_uri", request_uri), 854 ("username", &handle), 855 ("password", password), 856 ("remember_device", "false"), 857 ]) 858 .send() 859 .await 860 .unwrap(); 861 862 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 863 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 864 865 let first_token_res = http_client 866 .post(format!("{}/oauth/token", url)) 867 .form(&[ 868 ("grant_type", "authorization_code"), 869 ("code", code), 870 ("redirect_uri", redirect_uri), 871 ("code_verifier", &code_verifier), 872 ("client_id", &client_id), 873 ]) 874 .send() 875 .await 876 .unwrap(); 877 878 assert_eq!(first_token_res.status(), StatusCode::OK, "First use should succeed"); 879 880 let second_token_res = http_client 881 .post(format!("{}/oauth/token", url)) 882 .form(&[ 883 ("grant_type", "authorization_code"), 884 ("code", code), 885 ("redirect_uri", redirect_uri), 886 ("code_verifier", &code_verifier), 887 ("client_id", &client_id), 888 ]) 889 .send() 890 .await 891 .unwrap(); 892 893 assert_eq!(second_token_res.status(), StatusCode::BAD_REQUEST, "Second use should fail"); 894 895 let error_body: Value = second_token_res.json().await.unwrap(); 896 assert_eq!(error_body["error"], "invalid_grant"); 897} 898 899#[tokio::test] 900async fn test_wrong_credentials_denied() { 901 let url = base_url().await; 902 let http_client = client(); 903 904 let ts = Utc::now().timestamp_millis(); 905 let handle = format!("wrong-creds-{}", ts); 906 let email = format!("wrong-creds-{}@example.com", ts); 907 let password = "correct-password"; 908 909 http_client 910 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 911 .json(&json!({ 912 "handle": handle, 913 "email": email, 914 "password": password 915 })) 916 .send() 917 .await 918 .unwrap(); 919 920 let redirect_uri = "https://example.com/wrong-creds-callback"; 921 let mock_client = setup_mock_client_metadata(redirect_uri).await; 922 let client_id = mock_client.uri(); 923 924 let (_, code_challenge) = generate_pkce(); 925 926 let par_body: Value = http_client 927 .post(format!("{}/oauth/par", url)) 928 .form(&[ 929 ("response_type", "code"), 930 ("client_id", &client_id), 931 ("redirect_uri", redirect_uri), 932 ("code_challenge", &code_challenge), 933 ("code_challenge_method", "S256"), 934 ]) 935 .send() 936 .await 937 .unwrap() 938 .json() 939 .await 940 .unwrap(); 941 942 let request_uri = par_body["request_uri"].as_str().unwrap(); 943 944 let auth_res = http_client 945 .post(format!("{}/oauth/authorize", url)) 946 .header("Accept", "application/json") 947 .form(&[ 948 ("request_uri", request_uri), 949 ("username", &handle), 950 ("password", "wrong-password"), 951 ("remember_device", "false"), 952 ]) 953 .send() 954 .await 955 .unwrap(); 956 957 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN); 958 959 let error_body: Value = auth_res.json().await.unwrap(); 960 assert_eq!(error_body["error"], "access_denied"); 961} 962 963#[tokio::test] 964async fn test_token_revocation() { 965 let url = base_url().await; 966 let http_client = client(); 967 968 let ts = Utc::now().timestamp_millis(); 969 let handle = format!("revoke-test-{}", ts); 970 let email = format!("revoke-test-{}@example.com", ts); 971 let password = "revoke-test-password"; 972 973 http_client 974 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 975 .json(&json!({ 976 "handle": handle, 977 "email": email, 978 "password": password 979 })) 980 .send() 981 .await 982 .unwrap(); 983 984 let redirect_uri = "https://example.com/revoke-callback"; 985 let mock_client = setup_mock_client_metadata(redirect_uri).await; 986 let client_id = mock_client.uri(); 987 988 let (code_verifier, code_challenge) = generate_pkce(); 989 990 let par_body: Value = http_client 991 .post(format!("{}/oauth/par", url)) 992 .form(&[ 993 ("response_type", "code"), 994 ("client_id", &client_id), 995 ("redirect_uri", redirect_uri), 996 ("code_challenge", &code_challenge), 997 ("code_challenge_method", "S256"), 998 ]) 999 .send() 1000 .await 1001 .unwrap() 1002 .json() 1003 .await 1004 .unwrap(); 1005 1006 let request_uri = par_body["request_uri"].as_str().unwrap(); 1007 1008 let auth_client = no_redirect_client(); 1009 let auth_res = auth_client 1010 .post(format!("{}/oauth/authorize", url)) 1011 .form(&[ 1012 ("request_uri", request_uri), 1013 ("username", &handle), 1014 ("password", password), 1015 ("remember_device", "false"), 1016 ]) 1017 .send() 1018 .await 1019 .unwrap(); 1020 1021 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 1022 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 1023 1024 let token_body: Value = http_client 1025 .post(format!("{}/oauth/token", url)) 1026 .form(&[ 1027 ("grant_type", "authorization_code"), 1028 ("code", code), 1029 ("redirect_uri", redirect_uri), 1030 ("code_verifier", &code_verifier), 1031 ("client_id", &client_id), 1032 ]) 1033 .send() 1034 .await 1035 .unwrap() 1036 .json() 1037 .await 1038 .unwrap(); 1039 1040 let refresh_token = token_body["refresh_token"].as_str().unwrap(); 1041 1042 let revoke_res = http_client 1043 .post(format!("{}/oauth/revoke", url)) 1044 .form(&[("token", refresh_token)]) 1045 .send() 1046 .await 1047 .unwrap(); 1048 1049 assert_eq!(revoke_res.status(), StatusCode::OK); 1050 1051 let refresh_after_revoke = http_client 1052 .post(format!("{}/oauth/token", url)) 1053 .form(&[ 1054 ("grant_type", "refresh_token"), 1055 ("refresh_token", refresh_token), 1056 ("client_id", &client_id), 1057 ]) 1058 .send() 1059 .await 1060 .unwrap(); 1061 1062 assert_eq!(refresh_after_revoke.status(), StatusCode::BAD_REQUEST); 1063} 1064 1065#[tokio::test] 1066async fn test_unsupported_grant_type() { 1067 let url = base_url().await; 1068 let http_client = client(); 1069 1070 let res = http_client 1071 .post(format!("{}/oauth/token", url)) 1072 .form(&[ 1073 ("grant_type", "client_credentials"), 1074 ("client_id", "https://example.com"), 1075 ]) 1076 .send() 1077 .await 1078 .unwrap(); 1079 1080 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 1081 1082 let body: Value = res.json().await.unwrap(); 1083 assert_eq!(body["error"], "unsupported_grant_type"); 1084} 1085 1086#[tokio::test] 1087async fn test_invalid_refresh_token() { 1088 let url = base_url().await; 1089 let http_client = client(); 1090 1091 let res = http_client 1092 .post(format!("{}/oauth/token", url)) 1093 .form(&[ 1094 ("grant_type", "refresh_token"), 1095 ("refresh_token", "invalid-refresh-token"), 1096 ("client_id", "https://example.com"), 1097 ]) 1098 .send() 1099 .await 1100 .unwrap(); 1101 1102 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 1103 1104 let body: Value = res.json().await.unwrap(); 1105 assert_eq!(body["error"], "invalid_grant"); 1106} 1107 1108#[tokio::test] 1109async fn test_deactivated_account_cannot_authorize() { 1110 let url = base_url().await; 1111 let http_client = client(); 1112 1113 let ts = Utc::now().timestamp_millis(); 1114 let handle = format!("deact-oauth-{}", ts); 1115 let email = format!("deact-oauth-{}@example.com", ts); 1116 let password = "deact-oauth-password"; 1117 1118 let create_res = http_client 1119 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1120 .json(&json!({ 1121 "handle": handle, 1122 "email": email, 1123 "password": password 1124 })) 1125 .send() 1126 .await 1127 .unwrap(); 1128 1129 assert_eq!(create_res.status(), StatusCode::OK); 1130 let account: Value = create_res.json().await.unwrap(); 1131 let access_jwt = account["accessJwt"].as_str().unwrap(); 1132 1133 let deact_res = http_client 1134 .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url)) 1135 .header("Authorization", format!("Bearer {}", access_jwt)) 1136 .json(&json!({})) 1137 .send() 1138 .await 1139 .unwrap(); 1140 assert_eq!(deact_res.status(), StatusCode::OK); 1141 1142 let redirect_uri = "https://example.com/deact-callback"; 1143 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1144 let client_id = mock_client.uri(); 1145 1146 let (_, code_challenge) = generate_pkce(); 1147 1148 let par_body: Value = http_client 1149 .post(format!("{}/oauth/par", url)) 1150 .form(&[ 1151 ("response_type", "code"), 1152 ("client_id", &client_id), 1153 ("redirect_uri", redirect_uri), 1154 ("code_challenge", &code_challenge), 1155 ("code_challenge_method", "S256"), 1156 ]) 1157 .send() 1158 .await 1159 .unwrap() 1160 .json() 1161 .await 1162 .unwrap(); 1163 1164 let request_uri = par_body["request_uri"].as_str().unwrap(); 1165 1166 let auth_res = http_client 1167 .post(format!("{}/oauth/authorize", url)) 1168 .header("Accept", "application/json") 1169 .form(&[ 1170 ("request_uri", request_uri), 1171 ("username", &handle), 1172 ("password", password), 1173 ("remember_device", "false"), 1174 ]) 1175 .send() 1176 .await 1177 .unwrap(); 1178 1179 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN, "Deactivated account should not be able to authorize"); 1180 let body: Value = auth_res.json().await.unwrap(); 1181 assert_eq!(body["error"], "access_denied"); 1182} 1183 1184#[tokio::test] 1185async fn test_expired_authorization_request() { 1186 let url = base_url().await; 1187 let http_client = client(); 1188 1189 let res = http_client 1190 .get(format!("{}/oauth/authorize", url)) 1191 .header("Accept", "application/json") 1192 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:expired-or-nonexistent")]) 1193 .send() 1194 .await 1195 .unwrap(); 1196 1197 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 1198 let body: Value = res.json().await.unwrap(); 1199 assert_eq!(body["error"], "invalid_request"); 1200} 1201 1202#[tokio::test] 1203async fn test_token_introspection() { 1204 let url = base_url().await; 1205 let http_client = client(); 1206 1207 let ts = Utc::now().timestamp_millis(); 1208 let handle = format!("introspect-{}", ts); 1209 let email = format!("introspect-{}@example.com", ts); 1210 let password = "introspect-password"; 1211 1212 http_client 1213 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1214 .json(&json!({ 1215 "handle": handle, 1216 "email": email, 1217 "password": password 1218 })) 1219 .send() 1220 .await 1221 .unwrap(); 1222 1223 let redirect_uri = "https://example.com/introspect-callback"; 1224 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1225 let client_id = mock_client.uri(); 1226 1227 let (code_verifier, code_challenge) = generate_pkce(); 1228 1229 let par_body: Value = http_client 1230 .post(format!("{}/oauth/par", url)) 1231 .form(&[ 1232 ("response_type", "code"), 1233 ("client_id", &client_id), 1234 ("redirect_uri", redirect_uri), 1235 ("code_challenge", &code_challenge), 1236 ("code_challenge_method", "S256"), 1237 ]) 1238 .send() 1239 .await 1240 .unwrap() 1241 .json() 1242 .await 1243 .unwrap(); 1244 1245 let request_uri = par_body["request_uri"].as_str().unwrap(); 1246 1247 let auth_client = no_redirect_client(); 1248 let auth_res = auth_client 1249 .post(format!("{}/oauth/authorize", url)) 1250 .form(&[ 1251 ("request_uri", request_uri), 1252 ("username", &handle), 1253 ("password", password), 1254 ("remember_device", "false"), 1255 ]) 1256 .send() 1257 .await 1258 .unwrap(); 1259 1260 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 1261 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 1262 1263 let token_body: Value = http_client 1264 .post(format!("{}/oauth/token", url)) 1265 .form(&[ 1266 ("grant_type", "authorization_code"), 1267 ("code", code), 1268 ("redirect_uri", redirect_uri), 1269 ("code_verifier", &code_verifier), 1270 ("client_id", &client_id), 1271 ]) 1272 .send() 1273 .await 1274 .unwrap() 1275 .json() 1276 .await 1277 .unwrap(); 1278 1279 let access_token = token_body["access_token"].as_str().unwrap(); 1280 1281 let introspect_res = http_client 1282 .post(format!("{}/oauth/introspect", url)) 1283 .form(&[("token", access_token)]) 1284 .send() 1285 .await 1286 .unwrap(); 1287 1288 assert_eq!(introspect_res.status(), StatusCode::OK); 1289 let introspect_body: Value = introspect_res.json().await.unwrap(); 1290 assert_eq!(introspect_body["active"], true); 1291 assert!(introspect_body["client_id"].is_string()); 1292 assert!(introspect_body["exp"].is_number()); 1293} 1294 1295#[tokio::test] 1296async fn test_introspect_invalid_token() { 1297 let url = base_url().await; 1298 let http_client = client(); 1299 1300 let res = http_client 1301 .post(format!("{}/oauth/introspect", url)) 1302 .form(&[("token", "invalid.token.here")]) 1303 .send() 1304 .await 1305 .unwrap(); 1306 1307 assert_eq!(res.status(), StatusCode::OK); 1308 let body: Value = res.json().await.unwrap(); 1309 assert_eq!(body["active"], false); 1310} 1311 1312#[tokio::test] 1313async fn test_introspect_revoked_token() { 1314 let url = base_url().await; 1315 let http_client = client(); 1316 1317 let ts = Utc::now().timestamp_millis(); 1318 let handle = format!("introspect-revoked-{}", ts); 1319 let email = format!("introspect-revoked-{}@example.com", ts); 1320 let password = "introspect-revoked-password"; 1321 1322 http_client 1323 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1324 .json(&json!({ 1325 "handle": handle, 1326 "email": email, 1327 "password": password 1328 })) 1329 .send() 1330 .await 1331 .unwrap(); 1332 1333 let redirect_uri = "https://example.com/introspect-revoked-callback"; 1334 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1335 let client_id = mock_client.uri(); 1336 1337 let (code_verifier, code_challenge) = generate_pkce(); 1338 1339 let par_body: Value = http_client 1340 .post(format!("{}/oauth/par", url)) 1341 .form(&[ 1342 ("response_type", "code"), 1343 ("client_id", &client_id), 1344 ("redirect_uri", redirect_uri), 1345 ("code_challenge", &code_challenge), 1346 ("code_challenge_method", "S256"), 1347 ]) 1348 .send() 1349 .await 1350 .unwrap() 1351 .json() 1352 .await 1353 .unwrap(); 1354 1355 let request_uri = par_body["request_uri"].as_str().unwrap(); 1356 1357 let auth_client = no_redirect_client(); 1358 let auth_res = auth_client 1359 .post(format!("{}/oauth/authorize", url)) 1360 .form(&[ 1361 ("request_uri", request_uri), 1362 ("username", &handle), 1363 ("password", password), 1364 ("remember_device", "false"), 1365 ]) 1366 .send() 1367 .await 1368 .unwrap(); 1369 1370 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 1371 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 1372 1373 let token_body: Value = http_client 1374 .post(format!("{}/oauth/token", url)) 1375 .form(&[ 1376 ("grant_type", "authorization_code"), 1377 ("code", code), 1378 ("redirect_uri", redirect_uri), 1379 ("code_verifier", &code_verifier), 1380 ("client_id", &client_id), 1381 ]) 1382 .send() 1383 .await 1384 .unwrap() 1385 .json() 1386 .await 1387 .unwrap(); 1388 1389 let access_token = token_body["access_token"].as_str().unwrap(); 1390 let refresh_token = token_body["refresh_token"].as_str().unwrap(); 1391 1392 http_client 1393 .post(format!("{}/oauth/revoke", url)) 1394 .form(&[("token", refresh_token)]) 1395 .send() 1396 .await 1397 .unwrap(); 1398 1399 let introspect_res = http_client 1400 .post(format!("{}/oauth/introspect", url)) 1401 .form(&[("token", access_token)]) 1402 .send() 1403 .await 1404 .unwrap(); 1405 1406 assert_eq!(introspect_res.status(), StatusCode::OK); 1407 let body: Value = introspect_res.json().await.unwrap(); 1408 assert_eq!(body["active"], false, "Revoked token should be inactive"); 1409} 1410 1411#[tokio::test] 1412async fn test_state_with_special_chars() { 1413 let url = base_url().await; 1414 let http_client = client(); 1415 1416 let ts = Utc::now().timestamp_millis(); 1417 let handle = format!("state-special-{}", ts); 1418 let email = format!("state-special-{}@example.com", ts); 1419 let password = "state-special-password"; 1420 1421 http_client 1422 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1423 .json(&json!({ 1424 "handle": handle, 1425 "email": email, 1426 "password": password 1427 })) 1428 .send() 1429 .await 1430 .unwrap(); 1431 1432 let redirect_uri = "https://example.com/state-special-callback"; 1433 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1434 let client_id = mock_client.uri(); 1435 1436 let (_code_verifier, code_challenge) = generate_pkce(); 1437 let special_state = "state=with&special=chars&plus+more"; 1438 1439 let par_body: Value = http_client 1440 .post(format!("{}/oauth/par", url)) 1441 .form(&[ 1442 ("response_type", "code"), 1443 ("client_id", &client_id), 1444 ("redirect_uri", redirect_uri), 1445 ("code_challenge", &code_challenge), 1446 ("code_challenge_method", "S256"), 1447 ("state", special_state), 1448 ]) 1449 .send() 1450 .await 1451 .unwrap() 1452 .json() 1453 .await 1454 .unwrap(); 1455 1456 let request_uri = par_body["request_uri"].as_str().unwrap(); 1457 1458 let auth_client = no_redirect_client(); 1459 let auth_res = auth_client 1460 .post(format!("{}/oauth/authorize", url)) 1461 .form(&[ 1462 ("request_uri", request_uri), 1463 ("username", &handle), 1464 ("password", password), 1465 ("remember_device", "false"), 1466 ]) 1467 .send() 1468 .await 1469 .unwrap(); 1470 1471 assert!( 1472 auth_res.status().is_redirection(), 1473 "Should redirect even with special chars in state" 1474 ); 1475 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 1476 assert!(location.contains("state="), "State should be in redirect URL"); 1477 1478 let encoded_state = urlencoding::encode(special_state); 1479 assert!( 1480 location.contains(&format!("state={}", encoded_state)), 1481 "State should be URL-encoded. Got: {}", 1482 location 1483 ); 1484} 1485 1486#[tokio::test] 1487async fn test_2fa_required_when_enabled() { 1488 let url = base_url().await; 1489 let http_client = client(); 1490 1491 let ts = Utc::now().timestamp_millis(); 1492 let handle = format!("2fa-required-{}", ts); 1493 let email = format!("2fa-required-{}@example.com", ts); 1494 let password = "2fa-test-password"; 1495 1496 let create_res = http_client 1497 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1498 .json(&json!({ 1499 "handle": handle, 1500 "email": email, 1501 "password": password 1502 })) 1503 .send() 1504 .await 1505 .unwrap(); 1506 assert_eq!(create_res.status(), StatusCode::OK); 1507 let account: Value = create_res.json().await.unwrap(); 1508 let user_did = account["did"].as_str().unwrap(); 1509 1510 let db_url = common::get_db_connection_string().await; 1511 let pool = sqlx::postgres::PgPoolOptions::new() 1512 .max_connections(1) 1513 .connect(&db_url) 1514 .await 1515 .expect("Failed to connect to database"); 1516 1517 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1518 .bind(user_did) 1519 .execute(&pool) 1520 .await 1521 .expect("Failed to enable 2FA"); 1522 1523 let redirect_uri = "https://example.com/2fa-callback"; 1524 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1525 let client_id = mock_client.uri(); 1526 1527 let (_, code_challenge) = generate_pkce(); 1528 1529 let par_body: Value = http_client 1530 .post(format!("{}/oauth/par", url)) 1531 .form(&[ 1532 ("response_type", "code"), 1533 ("client_id", &client_id), 1534 ("redirect_uri", redirect_uri), 1535 ("code_challenge", &code_challenge), 1536 ("code_challenge_method", "S256"), 1537 ]) 1538 .send() 1539 .await 1540 .unwrap() 1541 .json() 1542 .await 1543 .unwrap(); 1544 1545 let request_uri = par_body["request_uri"].as_str().unwrap(); 1546 1547 let auth_client = no_redirect_client(); 1548 let auth_res = auth_client 1549 .post(format!("{}/oauth/authorize", url)) 1550 .form(&[ 1551 ("request_uri", request_uri), 1552 ("username", &handle), 1553 ("password", password), 1554 ("remember_device", "false"), 1555 ]) 1556 .send() 1557 .await 1558 .unwrap(); 1559 1560 assert!( 1561 auth_res.status().is_redirection(), 1562 "Should redirect to 2FA page, got status: {}", 1563 auth_res.status() 1564 ); 1565 1566 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 1567 assert!( 1568 location.contains("/oauth/authorize/2fa"), 1569 "Should redirect to 2FA page, got: {}", 1570 location 1571 ); 1572 assert!( 1573 location.contains("request_uri="), 1574 "2FA redirect should include request_uri" 1575 ); 1576} 1577 1578#[tokio::test] 1579async fn test_2fa_invalid_code_rejected() { 1580 let url = base_url().await; 1581 let http_client = client(); 1582 1583 let ts = Utc::now().timestamp_millis(); 1584 let handle = format!("2fa-invalid-{}", ts); 1585 let email = format!("2fa-invalid-{}@example.com", ts); 1586 let password = "2fa-test-password"; 1587 1588 let create_res = http_client 1589 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1590 .json(&json!({ 1591 "handle": handle, 1592 "email": email, 1593 "password": password 1594 })) 1595 .send() 1596 .await 1597 .unwrap(); 1598 assert_eq!(create_res.status(), StatusCode::OK); 1599 let account: Value = create_res.json().await.unwrap(); 1600 let user_did = account["did"].as_str().unwrap(); 1601 1602 let db_url = common::get_db_connection_string().await; 1603 let pool = sqlx::postgres::PgPoolOptions::new() 1604 .max_connections(1) 1605 .connect(&db_url) 1606 .await 1607 .expect("Failed to connect to database"); 1608 1609 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1610 .bind(user_did) 1611 .execute(&pool) 1612 .await 1613 .expect("Failed to enable 2FA"); 1614 1615 let redirect_uri = "https://example.com/2fa-invalid-callback"; 1616 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1617 let client_id = mock_client.uri(); 1618 1619 let (_, code_challenge) = generate_pkce(); 1620 1621 let par_body: Value = http_client 1622 .post(format!("{}/oauth/par", url)) 1623 .form(&[ 1624 ("response_type", "code"), 1625 ("client_id", &client_id), 1626 ("redirect_uri", redirect_uri), 1627 ("code_challenge", &code_challenge), 1628 ("code_challenge_method", "S256"), 1629 ]) 1630 .send() 1631 .await 1632 .unwrap() 1633 .json() 1634 .await 1635 .unwrap(); 1636 1637 let request_uri = par_body["request_uri"].as_str().unwrap(); 1638 1639 let auth_client = no_redirect_client(); 1640 let auth_res = auth_client 1641 .post(format!("{}/oauth/authorize", url)) 1642 .form(&[ 1643 ("request_uri", request_uri), 1644 ("username", &handle), 1645 ("password", password), 1646 ("remember_device", "false"), 1647 ]) 1648 .send() 1649 .await 1650 .unwrap(); 1651 1652 assert!(auth_res.status().is_redirection()); 1653 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 1654 assert!(location.contains("/oauth/authorize/2fa")); 1655 1656 let twofa_res = http_client 1657 .post(format!("{}/oauth/authorize/2fa", url)) 1658 .form(&[ 1659 ("request_uri", request_uri), 1660 ("code", "000000"), 1661 ]) 1662 .send() 1663 .await 1664 .unwrap(); 1665 1666 assert_eq!(twofa_res.status(), StatusCode::OK); 1667 let body = twofa_res.text().await.unwrap(); 1668 assert!( 1669 body.contains("Invalid verification code") || body.contains("invalid"), 1670 "Should show error for invalid code" 1671 ); 1672} 1673 1674#[tokio::test] 1675async fn test_2fa_valid_code_completes_auth() { 1676 let url = base_url().await; 1677 let http_client = client(); 1678 1679 let ts = Utc::now().timestamp_millis(); 1680 let handle = format!("2fa-valid-{}", ts); 1681 let email = format!("2fa-valid-{}@example.com", ts); 1682 let password = "2fa-test-password"; 1683 1684 let create_res = http_client 1685 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1686 .json(&json!({ 1687 "handle": handle, 1688 "email": email, 1689 "password": password 1690 })) 1691 .send() 1692 .await 1693 .unwrap(); 1694 assert_eq!(create_res.status(), StatusCode::OK); 1695 let account: Value = create_res.json().await.unwrap(); 1696 let user_did = account["did"].as_str().unwrap(); 1697 1698 let db_url = common::get_db_connection_string().await; 1699 let pool = sqlx::postgres::PgPoolOptions::new() 1700 .max_connections(1) 1701 .connect(&db_url) 1702 .await 1703 .expect("Failed to connect to database"); 1704 1705 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1706 .bind(user_did) 1707 .execute(&pool) 1708 .await 1709 .expect("Failed to enable 2FA"); 1710 1711 let redirect_uri = "https://example.com/2fa-valid-callback"; 1712 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1713 let client_id = mock_client.uri(); 1714 1715 let (code_verifier, code_challenge) = generate_pkce(); 1716 1717 let par_body: Value = http_client 1718 .post(format!("{}/oauth/par", url)) 1719 .form(&[ 1720 ("response_type", "code"), 1721 ("client_id", &client_id), 1722 ("redirect_uri", redirect_uri), 1723 ("code_challenge", &code_challenge), 1724 ("code_challenge_method", "S256"), 1725 ]) 1726 .send() 1727 .await 1728 .unwrap() 1729 .json() 1730 .await 1731 .unwrap(); 1732 1733 let request_uri = par_body["request_uri"].as_str().unwrap(); 1734 1735 let auth_client = no_redirect_client(); 1736 let auth_res = auth_client 1737 .post(format!("{}/oauth/authorize", url)) 1738 .form(&[ 1739 ("request_uri", request_uri), 1740 ("username", &handle), 1741 ("password", password), 1742 ("remember_device", "false"), 1743 ]) 1744 .send() 1745 .await 1746 .unwrap(); 1747 1748 assert!(auth_res.status().is_redirection()); 1749 1750 let twofa_code: String = sqlx::query_scalar( 1751 "SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1" 1752 ) 1753 .bind(request_uri) 1754 .fetch_one(&pool) 1755 .await 1756 .expect("Failed to get 2FA code from database"); 1757 1758 let twofa_res = auth_client 1759 .post(format!("{}/oauth/authorize/2fa", url)) 1760 .form(&[ 1761 ("request_uri", request_uri), 1762 ("code", &twofa_code), 1763 ]) 1764 .send() 1765 .await 1766 .unwrap(); 1767 1768 assert!( 1769 twofa_res.status().is_redirection(), 1770 "Valid 2FA code should redirect to success, got status: {}", 1771 twofa_res.status() 1772 ); 1773 1774 let location = twofa_res.headers().get("location").unwrap().to_str().unwrap(); 1775 assert!( 1776 location.starts_with(redirect_uri), 1777 "Should redirect to client callback, got: {}", 1778 location 1779 ); 1780 assert!( 1781 location.contains("code="), 1782 "Redirect should include authorization code" 1783 ); 1784 1785 let auth_code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 1786 1787 let token_res = http_client 1788 .post(format!("{}/oauth/token", url)) 1789 .form(&[ 1790 ("grant_type", "authorization_code"), 1791 ("code", auth_code), 1792 ("redirect_uri", redirect_uri), 1793 ("code_verifier", &code_verifier), 1794 ("client_id", &client_id), 1795 ]) 1796 .send() 1797 .await 1798 .unwrap(); 1799 1800 assert_eq!(token_res.status(), StatusCode::OK, "Token exchange should succeed"); 1801 let token_body: Value = token_res.json().await.unwrap(); 1802 assert!(token_body["access_token"].is_string()); 1803 assert_eq!(token_body["sub"], user_did); 1804} 1805 1806#[tokio::test] 1807async fn test_2fa_lockout_after_max_attempts() { 1808 let url = base_url().await; 1809 let http_client = client(); 1810 1811 let ts = Utc::now().timestamp_millis(); 1812 let handle = format!("2fa-lockout-{}", ts); 1813 let email = format!("2fa-lockout-{}@example.com", ts); 1814 let password = "2fa-test-password"; 1815 1816 let create_res = http_client 1817 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1818 .json(&json!({ 1819 "handle": handle, 1820 "email": email, 1821 "password": password 1822 })) 1823 .send() 1824 .await 1825 .unwrap(); 1826 assert_eq!(create_res.status(), StatusCode::OK); 1827 let account: Value = create_res.json().await.unwrap(); 1828 let user_did = account["did"].as_str().unwrap(); 1829 1830 let db_url = common::get_db_connection_string().await; 1831 let pool = sqlx::postgres::PgPoolOptions::new() 1832 .max_connections(1) 1833 .connect(&db_url) 1834 .await 1835 .expect("Failed to connect to database"); 1836 1837 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1838 .bind(user_did) 1839 .execute(&pool) 1840 .await 1841 .expect("Failed to enable 2FA"); 1842 1843 let redirect_uri = "https://example.com/2fa-lockout-callback"; 1844 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1845 let client_id = mock_client.uri(); 1846 1847 let (_, code_challenge) = generate_pkce(); 1848 1849 let par_body: Value = http_client 1850 .post(format!("{}/oauth/par", url)) 1851 .form(&[ 1852 ("response_type", "code"), 1853 ("client_id", &client_id), 1854 ("redirect_uri", redirect_uri), 1855 ("code_challenge", &code_challenge), 1856 ("code_challenge_method", "S256"), 1857 ]) 1858 .send() 1859 .await 1860 .unwrap() 1861 .json() 1862 .await 1863 .unwrap(); 1864 1865 let request_uri = par_body["request_uri"].as_str().unwrap(); 1866 1867 let auth_client = no_redirect_client(); 1868 let auth_res = auth_client 1869 .post(format!("{}/oauth/authorize", url)) 1870 .form(&[ 1871 ("request_uri", request_uri), 1872 ("username", &handle), 1873 ("password", password), 1874 ("remember_device", "false"), 1875 ]) 1876 .send() 1877 .await 1878 .unwrap(); 1879 1880 assert!(auth_res.status().is_redirection()); 1881 1882 for i in 0..5 { 1883 let res = http_client 1884 .post(format!("{}/oauth/authorize/2fa", url)) 1885 .form(&[ 1886 ("request_uri", request_uri), 1887 ("code", "999999"), 1888 ]) 1889 .send() 1890 .await 1891 .unwrap(); 1892 1893 if i < 4 { 1894 assert_eq!(res.status(), StatusCode::OK, "Attempt {} should show error page", i + 1); 1895 let body = res.text().await.unwrap(); 1896 assert!( 1897 body.contains("Invalid verification code"), 1898 "Should show invalid code error on attempt {}", i + 1 1899 ); 1900 } 1901 } 1902 1903 let lockout_res = http_client 1904 .post(format!("{}/oauth/authorize/2fa", url)) 1905 .form(&[ 1906 ("request_uri", request_uri), 1907 ("code", "999999"), 1908 ]) 1909 .send() 1910 .await 1911 .unwrap(); 1912 1913 assert_eq!(lockout_res.status(), StatusCode::OK); 1914 let body = lockout_res.text().await.unwrap(); 1915 assert!( 1916 body.contains("Too many failed attempts") || body.contains("No 2FA challenge found"), 1917 "Should be locked out after max attempts. Body: {}", 1918 &body[..body.len().min(500)] 1919 ); 1920} 1921 1922#[tokio::test] 1923async fn test_account_selector_with_2fa_requires_verification() { 1924 let url = base_url().await; 1925 let http_client = client(); 1926 1927 let ts = Utc::now().timestamp_millis(); 1928 let handle = format!("selector-2fa-{}", ts); 1929 let email = format!("selector-2fa-{}@example.com", ts); 1930 let password = "selector-2fa-password"; 1931 1932 let create_res = http_client 1933 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1934 .json(&json!({ 1935 "handle": handle, 1936 "email": email, 1937 "password": password 1938 })) 1939 .send() 1940 .await 1941 .unwrap(); 1942 assert_eq!(create_res.status(), StatusCode::OK); 1943 let account: Value = create_res.json().await.unwrap(); 1944 let user_did = account["did"].as_str().unwrap().to_string(); 1945 1946 let redirect_uri = "https://example.com/selector-2fa-callback"; 1947 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1948 let client_id = mock_client.uri(); 1949 1950 let (code_verifier, code_challenge) = generate_pkce(); 1951 1952 let par_body: Value = http_client 1953 .post(format!("{}/oauth/par", url)) 1954 .form(&[ 1955 ("response_type", "code"), 1956 ("client_id", &client_id), 1957 ("redirect_uri", redirect_uri), 1958 ("code_challenge", &code_challenge), 1959 ("code_challenge_method", "S256"), 1960 ]) 1961 .send() 1962 .await 1963 .unwrap() 1964 .json() 1965 .await 1966 .unwrap(); 1967 1968 let request_uri = par_body["request_uri"].as_str().unwrap(); 1969 1970 let auth_client = no_redirect_client(); 1971 let auth_res = auth_client 1972 .post(format!("{}/oauth/authorize", url)) 1973 .form(&[ 1974 ("request_uri", request_uri), 1975 ("username", &handle), 1976 ("password", password), 1977 ("remember_device", "true"), 1978 ]) 1979 .send() 1980 .await 1981 .unwrap(); 1982 1983 assert!(auth_res.status().is_redirection()); 1984 1985 let device_cookie = auth_res.headers() 1986 .get("set-cookie") 1987 .and_then(|v| v.to_str().ok()) 1988 .map(|s| s.split(';').next().unwrap_or("").to_string()) 1989 .expect("Should have received device cookie"); 1990 1991 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 1992 assert!(location.contains("code="), "First auth should succeed"); 1993 1994 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 1995 let _token_body: Value = http_client 1996 .post(format!("{}/oauth/token", url)) 1997 .form(&[ 1998 ("grant_type", "authorization_code"), 1999 ("code", code), 2000 ("redirect_uri", redirect_uri), 2001 ("code_verifier", &code_verifier), 2002 ("client_id", &client_id), 2003 ]) 2004 .send() 2005 .await 2006 .unwrap() 2007 .json() 2008 .await 2009 .unwrap(); 2010 2011 let db_url = common::get_db_connection_string().await; 2012 let pool = sqlx::postgres::PgPoolOptions::new() 2013 .max_connections(1) 2014 .connect(&db_url) 2015 .await 2016 .expect("Failed to connect to database"); 2017 2018 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 2019 .bind(&user_did) 2020 .execute(&pool) 2021 .await 2022 .expect("Failed to enable 2FA"); 2023 2024 let (code_verifier2, code_challenge2) = generate_pkce(); 2025 2026 let par_body2: Value = http_client 2027 .post(format!("{}/oauth/par", url)) 2028 .form(&[ 2029 ("response_type", "code"), 2030 ("client_id", &client_id), 2031 ("redirect_uri", redirect_uri), 2032 ("code_challenge", &code_challenge2), 2033 ("code_challenge_method", "S256"), 2034 ]) 2035 .send() 2036 .await 2037 .unwrap() 2038 .json() 2039 .await 2040 .unwrap(); 2041 2042 let request_uri2 = par_body2["request_uri"].as_str().unwrap(); 2043 2044 let select_res = auth_client 2045 .post(format!("{}/oauth/authorize/select", url)) 2046 .header("cookie", &device_cookie) 2047 .form(&[ 2048 ("request_uri", request_uri2), 2049 ("did", &user_did), 2050 ]) 2051 .send() 2052 .await 2053 .unwrap(); 2054 2055 assert!( 2056 select_res.status().is_redirection(), 2057 "Account selector should redirect, got status: {}", 2058 select_res.status() 2059 ); 2060 2061 let select_location = select_res.headers().get("location").unwrap().to_str().unwrap(); 2062 assert!( 2063 select_location.contains("/oauth/authorize/2fa"), 2064 "Account selector with 2FA enabled should redirect to 2FA page, got: {}", 2065 select_location 2066 ); 2067 2068 let twofa_code: String = sqlx::query_scalar( 2069 "SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1" 2070 ) 2071 .bind(request_uri2) 2072 .fetch_one(&pool) 2073 .await 2074 .expect("Failed to get 2FA code"); 2075 2076 let twofa_res = auth_client 2077 .post(format!("{}/oauth/authorize/2fa", url)) 2078 .header("cookie", &device_cookie) 2079 .form(&[ 2080 ("request_uri", request_uri2), 2081 ("code", &twofa_code), 2082 ]) 2083 .send() 2084 .await 2085 .unwrap(); 2086 2087 assert!(twofa_res.status().is_redirection()); 2088 let final_location = twofa_res.headers().get("location").unwrap().to_str().unwrap(); 2089 assert!( 2090 final_location.starts_with(redirect_uri) && final_location.contains("code="), 2091 "After 2FA, should redirect to client with code, got: {}", 2092 final_location 2093 ); 2094 2095 let final_code = final_location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 2096 let token_res = http_client 2097 .post(format!("{}/oauth/token", url)) 2098 .form(&[ 2099 ("grant_type", "authorization_code"), 2100 ("code", final_code), 2101 ("redirect_uri", redirect_uri), 2102 ("code_verifier", &code_verifier2), 2103 ("client_id", &client_id), 2104 ]) 2105 .send() 2106 .await 2107 .unwrap(); 2108 2109 assert_eq!(token_res.status(), StatusCode::OK); 2110 let final_token: Value = token_res.json().await.unwrap(); 2111 assert_eq!(final_token["sub"], user_did, "Token should be for the correct user"); 2112}