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_authorize_get_with_valid_request_uri() { 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 (_, code_challenge) = generate_pkce(); 218 219 let par_res = client 220 .post(format!("{}/oauth/par", url)) 221 .form(&[ 222 ("response_type", "code"), 223 ("client_id", &client_id), 224 ("redirect_uri", redirect_uri), 225 ("code_challenge", &code_challenge), 226 ("code_challenge_method", "S256"), 227 ("scope", "atproto"), 228 ("state", "test-state"), 229 ]) 230 .send() 231 .await 232 .expect("PAR failed"); 233 234 let par_body: Value = par_res.json().await.expect("Invalid PAR JSON"); 235 let request_uri = par_body["request_uri"].as_str().unwrap(); 236 237 let auth_res = client 238 .get(format!("{}/oauth/authorize", url)) 239 .header("Accept", "application/json") 240 .query(&[("request_uri", request_uri)]) 241 .send() 242 .await 243 .expect("Authorize GET failed"); 244 245 assert_eq!(auth_res.status(), StatusCode::OK); 246 247 let auth_body: Value = auth_res.json().await.expect("Invalid auth JSON"); 248 assert_eq!(auth_body["client_id"], client_id); 249 assert_eq!(auth_body["redirect_uri"], redirect_uri); 250 assert_eq!(auth_body["scope"], "atproto"); 251 assert_eq!(auth_body["state"], "test-state"); 252} 253 254#[tokio::test] 255async fn test_authorize_rejects_invalid_request_uri() { 256 let url = base_url().await; 257 let client = client(); 258 259 let res = client 260 .get(format!("{}/oauth/authorize", url)) 261 .header("Accept", "application/json") 262 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:nonexistent")]) 263 .send() 264 .await 265 .expect("Request failed"); 266 267 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 268 269 let body: Value = res.json().await.expect("Invalid JSON"); 270 assert_eq!(body["error"], "invalid_request"); 271} 272 273#[tokio::test] 274async fn test_authorize_requires_request_uri() { 275 let url = base_url().await; 276 let client = client(); 277 278 let res = client 279 .get(format!("{}/oauth/authorize", url)) 280 .send() 281 .await 282 .expect("Request failed"); 283 284 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 285} 286 287#[tokio::test] 288async fn test_full_oauth_flow_without_dpop() { 289 let url = base_url().await; 290 let http_client = client(); 291 292 let (_, _user_did) = create_account_and_login(&http_client).await; 293 294 let ts = Utc::now().timestamp_millis(); 295 let handle = format!("oauth-test-{}", ts); 296 let email = format!("oauth-test-{}@example.com", ts); 297 let password = "oauth-test-password"; 298 299 let create_res = http_client 300 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 301 .json(&json!({ 302 "handle": handle, 303 "email": email, 304 "password": password 305 })) 306 .send() 307 .await 308 .expect("Account creation failed"); 309 310 assert_eq!(create_res.status(), StatusCode::OK); 311 let account: Value = create_res.json().await.unwrap(); 312 let user_did = account["did"].as_str().unwrap(); 313 314 let redirect_uri = "https://example.com/oauth/callback"; 315 let mock_client = setup_mock_client_metadata(redirect_uri).await; 316 let client_id = mock_client.uri(); 317 318 let (code_verifier, code_challenge) = generate_pkce(); 319 let state = format!("state-{}", ts); 320 321 let par_res = http_client 322 .post(format!("{}/oauth/par", url)) 323 .form(&[ 324 ("response_type", "code"), 325 ("client_id", &client_id), 326 ("redirect_uri", redirect_uri), 327 ("code_challenge", &code_challenge), 328 ("code_challenge_method", "S256"), 329 ("scope", "atproto"), 330 ("state", &state), 331 ]) 332 .send() 333 .await 334 .expect("PAR failed"); 335 336 let par_status = par_res.status(); 337 let par_text = par_res.text().await.unwrap_or_default(); 338 if par_status != StatusCode::OK { 339 panic!("PAR failed with status {}: {}", par_status, par_text); 340 } 341 let par_body: Value = serde_json::from_str(&par_text).unwrap(); 342 let request_uri = par_body["request_uri"].as_str().unwrap(); 343 344 let auth_client = no_redirect_client(); 345 let auth_res = auth_client 346 .post(format!("{}/oauth/authorize", url)) 347 .form(&[ 348 ("request_uri", request_uri), 349 ("username", &handle), 350 ("password", password), 351 ("remember_device", "false"), 352 ]) 353 .send() 354 .await 355 .expect("Authorize POST failed"); 356 357 let auth_status = auth_res.status(); 358 if auth_status != StatusCode::TEMPORARY_REDIRECT 359 && auth_status != StatusCode::SEE_OTHER 360 && auth_status != StatusCode::FOUND 361 { 362 let auth_text = auth_res.text().await.unwrap_or_default(); 363 panic!( 364 "Expected redirect, got {}: {}", 365 auth_status, auth_text 366 ); 367 } 368 369 let location = auth_res.headers().get("location") 370 .expect("No Location header") 371 .to_str() 372 .unwrap(); 373 374 assert!(location.starts_with(redirect_uri), "Redirect to wrong URI: {}", location); 375 assert!(location.contains("code="), "No code in redirect: {}", location); 376 assert!(location.contains(&format!("state={}", state)), "Wrong state in redirect"); 377 378 let code = location 379 .split("code=") 380 .nth(1) 381 .unwrap() 382 .split('&') 383 .next() 384 .unwrap(); 385 386 let token_res = http_client 387 .post(format!("{}/oauth/token", url)) 388 .form(&[ 389 ("grant_type", "authorization_code"), 390 ("code", code), 391 ("redirect_uri", redirect_uri), 392 ("code_verifier", &code_verifier), 393 ("client_id", &client_id), 394 ]) 395 .send() 396 .await 397 .expect("Token request failed"); 398 399 let token_status = token_res.status(); 400 let token_text = token_res.text().await.unwrap_or_default(); 401 if token_status != StatusCode::OK { 402 panic!("Token request failed with status {}: {}", token_status, token_text); 403 } 404 405 let token_body: Value = serde_json::from_str(&token_text).unwrap(); 406 407 assert!(token_body["access_token"].is_string()); 408 assert!(token_body["refresh_token"].is_string()); 409 assert_eq!(token_body["token_type"], "Bearer"); 410 assert!(token_body["expires_in"].is_number()); 411 assert_eq!(token_body["sub"], user_did); 412} 413 414#[tokio::test] 415async fn test_token_refresh_flow() { 416 let url = base_url().await; 417 let http_client = client(); 418 419 let ts = Utc::now().timestamp_millis(); 420 let handle = format!("refresh-test-{}", ts); 421 let email = format!("refresh-test-{}@example.com", ts); 422 let password = "refresh-test-password"; 423 424 http_client 425 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 426 .json(&json!({ 427 "handle": handle, 428 "email": email, 429 "password": password 430 })) 431 .send() 432 .await 433 .expect("Account creation failed"); 434 435 let redirect_uri = "https://example.com/refresh-callback"; 436 let mock_client = setup_mock_client_metadata(redirect_uri).await; 437 let client_id = mock_client.uri(); 438 439 let (code_verifier, code_challenge) = generate_pkce(); 440 441 let par_body: Value = http_client 442 .post(format!("{}/oauth/par", url)) 443 .form(&[ 444 ("response_type", "code"), 445 ("client_id", &client_id), 446 ("redirect_uri", redirect_uri), 447 ("code_challenge", &code_challenge), 448 ("code_challenge_method", "S256"), 449 ]) 450 .send() 451 .await 452 .unwrap() 453 .json() 454 .await 455 .unwrap(); 456 457 let request_uri = par_body["request_uri"].as_str().unwrap(); 458 459 let auth_client = no_redirect_client(); 460 let auth_res = auth_client 461 .post(format!("{}/oauth/authorize", url)) 462 .form(&[ 463 ("request_uri", request_uri), 464 ("username", &handle), 465 ("password", password), 466 ("remember_device", "false"), 467 ]) 468 .send() 469 .await 470 .unwrap(); 471 472 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 473 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 474 475 let token_body: Value = http_client 476 .post(format!("{}/oauth/token", url)) 477 .form(&[ 478 ("grant_type", "authorization_code"), 479 ("code", code), 480 ("redirect_uri", redirect_uri), 481 ("code_verifier", &code_verifier), 482 ("client_id", &client_id), 483 ]) 484 .send() 485 .await 486 .unwrap() 487 .json() 488 .await 489 .unwrap(); 490 491 let refresh_token = token_body["refresh_token"].as_str().unwrap(); 492 let original_access_token = token_body["access_token"].as_str().unwrap(); 493 494 let refresh_res = http_client 495 .post(format!("{}/oauth/token", url)) 496 .form(&[ 497 ("grant_type", "refresh_token"), 498 ("refresh_token", refresh_token), 499 ("client_id", &client_id), 500 ]) 501 .send() 502 .await 503 .expect("Refresh request failed"); 504 505 assert_eq!(refresh_res.status(), StatusCode::OK); 506 507 let refresh_body: Value = refresh_res.json().await.unwrap(); 508 509 assert!(refresh_body["access_token"].is_string()); 510 assert!(refresh_body["refresh_token"].is_string()); 511 512 let new_access_token = refresh_body["access_token"].as_str().unwrap(); 513 let new_refresh_token = refresh_body["refresh_token"].as_str().unwrap(); 514 515 assert_ne!(new_access_token, original_access_token, "Access token should rotate"); 516 assert_ne!(new_refresh_token, refresh_token, "Refresh token should rotate"); 517} 518 519#[tokio::test] 520async fn test_wrong_credentials_denied() { 521 let url = base_url().await; 522 let http_client = client(); 523 524 let ts = Utc::now().timestamp_millis(); 525 let handle = format!("wrong-creds-{}", ts); 526 let email = format!("wrong-creds-{}@example.com", ts); 527 let password = "correct-password"; 528 529 http_client 530 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 531 .json(&json!({ 532 "handle": handle, 533 "email": email, 534 "password": password 535 })) 536 .send() 537 .await 538 .unwrap(); 539 540 let redirect_uri = "https://example.com/wrong-creds-callback"; 541 let mock_client = setup_mock_client_metadata(redirect_uri).await; 542 let client_id = mock_client.uri(); 543 544 let (_, code_challenge) = generate_pkce(); 545 546 let par_body: Value = http_client 547 .post(format!("{}/oauth/par", url)) 548 .form(&[ 549 ("response_type", "code"), 550 ("client_id", &client_id), 551 ("redirect_uri", redirect_uri), 552 ("code_challenge", &code_challenge), 553 ("code_challenge_method", "S256"), 554 ]) 555 .send() 556 .await 557 .unwrap() 558 .json() 559 .await 560 .unwrap(); 561 562 let request_uri = par_body["request_uri"].as_str().unwrap(); 563 564 let auth_res = http_client 565 .post(format!("{}/oauth/authorize", url)) 566 .header("Accept", "application/json") 567 .form(&[ 568 ("request_uri", request_uri), 569 ("username", &handle), 570 ("password", "wrong-password"), 571 ("remember_device", "false"), 572 ]) 573 .send() 574 .await 575 .unwrap(); 576 577 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN); 578 579 let error_body: Value = auth_res.json().await.unwrap(); 580 assert_eq!(error_body["error"], "access_denied"); 581} 582 583#[tokio::test] 584async fn test_token_revocation() { 585 let url = base_url().await; 586 let http_client = client(); 587 588 let ts = Utc::now().timestamp_millis(); 589 let handle = format!("revoke-test-{}", ts); 590 let email = format!("revoke-test-{}@example.com", ts); 591 let password = "revoke-test-password"; 592 593 http_client 594 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 595 .json(&json!({ 596 "handle": handle, 597 "email": email, 598 "password": password 599 })) 600 .send() 601 .await 602 .unwrap(); 603 604 let redirect_uri = "https://example.com/revoke-callback"; 605 let mock_client = setup_mock_client_metadata(redirect_uri).await; 606 let client_id = mock_client.uri(); 607 608 let (code_verifier, code_challenge) = generate_pkce(); 609 610 let par_body: Value = http_client 611 .post(format!("{}/oauth/par", url)) 612 .form(&[ 613 ("response_type", "code"), 614 ("client_id", &client_id), 615 ("redirect_uri", redirect_uri), 616 ("code_challenge", &code_challenge), 617 ("code_challenge_method", "S256"), 618 ]) 619 .send() 620 .await 621 .unwrap() 622 .json() 623 .await 624 .unwrap(); 625 626 let request_uri = par_body["request_uri"].as_str().unwrap(); 627 628 let auth_client = no_redirect_client(); 629 let auth_res = auth_client 630 .post(format!("{}/oauth/authorize", url)) 631 .form(&[ 632 ("request_uri", request_uri), 633 ("username", &handle), 634 ("password", password), 635 ("remember_device", "false"), 636 ]) 637 .send() 638 .await 639 .unwrap(); 640 641 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 642 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 643 644 let token_body: Value = http_client 645 .post(format!("{}/oauth/token", url)) 646 .form(&[ 647 ("grant_type", "authorization_code"), 648 ("code", code), 649 ("redirect_uri", redirect_uri), 650 ("code_verifier", &code_verifier), 651 ("client_id", &client_id), 652 ]) 653 .send() 654 .await 655 .unwrap() 656 .json() 657 .await 658 .unwrap(); 659 660 let refresh_token = token_body["refresh_token"].as_str().unwrap(); 661 662 let revoke_res = http_client 663 .post(format!("{}/oauth/revoke", url)) 664 .form(&[("token", refresh_token)]) 665 .send() 666 .await 667 .unwrap(); 668 669 assert_eq!(revoke_res.status(), StatusCode::OK); 670 671 let refresh_after_revoke = http_client 672 .post(format!("{}/oauth/token", url)) 673 .form(&[ 674 ("grant_type", "refresh_token"), 675 ("refresh_token", refresh_token), 676 ("client_id", &client_id), 677 ]) 678 .send() 679 .await 680 .unwrap(); 681 682 assert_eq!(refresh_after_revoke.status(), StatusCode::BAD_REQUEST); 683} 684 685#[tokio::test] 686async fn test_unsupported_grant_type() { 687 let url = base_url().await; 688 let http_client = client(); 689 690 let res = http_client 691 .post(format!("{}/oauth/token", url)) 692 .form(&[ 693 ("grant_type", "client_credentials"), 694 ("client_id", "https://example.com"), 695 ]) 696 .send() 697 .await 698 .unwrap(); 699 700 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 701 702 let body: Value = res.json().await.unwrap(); 703 assert_eq!(body["error"], "unsupported_grant_type"); 704} 705 706#[tokio::test] 707async fn test_invalid_refresh_token() { 708 let url = base_url().await; 709 let http_client = client(); 710 711 let res = http_client 712 .post(format!("{}/oauth/token", url)) 713 .form(&[ 714 ("grant_type", "refresh_token"), 715 ("refresh_token", "invalid-refresh-token"), 716 ("client_id", "https://example.com"), 717 ]) 718 .send() 719 .await 720 .unwrap(); 721 722 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 723 724 let body: Value = res.json().await.unwrap(); 725 assert_eq!(body["error"], "invalid_grant"); 726} 727 728#[tokio::test] 729async fn test_expired_authorization_request() { 730 let url = base_url().await; 731 let http_client = client(); 732 733 let res = http_client 734 .get(format!("{}/oauth/authorize", url)) 735 .header("Accept", "application/json") 736 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:expired-or-nonexistent")]) 737 .send() 738 .await 739 .unwrap(); 740 741 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 742 let body: Value = res.json().await.unwrap(); 743 assert_eq!(body["error"], "invalid_request"); 744} 745 746#[tokio::test] 747async fn test_token_introspection() { 748 let url = base_url().await; 749 let http_client = client(); 750 751 let ts = Utc::now().timestamp_millis(); 752 let handle = format!("introspect-{}", ts); 753 let email = format!("introspect-{}@example.com", ts); 754 let password = "introspect-password"; 755 756 http_client 757 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 758 .json(&json!({ 759 "handle": handle, 760 "email": email, 761 "password": password 762 })) 763 .send() 764 .await 765 .unwrap(); 766 767 let redirect_uri = "https://example.com/introspect-callback"; 768 let mock_client = setup_mock_client_metadata(redirect_uri).await; 769 let client_id = mock_client.uri(); 770 771 let (code_verifier, code_challenge) = generate_pkce(); 772 773 let par_body: Value = http_client 774 .post(format!("{}/oauth/par", url)) 775 .form(&[ 776 ("response_type", "code"), 777 ("client_id", &client_id), 778 ("redirect_uri", redirect_uri), 779 ("code_challenge", &code_challenge), 780 ("code_challenge_method", "S256"), 781 ]) 782 .send() 783 .await 784 .unwrap() 785 .json() 786 .await 787 .unwrap(); 788 789 let request_uri = par_body["request_uri"].as_str().unwrap(); 790 791 let auth_client = no_redirect_client(); 792 let auth_res = auth_client 793 .post(format!("{}/oauth/authorize", url)) 794 .form(&[ 795 ("request_uri", request_uri), 796 ("username", &handle), 797 ("password", password), 798 ("remember_device", "false"), 799 ]) 800 .send() 801 .await 802 .unwrap(); 803 804 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 805 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 806 807 let token_body: Value = http_client 808 .post(format!("{}/oauth/token", url)) 809 .form(&[ 810 ("grant_type", "authorization_code"), 811 ("code", code), 812 ("redirect_uri", redirect_uri), 813 ("code_verifier", &code_verifier), 814 ("client_id", &client_id), 815 ]) 816 .send() 817 .await 818 .unwrap() 819 .json() 820 .await 821 .unwrap(); 822 823 let access_token = token_body["access_token"].as_str().unwrap(); 824 825 let introspect_res = http_client 826 .post(format!("{}/oauth/introspect", url)) 827 .form(&[("token", access_token)]) 828 .send() 829 .await 830 .unwrap(); 831 832 assert_eq!(introspect_res.status(), StatusCode::OK); 833 let introspect_body: Value = introspect_res.json().await.unwrap(); 834 assert_eq!(introspect_body["active"], true); 835 assert!(introspect_body["client_id"].is_string()); 836 assert!(introspect_body["exp"].is_number()); 837} 838 839#[tokio::test] 840async fn test_introspect_invalid_token() { 841 let url = base_url().await; 842 let http_client = client(); 843 844 let res = http_client 845 .post(format!("{}/oauth/introspect", url)) 846 .form(&[("token", "invalid.token.here")]) 847 .send() 848 .await 849 .unwrap(); 850 851 assert_eq!(res.status(), StatusCode::OK); 852 let body: Value = res.json().await.unwrap(); 853 assert_eq!(body["active"], false); 854} 855 856#[tokio::test] 857async fn test_introspect_revoked_token() { 858 let url = base_url().await; 859 let http_client = client(); 860 861 let ts = Utc::now().timestamp_millis(); 862 let handle = format!("introspect-revoked-{}", ts); 863 let email = format!("introspect-revoked-{}@example.com", ts); 864 let password = "introspect-revoked-password"; 865 866 http_client 867 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 868 .json(&json!({ 869 "handle": handle, 870 "email": email, 871 "password": password 872 })) 873 .send() 874 .await 875 .unwrap(); 876 877 let redirect_uri = "https://example.com/introspect-revoked-callback"; 878 let mock_client = setup_mock_client_metadata(redirect_uri).await; 879 let client_id = mock_client.uri(); 880 881 let (code_verifier, code_challenge) = generate_pkce(); 882 883 let par_body: Value = http_client 884 .post(format!("{}/oauth/par", url)) 885 .form(&[ 886 ("response_type", "code"), 887 ("client_id", &client_id), 888 ("redirect_uri", redirect_uri), 889 ("code_challenge", &code_challenge), 890 ("code_challenge_method", "S256"), 891 ]) 892 .send() 893 .await 894 .unwrap() 895 .json() 896 .await 897 .unwrap(); 898 899 let request_uri = par_body["request_uri"].as_str().unwrap(); 900 901 let auth_client = no_redirect_client(); 902 let auth_res = auth_client 903 .post(format!("{}/oauth/authorize", url)) 904 .form(&[ 905 ("request_uri", request_uri), 906 ("username", &handle), 907 ("password", password), 908 ("remember_device", "false"), 909 ]) 910 .send() 911 .await 912 .unwrap(); 913 914 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 915 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 916 917 let token_body: Value = http_client 918 .post(format!("{}/oauth/token", url)) 919 .form(&[ 920 ("grant_type", "authorization_code"), 921 ("code", code), 922 ("redirect_uri", redirect_uri), 923 ("code_verifier", &code_verifier), 924 ("client_id", &client_id), 925 ]) 926 .send() 927 .await 928 .unwrap() 929 .json() 930 .await 931 .unwrap(); 932 933 let access_token = token_body["access_token"].as_str().unwrap(); 934 let refresh_token = token_body["refresh_token"].as_str().unwrap(); 935 936 http_client 937 .post(format!("{}/oauth/revoke", url)) 938 .form(&[("token", refresh_token)]) 939 .send() 940 .await 941 .unwrap(); 942 943 let introspect_res = http_client 944 .post(format!("{}/oauth/introspect", url)) 945 .form(&[("token", access_token)]) 946 .send() 947 .await 948 .unwrap(); 949 950 assert_eq!(introspect_res.status(), StatusCode::OK); 951 let body: Value = introspect_res.json().await.unwrap(); 952 assert_eq!(body["active"], false, "Revoked token should be inactive"); 953} 954 955#[tokio::test] 956async fn test_state_with_special_chars() { 957 let url = base_url().await; 958 let http_client = client(); 959 960 let ts = Utc::now().timestamp_millis(); 961 let handle = format!("state-special-{}", ts); 962 let email = format!("state-special-{}@example.com", ts); 963 let password = "state-special-password"; 964 965 http_client 966 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 967 .json(&json!({ 968 "handle": handle, 969 "email": email, 970 "password": password 971 })) 972 .send() 973 .await 974 .unwrap(); 975 976 let redirect_uri = "https://example.com/state-special-callback"; 977 let mock_client = setup_mock_client_metadata(redirect_uri).await; 978 let client_id = mock_client.uri(); 979 980 let (_code_verifier, code_challenge) = generate_pkce(); 981 let special_state = "state=with&special=chars&plus+more"; 982 983 let par_body: Value = http_client 984 .post(format!("{}/oauth/par", url)) 985 .form(&[ 986 ("response_type", "code"), 987 ("client_id", &client_id), 988 ("redirect_uri", redirect_uri), 989 ("code_challenge", &code_challenge), 990 ("code_challenge_method", "S256"), 991 ("state", special_state), 992 ]) 993 .send() 994 .await 995 .unwrap() 996 .json() 997 .await 998 .unwrap(); 999 1000 let request_uri = par_body["request_uri"].as_str().unwrap(); 1001 1002 let auth_client = no_redirect_client(); 1003 let auth_res = auth_client 1004 .post(format!("{}/oauth/authorize", url)) 1005 .form(&[ 1006 ("request_uri", request_uri), 1007 ("username", &handle), 1008 ("password", password), 1009 ("remember_device", "false"), 1010 ]) 1011 .send() 1012 .await 1013 .unwrap(); 1014 1015 assert!( 1016 auth_res.status().is_redirection(), 1017 "Should redirect even with special chars in state" 1018 ); 1019 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 1020 assert!(location.contains("state="), "State should be in redirect URL"); 1021 1022 let encoded_state = urlencoding::encode(special_state); 1023 assert!( 1024 location.contains(&format!("state={}", encoded_state)), 1025 "State should be URL-encoded. Got: {}", 1026 location 1027 ); 1028} 1029 1030#[tokio::test] 1031async fn test_2fa_required_when_enabled() { 1032 let url = base_url().await; 1033 let http_client = client(); 1034 1035 let ts = Utc::now().timestamp_millis(); 1036 let handle = format!("2fa-required-{}", ts); 1037 let email = format!("2fa-required-{}@example.com", ts); 1038 let password = "2fa-test-password"; 1039 1040 let create_res = http_client 1041 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1042 .json(&json!({ 1043 "handle": handle, 1044 "email": email, 1045 "password": password 1046 })) 1047 .send() 1048 .await 1049 .unwrap(); 1050 assert_eq!(create_res.status(), StatusCode::OK); 1051 let account: Value = create_res.json().await.unwrap(); 1052 let user_did = account["did"].as_str().unwrap(); 1053 1054 let db_url = common::get_db_connection_string().await; 1055 let pool = sqlx::postgres::PgPoolOptions::new() 1056 .max_connections(1) 1057 .connect(&db_url) 1058 .await 1059 .expect("Failed to connect to database"); 1060 1061 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1062 .bind(user_did) 1063 .execute(&pool) 1064 .await 1065 .expect("Failed to enable 2FA"); 1066 1067 let redirect_uri = "https://example.com/2fa-callback"; 1068 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1069 let client_id = mock_client.uri(); 1070 1071 let (_, code_challenge) = generate_pkce(); 1072 1073 let par_body: Value = http_client 1074 .post(format!("{}/oauth/par", url)) 1075 .form(&[ 1076 ("response_type", "code"), 1077 ("client_id", &client_id), 1078 ("redirect_uri", redirect_uri), 1079 ("code_challenge", &code_challenge), 1080 ("code_challenge_method", "S256"), 1081 ]) 1082 .send() 1083 .await 1084 .unwrap() 1085 .json() 1086 .await 1087 .unwrap(); 1088 1089 let request_uri = par_body["request_uri"].as_str().unwrap(); 1090 1091 let auth_client = no_redirect_client(); 1092 let auth_res = auth_client 1093 .post(format!("{}/oauth/authorize", url)) 1094 .form(&[ 1095 ("request_uri", request_uri), 1096 ("username", &handle), 1097 ("password", password), 1098 ("remember_device", "false"), 1099 ]) 1100 .send() 1101 .await 1102 .unwrap(); 1103 1104 assert!( 1105 auth_res.status().is_redirection(), 1106 "Should redirect to 2FA page, got status: {}", 1107 auth_res.status() 1108 ); 1109 1110 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 1111 assert!( 1112 location.contains("/oauth/authorize/2fa"), 1113 "Should redirect to 2FA page, got: {}", 1114 location 1115 ); 1116 assert!( 1117 location.contains("request_uri="), 1118 "2FA redirect should include request_uri" 1119 ); 1120} 1121 1122#[tokio::test] 1123async fn test_2fa_invalid_code_rejected() { 1124 let url = base_url().await; 1125 let http_client = client(); 1126 1127 let ts = Utc::now().timestamp_millis(); 1128 let handle = format!("2fa-invalid-{}", ts); 1129 let email = format!("2fa-invalid-{}@example.com", ts); 1130 let password = "2fa-test-password"; 1131 1132 let create_res = http_client 1133 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1134 .json(&json!({ 1135 "handle": handle, 1136 "email": email, 1137 "password": password 1138 })) 1139 .send() 1140 .await 1141 .unwrap(); 1142 assert_eq!(create_res.status(), StatusCode::OK); 1143 let account: Value = create_res.json().await.unwrap(); 1144 let user_did = account["did"].as_str().unwrap(); 1145 1146 let db_url = common::get_db_connection_string().await; 1147 let pool = sqlx::postgres::PgPoolOptions::new() 1148 .max_connections(1) 1149 .connect(&db_url) 1150 .await 1151 .expect("Failed to connect to database"); 1152 1153 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1154 .bind(user_did) 1155 .execute(&pool) 1156 .await 1157 .expect("Failed to enable 2FA"); 1158 1159 let redirect_uri = "https://example.com/2fa-invalid-callback"; 1160 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1161 let client_id = mock_client.uri(); 1162 1163 let (_, code_challenge) = generate_pkce(); 1164 1165 let par_body: Value = http_client 1166 .post(format!("{}/oauth/par", url)) 1167 .form(&[ 1168 ("response_type", "code"), 1169 ("client_id", &client_id), 1170 ("redirect_uri", redirect_uri), 1171 ("code_challenge", &code_challenge), 1172 ("code_challenge_method", "S256"), 1173 ]) 1174 .send() 1175 .await 1176 .unwrap() 1177 .json() 1178 .await 1179 .unwrap(); 1180 1181 let request_uri = par_body["request_uri"].as_str().unwrap(); 1182 1183 let auth_client = no_redirect_client(); 1184 let auth_res = auth_client 1185 .post(format!("{}/oauth/authorize", url)) 1186 .form(&[ 1187 ("request_uri", request_uri), 1188 ("username", &handle), 1189 ("password", password), 1190 ("remember_device", "false"), 1191 ]) 1192 .send() 1193 .await 1194 .unwrap(); 1195 1196 assert!(auth_res.status().is_redirection()); 1197 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 1198 assert!(location.contains("/oauth/authorize/2fa")); 1199 1200 let twofa_res = http_client 1201 .post(format!("{}/oauth/authorize/2fa", url)) 1202 .form(&[ 1203 ("request_uri", request_uri), 1204 ("code", "000000"), 1205 ]) 1206 .send() 1207 .await 1208 .unwrap(); 1209 1210 assert_eq!(twofa_res.status(), StatusCode::OK); 1211 let body = twofa_res.text().await.unwrap(); 1212 assert!( 1213 body.contains("Invalid verification code") || body.contains("invalid"), 1214 "Should show error for invalid code" 1215 ); 1216} 1217 1218#[tokio::test] 1219async fn test_2fa_valid_code_completes_auth() { 1220 let url = base_url().await; 1221 let http_client = client(); 1222 1223 let ts = Utc::now().timestamp_millis(); 1224 let handle = format!("2fa-valid-{}", ts); 1225 let email = format!("2fa-valid-{}@example.com", ts); 1226 let password = "2fa-test-password"; 1227 1228 let create_res = http_client 1229 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1230 .json(&json!({ 1231 "handle": handle, 1232 "email": email, 1233 "password": password 1234 })) 1235 .send() 1236 .await 1237 .unwrap(); 1238 assert_eq!(create_res.status(), StatusCode::OK); 1239 let account: Value = create_res.json().await.unwrap(); 1240 let user_did = account["did"].as_str().unwrap(); 1241 1242 let db_url = common::get_db_connection_string().await; 1243 let pool = sqlx::postgres::PgPoolOptions::new() 1244 .max_connections(1) 1245 .connect(&db_url) 1246 .await 1247 .expect("Failed to connect to database"); 1248 1249 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1250 .bind(user_did) 1251 .execute(&pool) 1252 .await 1253 .expect("Failed to enable 2FA"); 1254 1255 let redirect_uri = "https://example.com/2fa-valid-callback"; 1256 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1257 let client_id = mock_client.uri(); 1258 1259 let (code_verifier, code_challenge) = generate_pkce(); 1260 1261 let par_body: Value = http_client 1262 .post(format!("{}/oauth/par", url)) 1263 .form(&[ 1264 ("response_type", "code"), 1265 ("client_id", &client_id), 1266 ("redirect_uri", redirect_uri), 1267 ("code_challenge", &code_challenge), 1268 ("code_challenge_method", "S256"), 1269 ]) 1270 .send() 1271 .await 1272 .unwrap() 1273 .json() 1274 .await 1275 .unwrap(); 1276 1277 let request_uri = par_body["request_uri"].as_str().unwrap(); 1278 1279 let auth_client = no_redirect_client(); 1280 let auth_res = auth_client 1281 .post(format!("{}/oauth/authorize", url)) 1282 .form(&[ 1283 ("request_uri", request_uri), 1284 ("username", &handle), 1285 ("password", password), 1286 ("remember_device", "false"), 1287 ]) 1288 .send() 1289 .await 1290 .unwrap(); 1291 1292 assert!(auth_res.status().is_redirection()); 1293 1294 let twofa_code: String = sqlx::query_scalar( 1295 "SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1" 1296 ) 1297 .bind(request_uri) 1298 .fetch_one(&pool) 1299 .await 1300 .expect("Failed to get 2FA code from database"); 1301 1302 let twofa_res = auth_client 1303 .post(format!("{}/oauth/authorize/2fa", url)) 1304 .form(&[ 1305 ("request_uri", request_uri), 1306 ("code", &twofa_code), 1307 ]) 1308 .send() 1309 .await 1310 .unwrap(); 1311 1312 assert!( 1313 twofa_res.status().is_redirection(), 1314 "Valid 2FA code should redirect to success, got status: {}", 1315 twofa_res.status() 1316 ); 1317 1318 let location = twofa_res.headers().get("location").unwrap().to_str().unwrap(); 1319 assert!( 1320 location.starts_with(redirect_uri), 1321 "Should redirect to client callback, got: {}", 1322 location 1323 ); 1324 assert!( 1325 location.contains("code="), 1326 "Redirect should include authorization code" 1327 ); 1328 1329 let auth_code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 1330 1331 let token_res = http_client 1332 .post(format!("{}/oauth/token", url)) 1333 .form(&[ 1334 ("grant_type", "authorization_code"), 1335 ("code", auth_code), 1336 ("redirect_uri", redirect_uri), 1337 ("code_verifier", &code_verifier), 1338 ("client_id", &client_id), 1339 ]) 1340 .send() 1341 .await 1342 .unwrap(); 1343 1344 assert_eq!(token_res.status(), StatusCode::OK, "Token exchange should succeed"); 1345 let token_body: Value = token_res.json().await.unwrap(); 1346 assert!(token_body["access_token"].is_string()); 1347 assert_eq!(token_body["sub"], user_did); 1348} 1349 1350#[tokio::test] 1351async fn test_2fa_lockout_after_max_attempts() { 1352 let url = base_url().await; 1353 let http_client = client(); 1354 1355 let ts = Utc::now().timestamp_millis(); 1356 let handle = format!("2fa-lockout-{}", ts); 1357 let email = format!("2fa-lockout-{}@example.com", ts); 1358 let password = "2fa-test-password"; 1359 1360 let create_res = http_client 1361 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1362 .json(&json!({ 1363 "handle": handle, 1364 "email": email, 1365 "password": password 1366 })) 1367 .send() 1368 .await 1369 .unwrap(); 1370 assert_eq!(create_res.status(), StatusCode::OK); 1371 let account: Value = create_res.json().await.unwrap(); 1372 let user_did = account["did"].as_str().unwrap(); 1373 1374 let db_url = common::get_db_connection_string().await; 1375 let pool = sqlx::postgres::PgPoolOptions::new() 1376 .max_connections(1) 1377 .connect(&db_url) 1378 .await 1379 .expect("Failed to connect to database"); 1380 1381 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1382 .bind(user_did) 1383 .execute(&pool) 1384 .await 1385 .expect("Failed to enable 2FA"); 1386 1387 let redirect_uri = "https://example.com/2fa-lockout-callback"; 1388 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1389 let client_id = mock_client.uri(); 1390 1391 let (_, code_challenge) = generate_pkce(); 1392 1393 let par_body: Value = http_client 1394 .post(format!("{}/oauth/par", url)) 1395 .form(&[ 1396 ("response_type", "code"), 1397 ("client_id", &client_id), 1398 ("redirect_uri", redirect_uri), 1399 ("code_challenge", &code_challenge), 1400 ("code_challenge_method", "S256"), 1401 ]) 1402 .send() 1403 .await 1404 .unwrap() 1405 .json() 1406 .await 1407 .unwrap(); 1408 1409 let request_uri = par_body["request_uri"].as_str().unwrap(); 1410 1411 let auth_client = no_redirect_client(); 1412 let auth_res = auth_client 1413 .post(format!("{}/oauth/authorize", url)) 1414 .form(&[ 1415 ("request_uri", request_uri), 1416 ("username", &handle), 1417 ("password", password), 1418 ("remember_device", "false"), 1419 ]) 1420 .send() 1421 .await 1422 .unwrap(); 1423 1424 assert!(auth_res.status().is_redirection()); 1425 1426 for i in 0..5 { 1427 let res = http_client 1428 .post(format!("{}/oauth/authorize/2fa", url)) 1429 .form(&[ 1430 ("request_uri", request_uri), 1431 ("code", "999999"), 1432 ]) 1433 .send() 1434 .await 1435 .unwrap(); 1436 1437 if i < 4 { 1438 assert_eq!(res.status(), StatusCode::OK, "Attempt {} should show error page", i + 1); 1439 let body = res.text().await.unwrap(); 1440 assert!( 1441 body.contains("Invalid verification code"), 1442 "Should show invalid code error on attempt {}", i + 1 1443 ); 1444 } 1445 } 1446 1447 let lockout_res = http_client 1448 .post(format!("{}/oauth/authorize/2fa", url)) 1449 .form(&[ 1450 ("request_uri", request_uri), 1451 ("code", "999999"), 1452 ]) 1453 .send() 1454 .await 1455 .unwrap(); 1456 1457 assert_eq!(lockout_res.status(), StatusCode::OK); 1458 let body = lockout_res.text().await.unwrap(); 1459 assert!( 1460 body.contains("Too many failed attempts") || body.contains("No 2FA challenge found"), 1461 "Should be locked out after max attempts. Body: {}", 1462 &body[..body.len().min(500)] 1463 ); 1464} 1465 1466#[tokio::test] 1467async fn test_account_selector_with_2fa_requires_verification() { 1468 let url = base_url().await; 1469 let http_client = client(); 1470 1471 let ts = Utc::now().timestamp_millis(); 1472 let handle = format!("selector-2fa-{}", ts); 1473 let email = format!("selector-2fa-{}@example.com", ts); 1474 let password = "selector-2fa-password"; 1475 1476 let create_res = http_client 1477 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1478 .json(&json!({ 1479 "handle": handle, 1480 "email": email, 1481 "password": password 1482 })) 1483 .send() 1484 .await 1485 .unwrap(); 1486 assert_eq!(create_res.status(), StatusCode::OK); 1487 let account: Value = create_res.json().await.unwrap(); 1488 let user_did = account["did"].as_str().unwrap().to_string(); 1489 1490 let redirect_uri = "https://example.com/selector-2fa-callback"; 1491 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1492 let client_id = mock_client.uri(); 1493 1494 let (code_verifier, code_challenge) = generate_pkce(); 1495 1496 let par_body: Value = http_client 1497 .post(format!("{}/oauth/par", url)) 1498 .form(&[ 1499 ("response_type", "code"), 1500 ("client_id", &client_id), 1501 ("redirect_uri", redirect_uri), 1502 ("code_challenge", &code_challenge), 1503 ("code_challenge_method", "S256"), 1504 ]) 1505 .send() 1506 .await 1507 .unwrap() 1508 .json() 1509 .await 1510 .unwrap(); 1511 1512 let request_uri = par_body["request_uri"].as_str().unwrap(); 1513 1514 let auth_client = no_redirect_client(); 1515 let auth_res = auth_client 1516 .post(format!("{}/oauth/authorize", url)) 1517 .form(&[ 1518 ("request_uri", request_uri), 1519 ("username", &handle), 1520 ("password", password), 1521 ("remember_device", "true"), 1522 ]) 1523 .send() 1524 .await 1525 .unwrap(); 1526 1527 assert!(auth_res.status().is_redirection()); 1528 1529 let device_cookie = auth_res.headers() 1530 .get("set-cookie") 1531 .and_then(|v| v.to_str().ok()) 1532 .map(|s| s.split(';').next().unwrap_or("").to_string()) 1533 .expect("Should have received device cookie"); 1534 1535 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 1536 assert!(location.contains("code="), "First auth should succeed"); 1537 1538 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 1539 let _token_body: Value = http_client 1540 .post(format!("{}/oauth/token", url)) 1541 .form(&[ 1542 ("grant_type", "authorization_code"), 1543 ("code", code), 1544 ("redirect_uri", redirect_uri), 1545 ("code_verifier", &code_verifier), 1546 ("client_id", &client_id), 1547 ]) 1548 .send() 1549 .await 1550 .unwrap() 1551 .json() 1552 .await 1553 .unwrap(); 1554 1555 let db_url = common::get_db_connection_string().await; 1556 let pool = sqlx::postgres::PgPoolOptions::new() 1557 .max_connections(1) 1558 .connect(&db_url) 1559 .await 1560 .expect("Failed to connect to database"); 1561 1562 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1563 .bind(&user_did) 1564 .execute(&pool) 1565 .await 1566 .expect("Failed to enable 2FA"); 1567 1568 let (code_verifier2, code_challenge2) = generate_pkce(); 1569 1570 let par_body2: Value = http_client 1571 .post(format!("{}/oauth/par", url)) 1572 .form(&[ 1573 ("response_type", "code"), 1574 ("client_id", &client_id), 1575 ("redirect_uri", redirect_uri), 1576 ("code_challenge", &code_challenge2), 1577 ("code_challenge_method", "S256"), 1578 ]) 1579 .send() 1580 .await 1581 .unwrap() 1582 .json() 1583 .await 1584 .unwrap(); 1585 1586 let request_uri2 = par_body2["request_uri"].as_str().unwrap(); 1587 1588 let select_res = auth_client 1589 .post(format!("{}/oauth/authorize/select", url)) 1590 .header("cookie", &device_cookie) 1591 .form(&[ 1592 ("request_uri", request_uri2), 1593 ("did", &user_did), 1594 ]) 1595 .send() 1596 .await 1597 .unwrap(); 1598 1599 assert!( 1600 select_res.status().is_redirection(), 1601 "Account selector should redirect, got status: {}", 1602 select_res.status() 1603 ); 1604 1605 let select_location = select_res.headers().get("location").unwrap().to_str().unwrap(); 1606 assert!( 1607 select_location.contains("/oauth/authorize/2fa"), 1608 "Account selector with 2FA enabled should redirect to 2FA page, got: {}", 1609 select_location 1610 ); 1611 1612 let twofa_code: String = sqlx::query_scalar( 1613 "SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1" 1614 ) 1615 .bind(request_uri2) 1616 .fetch_one(&pool) 1617 .await 1618 .expect("Failed to get 2FA code"); 1619 1620 let twofa_res = auth_client 1621 .post(format!("{}/oauth/authorize/2fa", url)) 1622 .header("cookie", &device_cookie) 1623 .form(&[ 1624 ("request_uri", request_uri2), 1625 ("code", &twofa_code), 1626 ]) 1627 .send() 1628 .await 1629 .unwrap(); 1630 1631 assert!(twofa_res.status().is_redirection()); 1632 let final_location = twofa_res.headers().get("location").unwrap().to_str().unwrap(); 1633 assert!( 1634 final_location.starts_with(redirect_uri) && final_location.contains("code="), 1635 "After 2FA, should redirect to client with code, got: {}", 1636 final_location 1637 ); 1638 1639 let final_code = final_location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 1640 let token_res = http_client 1641 .post(format!("{}/oauth/token", url)) 1642 .form(&[ 1643 ("grant_type", "authorization_code"), 1644 ("code", final_code), 1645 ("redirect_uri", redirect_uri), 1646 ("code_verifier", &code_verifier2), 1647 ("client_id", &client_id), 1648 ]) 1649 .send() 1650 .await 1651 .unwrap(); 1652 1653 assert_eq!(token_res.status(), StatusCode::OK); 1654 let final_token: Value = token_res.json().await.unwrap(); 1655 assert_eq!(final_token["sub"], user_did, "Token should be for the correct user"); 1656}