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