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