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