this repo has no description
1mod common; 2mod helpers; 3use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 4use chrono::Utc; 5use common::{base_url, client, create_account_and_login}; 6use reqwest::{StatusCode, redirect}; 7use serde_json::{Value, json}; 8use sha2::{Digest, Sha256}; 9use wiremock::matchers::{method, path}; 10use wiremock::{Mock, MockServer, ResponseTemplate}; 11 12fn no_redirect_client() -> reqwest::Client { 13 reqwest::Client::builder() 14 .redirect(redirect::Policy::none()) 15 .build() 16 .unwrap() 17} 18 19fn generate_pkce() -> (String, String) { 20 let verifier_bytes: [u8; 32] = rand::random(); 21 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes); 22 let mut hasher = Sha256::new(); 23 hasher.update(code_verifier.as_bytes()); 24 let hash = hasher.finalize(); 25 let code_challenge = URL_SAFE_NO_PAD.encode(&hash); 26 (code_verifier, code_challenge) 27} 28 29async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer { 30 let mock_server = MockServer::start().await; 31 let client_id = mock_server.uri(); 32 let metadata = json!({ 33 "client_id": client_id, 34 "client_name": "Test OAuth Client", 35 "redirect_uris": [redirect_uri], 36 "grant_types": ["authorization_code", "refresh_token"], 37 "response_types": ["code"], 38 "token_endpoint_auth_method": "none", 39 "dpop_bound_access_tokens": false 40 }); 41 Mock::given(method("GET")) 42 .and(path("/")) 43 .respond_with(ResponseTemplate::new(200).set_body_json(metadata)) 44 .mount(&mock_server) 45 .await; 46 mock_server 47} 48#[allow(dead_code)] 49async fn setup_mock_dpop_client(redirect_uri: &str) -> MockServer { 50 let mock_server = MockServer::start().await; 51 let client_id = mock_server.uri(); 52 let metadata = json!({ 53 "client_id": client_id, 54 "client_name": "DPoP Test Client", 55 "redirect_uris": [redirect_uri], 56 "grant_types": ["authorization_code", "refresh_token"], 57 "response_types": ["code"], 58 "token_endpoint_auth_method": "none", 59 "dpop_bound_access_tokens": true 60 }); 61 Mock::given(method("GET")) 62 .and(path("/")) 63 .respond_with(ResponseTemplate::new(200).set_body_json(metadata)) 64 .mount(&mock_server) 65 .await; 66 mock_server 67} 68#[tokio::test] 69async fn test_oauth_protected_resource_metadata() { 70 let url = base_url().await; 71 let client = client(); 72 let res = client 73 .get(format!("{}/.well-known/oauth-protected-resource", url)) 74 .send() 75 .await 76 .expect("Failed to fetch protected resource metadata"); 77 assert_eq!(res.status(), StatusCode::OK); 78 let body: Value = res.json().await.expect("Invalid JSON"); 79 assert!(body["resource"].is_string()); 80 assert!(body["authorization_servers"].is_array()); 81 assert!(body["bearer_methods_supported"].is_array()); 82 let bearer_methods = body["bearer_methods_supported"].as_array().unwrap(); 83 assert!(bearer_methods.contains(&json!("header"))); 84} 85#[tokio::test] 86async fn test_oauth_authorization_server_metadata() { 87 let url = base_url().await; 88 let client = client(); 89 let res = client 90 .get(format!("{}/.well-known/oauth-authorization-server", url)) 91 .send() 92 .await 93 .expect("Failed to fetch authorization server metadata"); 94 assert_eq!(res.status(), StatusCode::OK); 95 let body: Value = res.json().await.expect("Invalid JSON"); 96 assert!(body["issuer"].is_string()); 97 assert!(body["authorization_endpoint"].is_string()); 98 assert!(body["token_endpoint"].is_string()); 99 assert!(body["jwks_uri"].is_string()); 100 let response_types = body["response_types_supported"].as_array().unwrap(); 101 assert!(response_types.contains(&json!("code"))); 102 let grant_types = body["grant_types_supported"].as_array().unwrap(); 103 assert!(grant_types.contains(&json!("authorization_code"))); 104 assert!(grant_types.contains(&json!("refresh_token"))); 105 let code_challenge_methods = body["code_challenge_methods_supported"].as_array().unwrap(); 106 assert!(code_challenge_methods.contains(&json!("S256"))); 107 assert_eq!(body["require_pushed_authorization_requests"], json!(true)); 108 let dpop_algs = body["dpop_signing_alg_values_supported"] 109 .as_array() 110 .unwrap(); 111 assert!(dpop_algs.contains(&json!("ES256"))); 112} 113#[tokio::test] 114async fn test_oauth_jwks_endpoint() { 115 let url = base_url().await; 116 let client = client(); 117 let res = client 118 .get(format!("{}/oauth/jwks", url)) 119 .send() 120 .await 121 .expect("Failed to fetch JWKS"); 122 assert_eq!(res.status(), StatusCode::OK); 123 let body: Value = res.json().await.expect("Invalid JSON"); 124 assert!(body["keys"].is_array()); 125} 126#[tokio::test] 127async fn test_par_success() { 128 let url = base_url().await; 129 let client = client(); 130 let redirect_uri = "https://example.com/callback"; 131 let mock_client = setup_mock_client_metadata(redirect_uri).await; 132 let client_id = mock_client.uri(); 133 let (_code_verifier, code_challenge) = generate_pkce(); 134 let res = client 135 .post(format!("{}/oauth/par", url)) 136 .form(&[ 137 ("response_type", "code"), 138 ("client_id", &client_id), 139 ("redirect_uri", redirect_uri), 140 ("code_challenge", &code_challenge), 141 ("code_challenge_method", "S256"), 142 ("scope", "atproto"), 143 ("state", "test-state-123"), 144 ]) 145 .send() 146 .await 147 .expect("Failed to send PAR request"); 148 assert_eq!( 149 res.status(), 150 StatusCode::CREATED, 151 "PAR should succeed: {:?}", 152 res.text().await 153 ); 154 let body: Value = client 155 .post(format!("{}/oauth/par", url)) 156 .form(&[ 157 ("response_type", "code"), 158 ("client_id", &client_id), 159 ("redirect_uri", redirect_uri), 160 ("code_challenge", &code_challenge), 161 ("code_challenge_method", "S256"), 162 ("scope", "atproto"), 163 ("state", "test-state-123"), 164 ]) 165 .send() 166 .await 167 .unwrap() 168 .json() 169 .await 170 .expect("Invalid JSON"); 171 assert!(body["request_uri"].is_string()); 172 assert!(body["expires_in"].is_number()); 173 let request_uri = body["request_uri"].as_str().unwrap(); 174 assert!(request_uri.starts_with("urn:ietf:params:oauth:request_uri:")); 175} 176#[tokio::test] 177async fn test_authorize_get_with_valid_request_uri() { 178 let url = base_url().await; 179 let client = client(); 180 let redirect_uri = "https://example.com/callback"; 181 let mock_client = setup_mock_client_metadata(redirect_uri).await; 182 let client_id = mock_client.uri(); 183 let (_, code_challenge) = generate_pkce(); 184 let par_res = client 185 .post(format!("{}/oauth/par", url)) 186 .form(&[ 187 ("response_type", "code"), 188 ("client_id", &client_id), 189 ("redirect_uri", redirect_uri), 190 ("code_challenge", &code_challenge), 191 ("code_challenge_method", "S256"), 192 ("scope", "atproto"), 193 ("state", "test-state"), 194 ]) 195 .send() 196 .await 197 .expect("PAR failed"); 198 let par_body: Value = par_res.json().await.expect("Invalid PAR JSON"); 199 let request_uri = par_body["request_uri"].as_str().unwrap(); 200 let auth_res = client 201 .get(format!("{}/oauth/authorize", url)) 202 .header("Accept", "application/json") 203 .query(&[("request_uri", request_uri)]) 204 .send() 205 .await 206 .expect("Authorize GET failed"); 207 assert_eq!(auth_res.status(), StatusCode::OK); 208 let auth_body: Value = auth_res.json().await.expect("Invalid auth JSON"); 209 assert_eq!(auth_body["client_id"], client_id); 210 assert_eq!(auth_body["redirect_uri"], redirect_uri); 211 assert_eq!(auth_body["scope"], "atproto"); 212 assert_eq!(auth_body["state"], "test-state"); 213} 214#[tokio::test] 215async fn test_authorize_rejects_invalid_request_uri() { 216 let url = base_url().await; 217 let client = client(); 218 let res = client 219 .get(format!("{}/oauth/authorize", url)) 220 .header("Accept", "application/json") 221 .query(&[( 222 "request_uri", 223 "urn:ietf:params:oauth:request_uri:nonexistent", 224 )]) 225 .send() 226 .await 227 .expect("Request failed"); 228 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 229 let body: Value = res.json().await.expect("Invalid JSON"); 230 assert_eq!(body["error"], "invalid_request"); 231} 232#[tokio::test] 233async fn test_authorize_requires_request_uri() { 234 let url = base_url().await; 235 let client = client(); 236 let res = client 237 .get(format!("{}/oauth/authorize", url)) 238 .send() 239 .await 240 .expect("Request failed"); 241 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 242} 243#[tokio::test] 244async fn test_full_oauth_flow_without_dpop() { 245 let url = base_url().await; 246 let http_client = client(); 247 let (_, _user_did) = create_account_and_login(&http_client).await; 248 let ts = Utc::now().timestamp_millis(); 249 let handle = format!("oauth-test-{}", ts); 250 let email = format!("oauth-test-{}@example.com", ts); 251 let password = "oauth-test-password"; 252 let create_res = http_client 253 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 254 .json(&json!({ 255 "handle": handle, 256 "email": email, 257 "password": password 258 })) 259 .send() 260 .await 261 .expect("Account creation failed"); 262 assert_eq!(create_res.status(), StatusCode::OK); 263 let account: Value = create_res.json().await.unwrap(); 264 let user_did = account["did"].as_str().unwrap(); 265 let redirect_uri = "https://example.com/oauth/callback"; 266 let mock_client = setup_mock_client_metadata(redirect_uri).await; 267 let client_id = mock_client.uri(); 268 let (code_verifier, code_challenge) = generate_pkce(); 269 let state = format!("state-{}", ts); 270 let par_res = http_client 271 .post(format!("{}/oauth/par", url)) 272 .form(&[ 273 ("response_type", "code"), 274 ("client_id", &client_id), 275 ("redirect_uri", redirect_uri), 276 ("code_challenge", &code_challenge), 277 ("code_challenge_method", "S256"), 278 ("scope", "atproto"), 279 ("state", &state), 280 ]) 281 .send() 282 .await 283 .expect("PAR failed"); 284 let par_status = par_res.status(); 285 let par_text = par_res.text().await.unwrap_or_default(); 286 if par_status != StatusCode::OK && par_status != StatusCode::CREATED { 287 panic!("PAR failed with status {}: {}", par_status, par_text); 288 } 289 let par_body: Value = serde_json::from_str(&par_text).unwrap(); 290 let request_uri = par_body["request_uri"].as_str().unwrap(); 291 let auth_client = no_redirect_client(); 292 let auth_res = auth_client 293 .post(format!("{}/oauth/authorize", url)) 294 .form(&[ 295 ("request_uri", request_uri), 296 ("username", &handle), 297 ("password", password), 298 ("remember_device", "false"), 299 ]) 300 .send() 301 .await 302 .expect("Authorize POST failed"); 303 let auth_status = auth_res.status(); 304 if auth_status != StatusCode::TEMPORARY_REDIRECT 305 && auth_status != StatusCode::SEE_OTHER 306 && auth_status != StatusCode::FOUND 307 { 308 let auth_text = auth_res.text().await.unwrap_or_default(); 309 panic!("Expected redirect, got {}: {}", auth_status, auth_text); 310 } 311 let location = auth_res 312 .headers() 313 .get("location") 314 .expect("No Location header") 315 .to_str() 316 .unwrap(); 317 assert!( 318 location.starts_with(redirect_uri), 319 "Redirect to wrong URI: {}", 320 location 321 ); 322 assert!( 323 location.contains("code="), 324 "No code in redirect: {}", 325 location 326 ); 327 assert!( 328 location.contains(&format!("state={}", state)), 329 "Wrong state in redirect" 330 ); 331 let code = location 332 .split("code=") 333 .nth(1) 334 .unwrap() 335 .split('&') 336 .next() 337 .unwrap(); 338 let token_res = http_client 339 .post(format!("{}/oauth/token", url)) 340 .form(&[ 341 ("grant_type", "authorization_code"), 342 ("code", code), 343 ("redirect_uri", redirect_uri), 344 ("code_verifier", &code_verifier), 345 ("client_id", &client_id), 346 ]) 347 .send() 348 .await 349 .expect("Token request failed"); 350 let token_status = token_res.status(); 351 let token_text = token_res.text().await.unwrap_or_default(); 352 if token_status != StatusCode::OK { 353 panic!( 354 "Token request failed with status {}: {}", 355 token_status, token_text 356 ); 357 } 358 let token_body: Value = serde_json::from_str(&token_text).unwrap(); 359 assert!(token_body["access_token"].is_string()); 360 assert!(token_body["refresh_token"].is_string()); 361 assert_eq!(token_body["token_type"], "Bearer"); 362 assert!(token_body["expires_in"].is_number()); 363 assert_eq!(token_body["sub"], user_did); 364} 365#[tokio::test] 366async fn test_token_refresh_flow() { 367 let url = base_url().await; 368 let http_client = client(); 369 let ts = Utc::now().timestamp_millis(); 370 let handle = format!("refresh-test-{}", ts); 371 let email = format!("refresh-test-{}@example.com", ts); 372 let password = "refresh-test-password"; 373 http_client 374 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 375 .json(&json!({ 376 "handle": handle, 377 "email": email, 378 "password": password 379 })) 380 .send() 381 .await 382 .expect("Account creation failed"); 383 let redirect_uri = "https://example.com/refresh-callback"; 384 let mock_client = setup_mock_client_metadata(redirect_uri).await; 385 let client_id = mock_client.uri(); 386 let (code_verifier, code_challenge) = generate_pkce(); 387 let par_body: Value = http_client 388 .post(format!("{}/oauth/par", url)) 389 .form(&[ 390 ("response_type", "code"), 391 ("client_id", &client_id), 392 ("redirect_uri", redirect_uri), 393 ("code_challenge", &code_challenge), 394 ("code_challenge_method", "S256"), 395 ]) 396 .send() 397 .await 398 .unwrap() 399 .json() 400 .await 401 .unwrap(); 402 let request_uri = par_body["request_uri"].as_str().unwrap(); 403 let auth_client = no_redirect_client(); 404 let auth_res = auth_client 405 .post(format!("{}/oauth/authorize", url)) 406 .form(&[ 407 ("request_uri", request_uri), 408 ("username", &handle), 409 ("password", password), 410 ("remember_device", "false"), 411 ]) 412 .send() 413 .await 414 .unwrap(); 415 let location = auth_res 416 .headers() 417 .get("location") 418 .unwrap() 419 .to_str() 420 .unwrap(); 421 let code = location 422 .split("code=") 423 .nth(1) 424 .unwrap() 425 .split('&') 426 .next() 427 .unwrap(); 428 let token_body: Value = http_client 429 .post(format!("{}/oauth/token", url)) 430 .form(&[ 431 ("grant_type", "authorization_code"), 432 ("code", code), 433 ("redirect_uri", redirect_uri), 434 ("code_verifier", &code_verifier), 435 ("client_id", &client_id), 436 ]) 437 .send() 438 .await 439 .unwrap() 440 .json() 441 .await 442 .unwrap(); 443 let refresh_token = token_body["refresh_token"].as_str().unwrap(); 444 let original_access_token = token_body["access_token"].as_str().unwrap(); 445 let refresh_res = http_client 446 .post(format!("{}/oauth/token", url)) 447 .form(&[ 448 ("grant_type", "refresh_token"), 449 ("refresh_token", refresh_token), 450 ("client_id", &client_id), 451 ]) 452 .send() 453 .await 454 .expect("Refresh request failed"); 455 assert_eq!(refresh_res.status(), StatusCode::OK); 456 let refresh_body: Value = refresh_res.json().await.unwrap(); 457 assert!(refresh_body["access_token"].is_string()); 458 assert!(refresh_body["refresh_token"].is_string()); 459 let new_access_token = refresh_body["access_token"].as_str().unwrap(); 460 let new_refresh_token = refresh_body["refresh_token"].as_str().unwrap(); 461 assert_ne!( 462 new_access_token, original_access_token, 463 "Access token should rotate" 464 ); 465 assert_ne!( 466 new_refresh_token, refresh_token, 467 "Refresh token should rotate" 468 ); 469} 470#[tokio::test] 471async fn test_wrong_credentials_denied() { 472 let url = base_url().await; 473 let http_client = client(); 474 let ts = Utc::now().timestamp_millis(); 475 let handle = format!("wrong-creds-{}", ts); 476 let email = format!("wrong-creds-{}@example.com", ts); 477 let password = "correct-password"; 478 http_client 479 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 480 .json(&json!({ 481 "handle": handle, 482 "email": email, 483 "password": password 484 })) 485 .send() 486 .await 487 .unwrap(); 488 let redirect_uri = "https://example.com/wrong-creds-callback"; 489 let mock_client = setup_mock_client_metadata(redirect_uri).await; 490 let client_id = mock_client.uri(); 491 let (_, code_challenge) = generate_pkce(); 492 let par_body: Value = http_client 493 .post(format!("{}/oauth/par", url)) 494 .form(&[ 495 ("response_type", "code"), 496 ("client_id", &client_id), 497 ("redirect_uri", redirect_uri), 498 ("code_challenge", &code_challenge), 499 ("code_challenge_method", "S256"), 500 ]) 501 .send() 502 .await 503 .unwrap() 504 .json() 505 .await 506 .unwrap(); 507 let request_uri = par_body["request_uri"].as_str().unwrap(); 508 let auth_res = http_client 509 .post(format!("{}/oauth/authorize", url)) 510 .header("Accept", "application/json") 511 .form(&[ 512 ("request_uri", request_uri), 513 ("username", &handle), 514 ("password", "wrong-password"), 515 ("remember_device", "false"), 516 ]) 517 .send() 518 .await 519 .unwrap(); 520 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN); 521 let error_body: Value = auth_res.json().await.unwrap(); 522 assert_eq!(error_body["error"], "access_denied"); 523} 524#[tokio::test] 525async fn test_token_revocation() { 526 let url = base_url().await; 527 let http_client = client(); 528 let ts = Utc::now().timestamp_millis(); 529 let handle = format!("revoke-test-{}", ts); 530 let email = format!("revoke-test-{}@example.com", ts); 531 let password = "revoke-test-password"; 532 http_client 533 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 534 .json(&json!({ 535 "handle": handle, 536 "email": email, 537 "password": password 538 })) 539 .send() 540 .await 541 .unwrap(); 542 let redirect_uri = "https://example.com/revoke-callback"; 543 let mock_client = setup_mock_client_metadata(redirect_uri).await; 544 let client_id = mock_client.uri(); 545 let (code_verifier, code_challenge) = generate_pkce(); 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 let request_uri = par_body["request_uri"].as_str().unwrap(); 562 let auth_client = no_redirect_client(); 563 let auth_res = auth_client 564 .post(format!("{}/oauth/authorize", url)) 565 .form(&[ 566 ("request_uri", request_uri), 567 ("username", &handle), 568 ("password", password), 569 ("remember_device", "false"), 570 ]) 571 .send() 572 .await 573 .unwrap(); 574 let location = auth_res 575 .headers() 576 .get("location") 577 .unwrap() 578 .to_str() 579 .unwrap(); 580 let code = location 581 .split("code=") 582 .nth(1) 583 .unwrap() 584 .split('&') 585 .next() 586 .unwrap(); 587 let token_body: Value = http_client 588 .post(format!("{}/oauth/token", url)) 589 .form(&[ 590 ("grant_type", "authorization_code"), 591 ("code", code), 592 ("redirect_uri", redirect_uri), 593 ("code_verifier", &code_verifier), 594 ("client_id", &client_id), 595 ]) 596 .send() 597 .await 598 .unwrap() 599 .json() 600 .await 601 .unwrap(); 602 let refresh_token = token_body["refresh_token"].as_str().unwrap(); 603 let revoke_res = http_client 604 .post(format!("{}/oauth/revoke", url)) 605 .form(&[("token", refresh_token)]) 606 .send() 607 .await 608 .unwrap(); 609 assert_eq!(revoke_res.status(), StatusCode::OK); 610 let refresh_after_revoke = http_client 611 .post(format!("{}/oauth/token", url)) 612 .form(&[ 613 ("grant_type", "refresh_token"), 614 ("refresh_token", refresh_token), 615 ("client_id", &client_id), 616 ]) 617 .send() 618 .await 619 .unwrap(); 620 assert_eq!(refresh_after_revoke.status(), StatusCode::BAD_REQUEST); 621} 622#[tokio::test] 623async fn test_unsupported_grant_type() { 624 let url = base_url().await; 625 let http_client = client(); 626 let res = http_client 627 .post(format!("{}/oauth/token", url)) 628 .form(&[ 629 ("grant_type", "client_credentials"), 630 ("client_id", "https://example.com"), 631 ]) 632 .send() 633 .await 634 .unwrap(); 635 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 636 let body: Value = res.json().await.unwrap(); 637 assert_eq!(body["error"], "unsupported_grant_type"); 638} 639#[tokio::test] 640async fn test_invalid_refresh_token() { 641 let url = base_url().await; 642 let http_client = client(); 643 let res = http_client 644 .post(format!("{}/oauth/token", url)) 645 .form(&[ 646 ("grant_type", "refresh_token"), 647 ("refresh_token", "invalid-refresh-token"), 648 ("client_id", "https://example.com"), 649 ]) 650 .send() 651 .await 652 .unwrap(); 653 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 654 let body: Value = res.json().await.unwrap(); 655 assert_eq!(body["error"], "invalid_grant"); 656} 657#[tokio::test] 658async fn test_expired_authorization_request() { 659 let url = base_url().await; 660 let http_client = client(); 661 let res = http_client 662 .get(format!("{}/oauth/authorize", url)) 663 .header("Accept", "application/json") 664 .query(&[( 665 "request_uri", 666 "urn:ietf:params:oauth:request_uri:expired-or-nonexistent", 667 )]) 668 .send() 669 .await 670 .unwrap(); 671 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 672 let body: Value = res.json().await.unwrap(); 673 assert_eq!(body["error"], "invalid_request"); 674} 675#[tokio::test] 676async fn test_token_introspection() { 677 let url = base_url().await; 678 let http_client = client(); 679 let ts = Utc::now().timestamp_millis(); 680 let handle = format!("introspect-{}", ts); 681 let email = format!("introspect-{}@example.com", ts); 682 let password = "introspect-password"; 683 http_client 684 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 685 .json(&json!({ 686 "handle": handle, 687 "email": email, 688 "password": password 689 })) 690 .send() 691 .await 692 .unwrap(); 693 let redirect_uri = "https://example.com/introspect-callback"; 694 let mock_client = setup_mock_client_metadata(redirect_uri).await; 695 let client_id = mock_client.uri(); 696 let (code_verifier, code_challenge) = generate_pkce(); 697 let par_body: Value = http_client 698 .post(format!("{}/oauth/par", url)) 699 .form(&[ 700 ("response_type", "code"), 701 ("client_id", &client_id), 702 ("redirect_uri", redirect_uri), 703 ("code_challenge", &code_challenge), 704 ("code_challenge_method", "S256"), 705 ]) 706 .send() 707 .await 708 .unwrap() 709 .json() 710 .await 711 .unwrap(); 712 let request_uri = par_body["request_uri"].as_str().unwrap(); 713 let auth_client = no_redirect_client(); 714 let auth_res = auth_client 715 .post(format!("{}/oauth/authorize", url)) 716 .form(&[ 717 ("request_uri", request_uri), 718 ("username", &handle), 719 ("password", password), 720 ("remember_device", "false"), 721 ]) 722 .send() 723 .await 724 .unwrap(); 725 let location = auth_res 726 .headers() 727 .get("location") 728 .unwrap() 729 .to_str() 730 .unwrap(); 731 let code = location 732 .split("code=") 733 .nth(1) 734 .unwrap() 735 .split('&') 736 .next() 737 .unwrap(); 738 let token_body: Value = http_client 739 .post(format!("{}/oauth/token", url)) 740 .form(&[ 741 ("grant_type", "authorization_code"), 742 ("code", code), 743 ("redirect_uri", redirect_uri), 744 ("code_verifier", &code_verifier), 745 ("client_id", &client_id), 746 ]) 747 .send() 748 .await 749 .unwrap() 750 .json() 751 .await 752 .unwrap(); 753 let access_token = token_body["access_token"].as_str().unwrap(); 754 let introspect_res = http_client 755 .post(format!("{}/oauth/introspect", url)) 756 .form(&[("token", access_token)]) 757 .send() 758 .await 759 .unwrap(); 760 assert_eq!(introspect_res.status(), StatusCode::OK); 761 let introspect_body: Value = introspect_res.json().await.unwrap(); 762 assert_eq!(introspect_body["active"], true); 763 assert!(introspect_body["client_id"].is_string()); 764 assert!(introspect_body["exp"].is_number()); 765} 766#[tokio::test] 767async fn test_introspect_invalid_token() { 768 let url = base_url().await; 769 let http_client = client(); 770 let res = http_client 771 .post(format!("{}/oauth/introspect", url)) 772 .form(&[("token", "invalid.token.here")]) 773 .send() 774 .await 775 .unwrap(); 776 assert_eq!(res.status(), StatusCode::OK); 777 let body: Value = res.json().await.unwrap(); 778 assert_eq!(body["active"], false); 779} 780#[tokio::test] 781async fn test_introspect_revoked_token() { 782 let url = base_url().await; 783 let http_client = client(); 784 let ts = Utc::now().timestamp_millis(); 785 let handle = format!("introspect-revoked-{}", ts); 786 let email = format!("introspect-revoked-{}@example.com", ts); 787 let password = "introspect-revoked-password"; 788 http_client 789 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 790 .json(&json!({ 791 "handle": handle, 792 "email": email, 793 "password": password 794 })) 795 .send() 796 .await 797 .unwrap(); 798 let redirect_uri = "https://example.com/introspect-revoked-callback"; 799 let mock_client = setup_mock_client_metadata(redirect_uri).await; 800 let client_id = mock_client.uri(); 801 let (code_verifier, code_challenge) = generate_pkce(); 802 let par_body: Value = http_client 803 .post(format!("{}/oauth/par", url)) 804 .form(&[ 805 ("response_type", "code"), 806 ("client_id", &client_id), 807 ("redirect_uri", redirect_uri), 808 ("code_challenge", &code_challenge), 809 ("code_challenge_method", "S256"), 810 ]) 811 .send() 812 .await 813 .unwrap() 814 .json() 815 .await 816 .unwrap(); 817 let request_uri = par_body["request_uri"].as_str().unwrap(); 818 let auth_client = no_redirect_client(); 819 let auth_res = auth_client 820 .post(format!("{}/oauth/authorize", url)) 821 .form(&[ 822 ("request_uri", request_uri), 823 ("username", &handle), 824 ("password", password), 825 ("remember_device", "false"), 826 ]) 827 .send() 828 .await 829 .unwrap(); 830 let location = auth_res 831 .headers() 832 .get("location") 833 .unwrap() 834 .to_str() 835 .unwrap(); 836 let code = location 837 .split("code=") 838 .nth(1) 839 .unwrap() 840 .split('&') 841 .next() 842 .unwrap(); 843 let token_body: Value = http_client 844 .post(format!("{}/oauth/token", url)) 845 .form(&[ 846 ("grant_type", "authorization_code"), 847 ("code", code), 848 ("redirect_uri", redirect_uri), 849 ("code_verifier", &code_verifier), 850 ("client_id", &client_id), 851 ]) 852 .send() 853 .await 854 .unwrap() 855 .json() 856 .await 857 .unwrap(); 858 let access_token = token_body["access_token"].as_str().unwrap(); 859 let refresh_token = token_body["refresh_token"].as_str().unwrap(); 860 http_client 861 .post(format!("{}/oauth/revoke", url)) 862 .form(&[("token", refresh_token)]) 863 .send() 864 .await 865 .unwrap(); 866 let introspect_res = http_client 867 .post(format!("{}/oauth/introspect", url)) 868 .form(&[("token", access_token)]) 869 .send() 870 .await 871 .unwrap(); 872 assert_eq!(introspect_res.status(), StatusCode::OK); 873 let body: Value = introspect_res.json().await.unwrap(); 874 assert_eq!(body["active"], false, "Revoked token should be inactive"); 875} 876#[tokio::test] 877async fn test_state_with_special_chars() { 878 let url = base_url().await; 879 let http_client = client(); 880 let ts = Utc::now().timestamp_millis(); 881 let handle = format!("state-special-{}", ts); 882 let email = format!("state-special-{}@example.com", ts); 883 let password = "state-special-password"; 884 http_client 885 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 886 .json(&json!({ 887 "handle": handle, 888 "email": email, 889 "password": password 890 })) 891 .send() 892 .await 893 .unwrap(); 894 let redirect_uri = "https://example.com/state-special-callback"; 895 let mock_client = setup_mock_client_metadata(redirect_uri).await; 896 let client_id = mock_client.uri(); 897 let (_code_verifier, code_challenge) = generate_pkce(); 898 let special_state = "state=with&special=chars&plus+more"; 899 let par_body: Value = http_client 900 .post(format!("{}/oauth/par", url)) 901 .form(&[ 902 ("response_type", "code"), 903 ("client_id", &client_id), 904 ("redirect_uri", redirect_uri), 905 ("code_challenge", &code_challenge), 906 ("code_challenge_method", "S256"), 907 ("state", special_state), 908 ]) 909 .send() 910 .await 911 .unwrap() 912 .json() 913 .await 914 .unwrap(); 915 let request_uri = par_body["request_uri"].as_str().unwrap(); 916 let auth_client = no_redirect_client(); 917 let auth_res = auth_client 918 .post(format!("{}/oauth/authorize", url)) 919 .form(&[ 920 ("request_uri", request_uri), 921 ("username", &handle), 922 ("password", password), 923 ("remember_device", "false"), 924 ]) 925 .send() 926 .await 927 .unwrap(); 928 assert!( 929 auth_res.status().is_redirection(), 930 "Should redirect even with special chars in state" 931 ); 932 let location = auth_res 933 .headers() 934 .get("location") 935 .unwrap() 936 .to_str() 937 .unwrap(); 938 assert!( 939 location.contains("state="), 940 "State should be in redirect URL" 941 ); 942 let encoded_state = urlencoding::encode(special_state); 943 assert!( 944 location.contains(&format!("state={}", encoded_state)), 945 "State should be URL-encoded. Got: {}", 946 location 947 ); 948} 949#[tokio::test] 950async fn test_2fa_required_when_enabled() { 951 let url = base_url().await; 952 let http_client = client(); 953 let ts = Utc::now().timestamp_millis(); 954 let handle = format!("2fa-required-{}", ts); 955 let email = format!("2fa-required-{}@example.com", ts); 956 let password = "2fa-test-password"; 957 let create_res = http_client 958 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 959 .json(&json!({ 960 "handle": handle, 961 "email": email, 962 "password": password 963 })) 964 .send() 965 .await 966 .unwrap(); 967 assert_eq!(create_res.status(), StatusCode::OK); 968 let account: Value = create_res.json().await.unwrap(); 969 let user_did = account["did"].as_str().unwrap(); 970 let db_url = common::get_db_connection_string().await; 971 let pool = sqlx::postgres::PgPoolOptions::new() 972 .max_connections(1) 973 .connect(&db_url) 974 .await 975 .expect("Failed to connect to database"); 976 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 977 .bind(user_did) 978 .execute(&pool) 979 .await 980 .expect("Failed to enable 2FA"); 981 let redirect_uri = "https://example.com/2fa-callback"; 982 let mock_client = setup_mock_client_metadata(redirect_uri).await; 983 let client_id = mock_client.uri(); 984 let (_, code_challenge) = generate_pkce(); 985 let par_body: Value = http_client 986 .post(format!("{}/oauth/par", url)) 987 .form(&[ 988 ("response_type", "code"), 989 ("client_id", &client_id), 990 ("redirect_uri", redirect_uri), 991 ("code_challenge", &code_challenge), 992 ("code_challenge_method", "S256"), 993 ]) 994 .send() 995 .await 996 .unwrap() 997 .json() 998 .await 999 .unwrap(); 1000 let request_uri = par_body["request_uri"].as_str().unwrap(); 1001 let auth_client = no_redirect_client(); 1002 let auth_res = auth_client 1003 .post(format!("{}/oauth/authorize", url)) 1004 .form(&[ 1005 ("request_uri", request_uri), 1006 ("username", &handle), 1007 ("password", password), 1008 ("remember_device", "false"), 1009 ]) 1010 .send() 1011 .await 1012 .unwrap(); 1013 assert!( 1014 auth_res.status().is_redirection(), 1015 "Should redirect to 2FA page, got status: {}", 1016 auth_res.status() 1017 ); 1018 let location = auth_res 1019 .headers() 1020 .get("location") 1021 .unwrap() 1022 .to_str() 1023 .unwrap(); 1024 assert!( 1025 location.contains("/oauth/authorize/2fa"), 1026 "Should redirect to 2FA page, got: {}", 1027 location 1028 ); 1029 assert!( 1030 location.contains("request_uri="), 1031 "2FA redirect should include request_uri" 1032 ); 1033} 1034#[tokio::test] 1035async fn test_2fa_invalid_code_rejected() { 1036 let url = base_url().await; 1037 let http_client = client(); 1038 let ts = Utc::now().timestamp_millis(); 1039 let handle = format!("2fa-invalid-{}", ts); 1040 let email = format!("2fa-invalid-{}@example.com", ts); 1041 let password = "2fa-test-password"; 1042 let create_res = http_client 1043 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1044 .json(&json!({ 1045 "handle": handle, 1046 "email": email, 1047 "password": password 1048 })) 1049 .send() 1050 .await 1051 .unwrap(); 1052 assert_eq!(create_res.status(), StatusCode::OK); 1053 let account: Value = create_res.json().await.unwrap(); 1054 let user_did = account["did"].as_str().unwrap(); 1055 let db_url = common::get_db_connection_string().await; 1056 let pool = sqlx::postgres::PgPoolOptions::new() 1057 .max_connections(1) 1058 .connect(&db_url) 1059 .await 1060 .expect("Failed to connect to database"); 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 let redirect_uri = "https://example.com/2fa-invalid-callback"; 1067 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1068 let client_id = mock_client.uri(); 1069 let (_, code_challenge) = generate_pkce(); 1070 let par_body: Value = http_client 1071 .post(format!("{}/oauth/par", url)) 1072 .form(&[ 1073 ("response_type", "code"), 1074 ("client_id", &client_id), 1075 ("redirect_uri", redirect_uri), 1076 ("code_challenge", &code_challenge), 1077 ("code_challenge_method", "S256"), 1078 ]) 1079 .send() 1080 .await 1081 .unwrap() 1082 .json() 1083 .await 1084 .unwrap(); 1085 let request_uri = par_body["request_uri"].as_str().unwrap(); 1086 let auth_client = no_redirect_client(); 1087 let auth_res = auth_client 1088 .post(format!("{}/oauth/authorize", url)) 1089 .form(&[ 1090 ("request_uri", request_uri), 1091 ("username", &handle), 1092 ("password", password), 1093 ("remember_device", "false"), 1094 ]) 1095 .send() 1096 .await 1097 .unwrap(); 1098 assert!(auth_res.status().is_redirection()); 1099 let location = auth_res 1100 .headers() 1101 .get("location") 1102 .unwrap() 1103 .to_str() 1104 .unwrap(); 1105 assert!(location.contains("/oauth/authorize/2fa")); 1106 let twofa_res = http_client 1107 .post(format!("{}/oauth/authorize/2fa", url)) 1108 .form(&[("request_uri", request_uri), ("code", "000000")]) 1109 .send() 1110 .await 1111 .unwrap(); 1112 assert_eq!(twofa_res.status(), StatusCode::OK); 1113 let body = twofa_res.text().await.unwrap(); 1114 assert!( 1115 body.contains("Invalid verification code") || body.contains("invalid"), 1116 "Should show error for invalid code" 1117 ); 1118} 1119#[tokio::test] 1120async fn test_2fa_valid_code_completes_auth() { 1121 let url = base_url().await; 1122 let http_client = client(); 1123 let ts = Utc::now().timestamp_millis(); 1124 let handle = format!("2fa-valid-{}", ts); 1125 let email = format!("2fa-valid-{}@example.com", ts); 1126 let password = "2fa-test-password"; 1127 let create_res = http_client 1128 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1129 .json(&json!({ 1130 "handle": handle, 1131 "email": email, 1132 "password": password 1133 })) 1134 .send() 1135 .await 1136 .unwrap(); 1137 assert_eq!(create_res.status(), StatusCode::OK); 1138 let account: Value = create_res.json().await.unwrap(); 1139 let user_did = account["did"].as_str().unwrap(); 1140 let db_url = common::get_db_connection_string().await; 1141 let pool = sqlx::postgres::PgPoolOptions::new() 1142 .max_connections(1) 1143 .connect(&db_url) 1144 .await 1145 .expect("Failed to connect to database"); 1146 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1147 .bind(user_did) 1148 .execute(&pool) 1149 .await 1150 .expect("Failed to enable 2FA"); 1151 let redirect_uri = "https://example.com/2fa-valid-callback"; 1152 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1153 let client_id = mock_client.uri(); 1154 let (code_verifier, code_challenge) = generate_pkce(); 1155 let par_body: Value = http_client 1156 .post(format!("{}/oauth/par", url)) 1157 .form(&[ 1158 ("response_type", "code"), 1159 ("client_id", &client_id), 1160 ("redirect_uri", redirect_uri), 1161 ("code_challenge", &code_challenge), 1162 ("code_challenge_method", "S256"), 1163 ]) 1164 .send() 1165 .await 1166 .unwrap() 1167 .json() 1168 .await 1169 .unwrap(); 1170 let request_uri = par_body["request_uri"].as_str().unwrap(); 1171 let auth_client = no_redirect_client(); 1172 let auth_res = auth_client 1173 .post(format!("{}/oauth/authorize", url)) 1174 .form(&[ 1175 ("request_uri", request_uri), 1176 ("username", &handle), 1177 ("password", password), 1178 ("remember_device", "false"), 1179 ]) 1180 .send() 1181 .await 1182 .unwrap(); 1183 assert!(auth_res.status().is_redirection()); 1184 let twofa_code: String = 1185 sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1") 1186 .bind(request_uri) 1187 .fetch_one(&pool) 1188 .await 1189 .expect("Failed to get 2FA code from database"); 1190 let twofa_res = auth_client 1191 .post(format!("{}/oauth/authorize/2fa", url)) 1192 .form(&[("request_uri", request_uri), ("code", &twofa_code)]) 1193 .send() 1194 .await 1195 .unwrap(); 1196 assert!( 1197 twofa_res.status().is_redirection(), 1198 "Valid 2FA code should redirect to success, got status: {}", 1199 twofa_res.status() 1200 ); 1201 let location = twofa_res 1202 .headers() 1203 .get("location") 1204 .unwrap() 1205 .to_str() 1206 .unwrap(); 1207 assert!( 1208 location.starts_with(redirect_uri), 1209 "Should redirect to client callback, got: {}", 1210 location 1211 ); 1212 assert!( 1213 location.contains("code="), 1214 "Redirect should include authorization code" 1215 ); 1216 let auth_code = location 1217 .split("code=") 1218 .nth(1) 1219 .unwrap() 1220 .split('&') 1221 .next() 1222 .unwrap(); 1223 let token_res = http_client 1224 .post(format!("{}/oauth/token", url)) 1225 .form(&[ 1226 ("grant_type", "authorization_code"), 1227 ("code", auth_code), 1228 ("redirect_uri", redirect_uri), 1229 ("code_verifier", &code_verifier), 1230 ("client_id", &client_id), 1231 ]) 1232 .send() 1233 .await 1234 .unwrap(); 1235 assert_eq!( 1236 token_res.status(), 1237 StatusCode::OK, 1238 "Token exchange should succeed" 1239 ); 1240 let token_body: Value = token_res.json().await.unwrap(); 1241 assert!(token_body["access_token"].is_string()); 1242 assert_eq!(token_body["sub"], user_did); 1243} 1244#[tokio::test] 1245async fn test_2fa_lockout_after_max_attempts() { 1246 let url = base_url().await; 1247 let http_client = client(); 1248 let ts = Utc::now().timestamp_millis(); 1249 let handle = format!("2fa-lockout-{}", ts); 1250 let email = format!("2fa-lockout-{}@example.com", ts); 1251 let password = "2fa-test-password"; 1252 let create_res = http_client 1253 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1254 .json(&json!({ 1255 "handle": handle, 1256 "email": email, 1257 "password": password 1258 })) 1259 .send() 1260 .await 1261 .unwrap(); 1262 assert_eq!(create_res.status(), StatusCode::OK); 1263 let account: Value = create_res.json().await.unwrap(); 1264 let user_did = account["did"].as_str().unwrap(); 1265 let db_url = common::get_db_connection_string().await; 1266 let pool = sqlx::postgres::PgPoolOptions::new() 1267 .max_connections(1) 1268 .connect(&db_url) 1269 .await 1270 .expect("Failed to connect to database"); 1271 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1272 .bind(user_did) 1273 .execute(&pool) 1274 .await 1275 .expect("Failed to enable 2FA"); 1276 let redirect_uri = "https://example.com/2fa-lockout-callback"; 1277 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1278 let client_id = mock_client.uri(); 1279 let (_, code_challenge) = generate_pkce(); 1280 let par_body: Value = http_client 1281 .post(format!("{}/oauth/par", url)) 1282 .form(&[ 1283 ("response_type", "code"), 1284 ("client_id", &client_id), 1285 ("redirect_uri", redirect_uri), 1286 ("code_challenge", &code_challenge), 1287 ("code_challenge_method", "S256"), 1288 ]) 1289 .send() 1290 .await 1291 .unwrap() 1292 .json() 1293 .await 1294 .unwrap(); 1295 let request_uri = par_body["request_uri"].as_str().unwrap(); 1296 let auth_client = no_redirect_client(); 1297 let auth_res = auth_client 1298 .post(format!("{}/oauth/authorize", url)) 1299 .form(&[ 1300 ("request_uri", request_uri), 1301 ("username", &handle), 1302 ("password", password), 1303 ("remember_device", "false"), 1304 ]) 1305 .send() 1306 .await 1307 .unwrap(); 1308 assert!(auth_res.status().is_redirection()); 1309 for i in 0..5 { 1310 let res = http_client 1311 .post(format!("{}/oauth/authorize/2fa", url)) 1312 .form(&[("request_uri", request_uri), ("code", "999999")]) 1313 .send() 1314 .await 1315 .unwrap(); 1316 if i < 4 { 1317 assert_eq!( 1318 res.status(), 1319 StatusCode::OK, 1320 "Attempt {} should show error page", 1321 i + 1 1322 ); 1323 let body = res.text().await.unwrap(); 1324 assert!( 1325 body.contains("Invalid verification code"), 1326 "Should show invalid code error on attempt {}", 1327 i + 1 1328 ); 1329 } 1330 } 1331 let lockout_res = http_client 1332 .post(format!("{}/oauth/authorize/2fa", url)) 1333 .form(&[("request_uri", request_uri), ("code", "999999")]) 1334 .send() 1335 .await 1336 .unwrap(); 1337 assert_eq!(lockout_res.status(), StatusCode::OK); 1338 let body = lockout_res.text().await.unwrap(); 1339 assert!( 1340 body.contains("Too many failed attempts") || body.contains("No 2FA challenge found"), 1341 "Should be locked out after max attempts. Body: {}", 1342 &body[..body.len().min(500)] 1343 ); 1344} 1345#[tokio::test] 1346async fn test_account_selector_with_2fa_requires_verification() { 1347 let url = base_url().await; 1348 let http_client = client(); 1349 let ts = Utc::now().timestamp_millis(); 1350 let handle = format!("selector-2fa-{}", ts); 1351 let email = format!("selector-2fa-{}@example.com", ts); 1352 let password = "selector-2fa-password"; 1353 let create_res = http_client 1354 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 1355 .json(&json!({ 1356 "handle": handle, 1357 "email": email, 1358 "password": password 1359 })) 1360 .send() 1361 .await 1362 .unwrap(); 1363 assert_eq!(create_res.status(), StatusCode::OK); 1364 let account: Value = create_res.json().await.unwrap(); 1365 let user_did = account["did"].as_str().unwrap().to_string(); 1366 let redirect_uri = "https://example.com/selector-2fa-callback"; 1367 let mock_client = setup_mock_client_metadata(redirect_uri).await; 1368 let client_id = mock_client.uri(); 1369 let (code_verifier, code_challenge) = generate_pkce(); 1370 let par_body: Value = http_client 1371 .post(format!("{}/oauth/par", url)) 1372 .form(&[ 1373 ("response_type", "code"), 1374 ("client_id", &client_id), 1375 ("redirect_uri", redirect_uri), 1376 ("code_challenge", &code_challenge), 1377 ("code_challenge_method", "S256"), 1378 ]) 1379 .send() 1380 .await 1381 .unwrap() 1382 .json() 1383 .await 1384 .unwrap(); 1385 let request_uri = par_body["request_uri"].as_str().unwrap(); 1386 let auth_client = no_redirect_client(); 1387 let auth_res = auth_client 1388 .post(format!("{}/oauth/authorize", url)) 1389 .form(&[ 1390 ("request_uri", request_uri), 1391 ("username", &handle), 1392 ("password", password), 1393 ("remember_device", "true"), 1394 ]) 1395 .send() 1396 .await 1397 .unwrap(); 1398 assert!(auth_res.status().is_redirection()); 1399 let device_cookie = auth_res 1400 .headers() 1401 .get("set-cookie") 1402 .and_then(|v| v.to_str().ok()) 1403 .map(|s| s.split(';').next().unwrap_or("").to_string()) 1404 .expect("Should have received device cookie"); 1405 let location = auth_res 1406 .headers() 1407 .get("location") 1408 .unwrap() 1409 .to_str() 1410 .unwrap(); 1411 assert!(location.contains("code="), "First auth should succeed"); 1412 let code = location 1413 .split("code=") 1414 .nth(1) 1415 .unwrap() 1416 .split('&') 1417 .next() 1418 .unwrap(); 1419 let _token_body: Value = http_client 1420 .post(format!("{}/oauth/token", url)) 1421 .form(&[ 1422 ("grant_type", "authorization_code"), 1423 ("code", code), 1424 ("redirect_uri", redirect_uri), 1425 ("code_verifier", &code_verifier), 1426 ("client_id", &client_id), 1427 ]) 1428 .send() 1429 .await 1430 .unwrap() 1431 .json() 1432 .await 1433 .unwrap(); 1434 let db_url = common::get_db_connection_string().await; 1435 let pool = sqlx::postgres::PgPoolOptions::new() 1436 .max_connections(1) 1437 .connect(&db_url) 1438 .await 1439 .expect("Failed to connect to database"); 1440 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 1441 .bind(&user_did) 1442 .execute(&pool) 1443 .await 1444 .expect("Failed to enable 2FA"); 1445 let (code_verifier2, code_challenge2) = generate_pkce(); 1446 let par_body2: Value = http_client 1447 .post(format!("{}/oauth/par", url)) 1448 .form(&[ 1449 ("response_type", "code"), 1450 ("client_id", &client_id), 1451 ("redirect_uri", redirect_uri), 1452 ("code_challenge", &code_challenge2), 1453 ("code_challenge_method", "S256"), 1454 ]) 1455 .send() 1456 .await 1457 .unwrap() 1458 .json() 1459 .await 1460 .unwrap(); 1461 let request_uri2 = par_body2["request_uri"].as_str().unwrap(); 1462 let select_res = auth_client 1463 .post(format!("{}/oauth/authorize/select", url)) 1464 .header("cookie", &device_cookie) 1465 .form(&[("request_uri", request_uri2), ("did", &user_did)]) 1466 .send() 1467 .await 1468 .unwrap(); 1469 assert!( 1470 select_res.status().is_redirection(), 1471 "Account selector should redirect, got status: {}", 1472 select_res.status() 1473 ); 1474 let select_location = select_res 1475 .headers() 1476 .get("location") 1477 .unwrap() 1478 .to_str() 1479 .unwrap(); 1480 assert!( 1481 select_location.contains("/oauth/authorize/2fa"), 1482 "Account selector with 2FA enabled should redirect to 2FA page, got: {}", 1483 select_location 1484 ); 1485 let twofa_code: String = 1486 sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1") 1487 .bind(request_uri2) 1488 .fetch_one(&pool) 1489 .await 1490 .expect("Failed to get 2FA code"); 1491 let twofa_res = auth_client 1492 .post(format!("{}/oauth/authorize/2fa", url)) 1493 .header("cookie", &device_cookie) 1494 .form(&[("request_uri", request_uri2), ("code", &twofa_code)]) 1495 .send() 1496 .await 1497 .unwrap(); 1498 assert!(twofa_res.status().is_redirection()); 1499 let final_location = twofa_res 1500 .headers() 1501 .get("location") 1502 .unwrap() 1503 .to_str() 1504 .unwrap(); 1505 assert!( 1506 final_location.starts_with(redirect_uri) && final_location.contains("code="), 1507 "After 2FA, should redirect to client with code, got: {}", 1508 final_location 1509 ); 1510 let final_code = final_location 1511 .split("code=") 1512 .nth(1) 1513 .unwrap() 1514 .split('&') 1515 .next() 1516 .unwrap(); 1517 let token_res = http_client 1518 .post(format!("{}/oauth/token", url)) 1519 .form(&[ 1520 ("grant_type", "authorization_code"), 1521 ("code", final_code), 1522 ("redirect_uri", redirect_uri), 1523 ("code_verifier", &code_verifier2), 1524 ("client_id", &client_id), 1525 ]) 1526 .send() 1527 .await 1528 .unwrap(); 1529 assert_eq!(token_res.status(), StatusCode::OK); 1530 let final_token: Value = token_res.json().await.unwrap(); 1531 assert_eq!( 1532 final_token["sub"], user_did, 1533 "Token should be for the correct user" 1534 ); 1535}