this repo has no description
1mod common; 2mod helpers; 3use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 4use common::{base_url, client, get_test_db_pool}; 5use helpers::verify_new_account; 6use reqwest::{StatusCode, redirect}; 7use serde_json::{Value, json}; 8use sha2::{Digest, Sha256}; 9use wiremock::matchers::{method, path}; 10use wiremock::{Mock, MockServer, ResponseTemplate}; 11 12fn no_redirect_client() -> reqwest::Client { 13 reqwest::Client::builder() 14 .redirect(redirect::Policy::none()) 15 .build() 16 .unwrap() 17} 18 19fn generate_pkce() -> (String, String) { 20 let verifier_bytes: [u8; 32] = rand::random(); 21 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes); 22 let mut hasher = Sha256::new(); 23 hasher.update(code_verifier.as_bytes()); 24 let code_challenge = URL_SAFE_NO_PAD.encode(hasher.finalize()); 25 (code_verifier, code_challenge) 26} 27 28async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer { 29 let mock_server = MockServer::start().await; 30 let client_id = mock_server.uri(); 31 let metadata = json!({ 32 "client_id": client_id, 33 "client_name": "Test OAuth Client", 34 "redirect_uris": [redirect_uri], 35 "grant_types": ["authorization_code", "refresh_token"], 36 "response_types": ["code"], 37 "token_endpoint_auth_method": "none", 38 "dpop_bound_access_tokens": false 39 }); 40 Mock::given(method("GET")) 41 .and(path("/")) 42 .respond_with(ResponseTemplate::new(200).set_body_json(metadata)) 43 .mount(&mock_server) 44 .await; 45 mock_server 46} 47 48#[tokio::test] 49async fn test_oauth_metadata_endpoints() { 50 let url = base_url().await; 51 let client = client(); 52 let pr_res = client 53 .get(format!("{}/.well-known/oauth-protected-resource", url)) 54 .send() 55 .await 56 .unwrap(); 57 assert_eq!(pr_res.status(), StatusCode::OK); 58 let pr_body: Value = pr_res.json().await.unwrap(); 59 assert!(pr_body["resource"].is_string()); 60 assert!(pr_body["authorization_servers"].is_array()); 61 assert!( 62 pr_body["bearer_methods_supported"] 63 .as_array() 64 .unwrap() 65 .contains(&json!("header")) 66 ); 67 let as_res = client 68 .get(format!("{}/.well-known/oauth-authorization-server", url)) 69 .send() 70 .await 71 .unwrap(); 72 assert_eq!(as_res.status(), StatusCode::OK); 73 let as_body: Value = as_res.json().await.unwrap(); 74 assert!(as_body["issuer"].is_string()); 75 assert!(as_body["authorization_endpoint"].is_string()); 76 assert!(as_body["token_endpoint"].is_string()); 77 assert!(as_body["jwks_uri"].is_string()); 78 assert!( 79 as_body["response_types_supported"] 80 .as_array() 81 .unwrap() 82 .contains(&json!("code")) 83 ); 84 assert!( 85 as_body["grant_types_supported"] 86 .as_array() 87 .unwrap() 88 .contains(&json!("authorization_code")) 89 ); 90 assert!( 91 as_body["code_challenge_methods_supported"] 92 .as_array() 93 .unwrap() 94 .contains(&json!("S256")) 95 ); 96 assert_eq!( 97 as_body["require_pushed_authorization_requests"], 98 json!(true) 99 ); 100 assert!( 101 as_body["dpop_signing_alg_values_supported"] 102 .as_array() 103 .unwrap() 104 .contains(&json!("ES256")) 105 ); 106 let jwks_res = client 107 .get(format!("{}/oauth/jwks", url)) 108 .send() 109 .await 110 .unwrap(); 111 assert_eq!(jwks_res.status(), StatusCode::OK); 112 let jwks_body: Value = jwks_res.json().await.unwrap(); 113 assert!(jwks_body["keys"].is_array()); 114} 115 116#[tokio::test] 117async fn test_par_and_authorize() { 118 let url = base_url().await; 119 let client = client(); 120 let redirect_uri = "https://example.com/callback"; 121 let mock_client = setup_mock_client_metadata(redirect_uri).await; 122 let client_id = mock_client.uri(); 123 let (_, code_challenge) = generate_pkce(); 124 let par_res = client 125 .post(format!("{}/oauth/par", url)) 126 .form(&[ 127 ("response_type", "code"), 128 ("client_id", &client_id), 129 ("redirect_uri", redirect_uri), 130 ("code_challenge", &code_challenge), 131 ("code_challenge_method", "S256"), 132 ("scope", "atproto"), 133 ("state", "test-state"), 134 ]) 135 .send() 136 .await 137 .unwrap(); 138 assert_eq!(par_res.status(), StatusCode::CREATED, "PAR should succeed"); 139 let par_body: Value = par_res.json().await.unwrap(); 140 assert!(par_body["request_uri"].is_string()); 141 assert!(par_body["expires_in"].is_number()); 142 let request_uri = par_body["request_uri"].as_str().unwrap(); 143 assert!(request_uri.starts_with("urn:ietf:params:oauth:request_uri:")); 144 let auth_res = client 145 .get(format!("{}/oauth/authorize", url)) 146 .header("Accept", "application/json") 147 .query(&[("request_uri", request_uri)]) 148 .send() 149 .await 150 .unwrap(); 151 assert_eq!(auth_res.status(), StatusCode::OK); 152 let auth_body: Value = auth_res.json().await.unwrap(); 153 assert_eq!(auth_body["client_id"], client_id); 154 assert_eq!(auth_body["redirect_uri"], redirect_uri); 155 assert_eq!(auth_body["scope"], "atproto"); 156 let invalid_res = client 157 .get(format!("{}/oauth/authorize", url)) 158 .header("Accept", "application/json") 159 .query(&[( 160 "request_uri", 161 "urn:ietf:params:oauth:request_uri:nonexistent", 162 )]) 163 .send() 164 .await 165 .unwrap(); 166 assert_eq!(invalid_res.status(), StatusCode::BAD_REQUEST); 167 let missing_client = no_redirect_client(); 168 let missing_res = missing_client 169 .get(format!("{}/oauth/authorize", url)) 170 .send() 171 .await 172 .unwrap(); 173 assert!( 174 missing_res.status().is_redirection(), 175 "Should redirect to error page" 176 ); 177 let error_location = missing_res 178 .headers() 179 .get("location") 180 .unwrap() 181 .to_str() 182 .unwrap(); 183 assert!( 184 error_location.contains("oauth/error"), 185 "Should redirect to error page" 186 ); 187} 188 189#[tokio::test] 190async fn test_full_oauth_flow() { 191 let url = base_url().await; 192 let http_client = client(); 193 let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 194 let handle = format!("ot{}", suffix); 195 let email = format!("ot{}@example.com", suffix); 196 let password = "Oauthtest123!"; 197 let create_res = http_client 198 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 199 .json(&json!({ "handle": handle, "email": email, "password": password })) 200 .send() 201 .await 202 .unwrap(); 203 assert_eq!(create_res.status(), StatusCode::OK); 204 let account: Value = create_res.json().await.unwrap(); 205 let user_did = account["did"].as_str().unwrap(); 206 verify_new_account(&http_client, user_did).await; 207 let redirect_uri = "https://example.com/oauth/callback"; 208 let mock_client = setup_mock_client_metadata(redirect_uri).await; 209 let client_id = mock_client.uri(); 210 let (code_verifier, code_challenge) = generate_pkce(); 211 let state = format!("state-{}", suffix); 212 let par_res = http_client 213 .post(format!("{}/oauth/par", url)) 214 .form(&[ 215 ("response_type", "code"), 216 ("client_id", &client_id), 217 ("redirect_uri", redirect_uri), 218 ("code_challenge", &code_challenge), 219 ("code_challenge_method", "S256"), 220 ("scope", "atproto"), 221 ("state", &state), 222 ]) 223 .send() 224 .await 225 .unwrap(); 226 let par_body: Value = par_res.json().await.unwrap(); 227 let request_uri = par_body["request_uri"].as_str().unwrap(); 228 let auth_res = http_client 229 .post(format!("{}/oauth/authorize", url)) 230 .header("Content-Type", "application/json") 231 .header("Accept", "application/json") 232 .json(&json!({"request_uri": request_uri, "username": &handle, "password": password, "remember_device": false})) 233 .send().await.unwrap(); 234 assert_eq!( 235 auth_res.status(), 236 StatusCode::OK, 237 "Expected OK with JSON response" 238 ); 239 let auth_body: Value = auth_res.json().await.unwrap(); 240 let mut location = auth_body["redirect_uri"] 241 .as_str() 242 .expect("Expected redirect_uri in response") 243 .to_string(); 244 if location.contains("/oauth/consent") { 245 let consent_res = http_client 246 .post(format!("{}/oauth/authorize/consent", url)) 247 .header("Content-Type", "application/json") 248 .json(&json!({"request_uri": request_uri, "approved_scopes": ["atproto"], "remember": false})) 249 .send().await.unwrap(); 250 let consent_status = consent_res.status(); 251 let consent_body: Value = consent_res.json().await.unwrap(); 252 assert_eq!( 253 consent_status, 254 StatusCode::OK, 255 "Consent should succeed. Got: {:?}", 256 consent_body 257 ); 258 location = consent_body["redirect_uri"] 259 .as_str() 260 .expect("Expected redirect_uri from consent") 261 .to_string(); 262 } 263 assert!( 264 location.contains("code="), 265 "No code in redirect URI: {}", 266 location 267 ); 268 assert!( 269 location.contains(&format!("state={}", state)) 270 || location.contains(&format!("state%3D{}", state)), 271 "Wrong state in redirect: {}", 272 location 273 ); 274 let code = location 275 .split("code=") 276 .nth(1) 277 .unwrap() 278 .split('&') 279 .next() 280 .unwrap(); 281 let token_res = http_client 282 .post(format!("{}/oauth/token", url)) 283 .form(&[ 284 ("grant_type", "authorization_code"), 285 ("code", code), 286 ("redirect_uri", redirect_uri), 287 ("code_verifier", &code_verifier), 288 ("client_id", &client_id), 289 ]) 290 .send() 291 .await 292 .unwrap(); 293 assert_eq!(token_res.status(), StatusCode::OK, "Token exchange failed"); 294 let token_body: Value = token_res.json().await.unwrap(); 295 assert!(token_body["access_token"].is_string()); 296 assert!(token_body["refresh_token"].is_string()); 297 assert_eq!(token_body["token_type"], "Bearer"); 298 assert!(token_body["expires_in"].is_number()); 299 assert_eq!(token_body["sub"], user_did); 300 let access_token = token_body["access_token"].as_str().unwrap(); 301 let refresh_token = token_body["refresh_token"].as_str().unwrap(); 302 let refresh_res = http_client 303 .post(format!("{}/oauth/token", url)) 304 .form(&[ 305 ("grant_type", "refresh_token"), 306 ("refresh_token", refresh_token), 307 ("client_id", &client_id), 308 ]) 309 .send() 310 .await 311 .unwrap(); 312 assert_eq!(refresh_res.status(), StatusCode::OK); 313 let refresh_body: Value = refresh_res.json().await.unwrap(); 314 assert_ne!(refresh_body["access_token"].as_str().unwrap(), access_token); 315 assert_ne!( 316 refresh_body["refresh_token"].as_str().unwrap(), 317 refresh_token 318 ); 319 let introspect_res = http_client 320 .post(format!("{}/oauth/introspect", url)) 321 .form(&[("token", refresh_body["access_token"].as_str().unwrap())]) 322 .send() 323 .await 324 .unwrap(); 325 assert_eq!(introspect_res.status(), StatusCode::OK); 326 let introspect_body: Value = introspect_res.json().await.unwrap(); 327 assert_eq!(introspect_body["active"], true); 328 let revoke_res = http_client 329 .post(format!("{}/oauth/revoke", url)) 330 .form(&[("token", refresh_body["refresh_token"].as_str().unwrap())]) 331 .send() 332 .await 333 .unwrap(); 334 assert_eq!(revoke_res.status(), StatusCode::OK); 335 let introspect_after = http_client 336 .post(format!("{}/oauth/introspect", url)) 337 .form(&[("token", refresh_body["access_token"].as_str().unwrap())]) 338 .send() 339 .await 340 .unwrap(); 341 let after_body: Value = introspect_after.json().await.unwrap(); 342 assert_eq!( 343 after_body["active"], false, 344 "Revoked token should be inactive" 345 ); 346} 347 348#[tokio::test] 349async fn test_oauth_error_cases() { 350 let url = base_url().await; 351 let http_client = client(); 352 let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 353 let handle = format!("wc{}", suffix); 354 let email = format!("wc{}@example.com", suffix); 355 http_client 356 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 357 .json(&json!({ "handle": handle, "email": email, "password": "Correct123!" })) 358 .send() 359 .await 360 .unwrap(); 361 let redirect_uri = "https://example.com/callback"; 362 let mock_client = setup_mock_client_metadata(redirect_uri).await; 363 let client_id = mock_client.uri(); 364 let (_, code_challenge) = generate_pkce(); 365 let par_body: Value = http_client 366 .post(format!("{}/oauth/par", url)) 367 .form(&[ 368 ("response_type", "code"), 369 ("client_id", &client_id), 370 ("redirect_uri", redirect_uri), 371 ("code_challenge", &code_challenge), 372 ("code_challenge_method", "S256"), 373 ]) 374 .send() 375 .await 376 .unwrap() 377 .json() 378 .await 379 .unwrap(); 380 let request_uri = par_body["request_uri"].as_str().unwrap(); 381 let auth_res = http_client 382 .post(format!("{}/oauth/authorize", url)) 383 .header("Content-Type", "application/json") 384 .header("Accept", "application/json") 385 .json(&json!({"request_uri": request_uri, "username": &handle, "password": "wrong-password", "remember_device": false})) 386 .send().await.unwrap(); 387 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN); 388 let error_body: Value = auth_res.json().await.unwrap(); 389 assert_eq!(error_body["error"], "access_denied"); 390 let unsupported = http_client 391 .post(format!("{}/oauth/token", url)) 392 .form(&[ 393 ("grant_type", "client_credentials"), 394 ("client_id", "https://example.com"), 395 ]) 396 .send() 397 .await 398 .unwrap(); 399 assert_eq!(unsupported.status(), StatusCode::BAD_REQUEST); 400 let body: Value = unsupported.json().await.unwrap(); 401 assert_eq!(body["error"], "unsupported_grant_type"); 402 let invalid_refresh = http_client 403 .post(format!("{}/oauth/token", url)) 404 .form(&[ 405 ("grant_type", "refresh_token"), 406 ("refresh_token", "invalid-token"), 407 ("client_id", "https://example.com"), 408 ]) 409 .send() 410 .await 411 .unwrap(); 412 assert_eq!(invalid_refresh.status(), StatusCode::BAD_REQUEST); 413 let body: Value = invalid_refresh.json().await.unwrap(); 414 assert_eq!(body["error"], "invalid_grant"); 415 let invalid_introspect = http_client 416 .post(format!("{}/oauth/introspect", url)) 417 .form(&[("token", "invalid.token.here")]) 418 .send() 419 .await 420 .unwrap(); 421 assert_eq!(invalid_introspect.status(), StatusCode::OK); 422 let body: Value = invalid_introspect.json().await.unwrap(); 423 assert_eq!(body["active"], false); 424 let expired_res = http_client 425 .get(format!("{}/oauth/authorize", url)) 426 .header("Accept", "application/json") 427 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:expired")]) 428 .send() 429 .await 430 .unwrap(); 431 assert_eq!(expired_res.status(), StatusCode::BAD_REQUEST); 432} 433 434#[tokio::test] 435async fn test_oauth_2fa_flow() { 436 let url = base_url().await; 437 let http_client = client(); 438 let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 439 let handle = format!("ft{}", suffix); 440 let email = format!("ft{}@example.com", suffix); 441 let password = "Twofa123test!"; 442 let create_res = http_client 443 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 444 .json(&json!({ "handle": handle, "email": email, "password": password })) 445 .send() 446 .await 447 .unwrap(); 448 assert_eq!(create_res.status(), StatusCode::OK); 449 let account: Value = create_res.json().await.unwrap(); 450 let user_did = account["did"].as_str().unwrap(); 451 verify_new_account(&http_client, user_did).await; 452 let pool = get_test_db_pool().await; 453 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 454 .bind(user_did) 455 .execute(pool) 456 .await 457 .unwrap(); 458 let redirect_uri = "https://example.com/2fa-callback"; 459 let mock_client = setup_mock_client_metadata(redirect_uri).await; 460 let client_id = mock_client.uri(); 461 let (code_verifier, code_challenge) = generate_pkce(); 462 let par_body: Value = http_client 463 .post(format!("{}/oauth/par", url)) 464 .form(&[ 465 ("response_type", "code"), 466 ("client_id", &client_id), 467 ("redirect_uri", redirect_uri), 468 ("code_challenge", &code_challenge), 469 ("code_challenge_method", "S256"), 470 ]) 471 .send() 472 .await 473 .unwrap() 474 .json() 475 .await 476 .unwrap(); 477 let request_uri = par_body["request_uri"].as_str().unwrap(); 478 let auth_res = http_client 479 .post(format!("{}/oauth/authorize", url)) 480 .header("Content-Type", "application/json") 481 .header("Accept", "application/json") 482 .json(&json!({"request_uri": request_uri, "username": &handle, "password": password, "remember_device": false})) 483 .send().await.unwrap(); 484 assert_eq!( 485 auth_res.status(), 486 StatusCode::OK, 487 "Should return OK with needs_2fa" 488 ); 489 let auth_body: Value = auth_res.json().await.unwrap(); 490 assert!( 491 auth_body["needs_2fa"].as_bool().unwrap_or(false), 492 "Should need 2FA, got: {:?}", 493 auth_body 494 ); 495 let twofa_invalid = http_client 496 .post(format!("{}/oauth/authorize/2fa", url)) 497 .header("Content-Type", "application/json") 498 .json(&json!({"request_uri": request_uri, "code": "000000"})) 499 .send() 500 .await 501 .unwrap(); 502 assert_eq!(twofa_invalid.status(), StatusCode::FORBIDDEN); 503 let body: Value = twofa_invalid.json().await.unwrap(); 504 assert!( 505 body["error_description"] 506 .as_str() 507 .unwrap_or("") 508 .contains("Invalid") 509 || body["error"].as_str().unwrap_or("") == "invalid_code" 510 ); 511 let twofa_code: String = 512 sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1") 513 .bind(request_uri) 514 .fetch_one(pool) 515 .await 516 .unwrap(); 517 let twofa_res = http_client 518 .post(format!("{}/oauth/authorize/2fa", url)) 519 .header("Content-Type", "application/json") 520 .json(&json!({"request_uri": request_uri, "code": &twofa_code})) 521 .send() 522 .await 523 .unwrap(); 524 assert_eq!( 525 twofa_res.status(), 526 StatusCode::OK, 527 "Valid 2FA code should succeed" 528 ); 529 let twofa_body: Value = twofa_res.json().await.unwrap(); 530 let final_location = twofa_body["redirect_uri"].as_str().unwrap(); 531 assert!( 532 final_location.contains("code="), 533 "No code in redirect URI: {}", 534 final_location 535 ); 536 let auth_code = final_location 537 .split("code=") 538 .nth(1) 539 .unwrap() 540 .split('&') 541 .next() 542 .unwrap(); 543 let token_res = http_client 544 .post(format!("{}/oauth/token", url)) 545 .form(&[ 546 ("grant_type", "authorization_code"), 547 ("code", auth_code), 548 ("redirect_uri", redirect_uri), 549 ("code_verifier", &code_verifier), 550 ("client_id", &client_id), 551 ]) 552 .send() 553 .await 554 .unwrap(); 555 assert_eq!(token_res.status(), StatusCode::OK); 556 let token_body: Value = token_res.json().await.unwrap(); 557 assert_eq!(token_body["sub"], user_did); 558} 559 560#[tokio::test] 561async fn test_oauth_2fa_lockout() { 562 let url = base_url().await; 563 let http_client = client(); 564 let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 565 let handle = format!("fl{}", suffix); 566 let email = format!("fl{}@example.com", suffix); 567 let password = "Twofa123test!"; 568 let create_res = http_client 569 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 570 .json(&json!({ "handle": handle, "email": email, "password": password })) 571 .send() 572 .await 573 .unwrap(); 574 let account: Value = create_res.json().await.unwrap(); 575 let user_did = account["did"].as_str().unwrap(); 576 verify_new_account(&http_client, user_did).await; 577 let pool = get_test_db_pool().await; 578 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 579 .bind(user_did) 580 .execute(pool) 581 .await 582 .unwrap(); 583 let redirect_uri = "https://example.com/2fa-lockout-callback"; 584 let mock_client = setup_mock_client_metadata(redirect_uri).await; 585 let client_id = mock_client.uri(); 586 let (_, code_challenge) = generate_pkce(); 587 let par_body: Value = http_client 588 .post(format!("{}/oauth/par", url)) 589 .form(&[ 590 ("response_type", "code"), 591 ("client_id", &client_id), 592 ("redirect_uri", redirect_uri), 593 ("code_challenge", &code_challenge), 594 ("code_challenge_method", "S256"), 595 ]) 596 .send() 597 .await 598 .unwrap() 599 .json() 600 .await 601 .unwrap(); 602 let request_uri = par_body["request_uri"].as_str().unwrap(); 603 let auth_res = http_client 604 .post(format!("{}/oauth/authorize", url)) 605 .header("Content-Type", "application/json") 606 .header("Accept", "application/json") 607 .json(&json!({"request_uri": request_uri, "username": &handle, "password": password, "remember_device": false})) 608 .send().await.unwrap(); 609 assert_eq!( 610 auth_res.status(), 611 StatusCode::OK, 612 "Should return OK with needs_2fa" 613 ); 614 let auth_body: Value = auth_res.json().await.unwrap(); 615 assert!( 616 auth_body["needs_2fa"].as_bool().unwrap_or(false), 617 "Should need 2FA" 618 ); 619 for i in 0..5 { 620 let res = http_client 621 .post(format!("{}/oauth/authorize/2fa", url)) 622 .header("Content-Type", "application/json") 623 .json(&json!({"request_uri": request_uri, "code": "999999"})) 624 .send() 625 .await 626 .unwrap(); 627 if i < 4 { 628 assert_eq!( 629 res.status(), 630 StatusCode::FORBIDDEN, 631 "Attempt {} should return 403", 632 i 633 ); 634 } 635 } 636 let lockout_res = http_client 637 .post(format!("{}/oauth/authorize/2fa", url)) 638 .header("Content-Type", "application/json") 639 .json(&json!({"request_uri": request_uri, "code": "999999"})) 640 .send() 641 .await 642 .unwrap(); 643 let body: Value = lockout_res.json().await.unwrap(); 644 let desc = body["error_description"].as_str().unwrap_or(""); 645 assert!( 646 desc.contains("Too many") || desc.contains("No 2FA") || body["error"] == "invalid_request", 647 "Expected lockout error, got: {:?}", 648 body 649 ); 650} 651 652#[tokio::test] 653async fn test_account_selector_with_2fa() { 654 let url = base_url().await; 655 let http_client = client(); 656 let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 657 let handle = format!("sf{}", suffix); 658 let email = format!("sf{}@example.com", suffix); 659 let password = "Selector2fa123!"; 660 let create_res = http_client 661 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 662 .json(&json!({ "handle": handle, "email": email, "password": password })) 663 .send() 664 .await 665 .unwrap(); 666 let account: Value = create_res.json().await.unwrap(); 667 let user_did = account["did"].as_str().unwrap().to_string(); 668 verify_new_account(&http_client, &user_did).await; 669 let redirect_uri = "https://example.com/selector-2fa-callback"; 670 let mock_client = setup_mock_client_metadata(redirect_uri).await; 671 let client_id = mock_client.uri(); 672 let (code_verifier, code_challenge) = generate_pkce(); 673 let par_body: Value = http_client 674 .post(format!("{}/oauth/par", url)) 675 .form(&[ 676 ("response_type", "code"), 677 ("client_id", &client_id), 678 ("redirect_uri", redirect_uri), 679 ("code_challenge", &code_challenge), 680 ("code_challenge_method", "S256"), 681 ]) 682 .send() 683 .await 684 .unwrap() 685 .json() 686 .await 687 .unwrap(); 688 let request_uri = par_body["request_uri"].as_str().unwrap(); 689 let auth_res = http_client 690 .post(format!("{}/oauth/authorize", url)) 691 .header("Content-Type", "application/json") 692 .header("Accept", "application/json") 693 .json(&json!({"request_uri": request_uri, "username": &handle, "password": password, "remember_device": true})) 694 .send().await.unwrap(); 695 assert_eq!( 696 auth_res.status(), 697 StatusCode::OK, 698 "Expected OK with JSON response" 699 ); 700 let device_cookie = auth_res 701 .headers() 702 .get("set-cookie") 703 .and_then(|v| v.to_str().ok()) 704 .map(|s| s.split(';').next().unwrap_or("").to_string()) 705 .expect("Should have device cookie"); 706 let auth_body: Value = auth_res.json().await.unwrap(); 707 let mut location = auth_body["redirect_uri"] 708 .as_str() 709 .expect("Expected redirect_uri") 710 .to_string(); 711 if location.contains("/oauth/consent") { 712 let consent_res = http_client 713 .post(format!("{}/oauth/authorize/consent", url)) 714 .header("Content-Type", "application/json") 715 .json(&json!({"request_uri": request_uri, "approved_scopes": ["atproto"], "remember": true})) 716 .send().await.unwrap(); 717 assert_eq!( 718 consent_res.status(), 719 StatusCode::OK, 720 "Consent should succeed" 721 ); 722 let consent_body: Value = consent_res.json().await.unwrap(); 723 location = consent_body["redirect_uri"] 724 .as_str() 725 .expect("Expected redirect_uri from consent") 726 .to_string(); 727 } 728 assert!(location.contains("code=")); 729 let code = location 730 .split("code=") 731 .nth(1) 732 .unwrap() 733 .split('&') 734 .next() 735 .unwrap(); 736 let _ = http_client 737 .post(format!("{}/oauth/token", url)) 738 .form(&[ 739 ("grant_type", "authorization_code"), 740 ("code", code), 741 ("redirect_uri", redirect_uri), 742 ("code_verifier", &code_verifier), 743 ("client_id", &client_id), 744 ]) 745 .send() 746 .await 747 .unwrap() 748 .json::<Value>() 749 .await 750 .unwrap(); 751 let pool = get_test_db_pool().await; 752 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 753 .bind(&user_did) 754 .execute(pool) 755 .await 756 .unwrap(); 757 let (code_verifier2, code_challenge2) = generate_pkce(); 758 let par_body2: Value = http_client 759 .post(format!("{}/oauth/par", url)) 760 .form(&[ 761 ("response_type", "code"), 762 ("client_id", &client_id), 763 ("redirect_uri", redirect_uri), 764 ("code_challenge", &code_challenge2), 765 ("code_challenge_method", "S256"), 766 ]) 767 .send() 768 .await 769 .unwrap() 770 .json() 771 .await 772 .unwrap(); 773 let request_uri2 = par_body2["request_uri"].as_str().unwrap(); 774 let select_res = http_client 775 .post(format!("{}/oauth/authorize/select", url)) 776 .header("cookie", &device_cookie) 777 .header("Content-Type", "application/json") 778 .json(&json!({"request_uri": request_uri2, "did": &user_did})) 779 .send() 780 .await 781 .unwrap(); 782 assert_eq!( 783 select_res.status(), 784 StatusCode::OK, 785 "Select should return OK with JSON" 786 ); 787 let select_body: Value = select_res.json().await.unwrap(); 788 assert!( 789 select_body["needs_2fa"].as_bool().unwrap_or(false), 790 "Should need 2FA" 791 ); 792 let twofa_code: String = 793 sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1") 794 .bind(request_uri2) 795 .fetch_one(pool) 796 .await 797 .unwrap(); 798 let twofa_res = http_client 799 .post(format!("{}/oauth/authorize/2fa", url)) 800 .header("cookie", &device_cookie) 801 .header("Content-Type", "application/json") 802 .json(&json!({"request_uri": request_uri2, "code": &twofa_code})) 803 .send() 804 .await 805 .unwrap(); 806 assert_eq!( 807 twofa_res.status(), 808 StatusCode::OK, 809 "Valid 2FA should succeed" 810 ); 811 let twofa_body: Value = twofa_res.json().await.unwrap(); 812 let final_location = twofa_body["redirect_uri"].as_str().unwrap(); 813 assert!( 814 final_location.contains("code="), 815 "No code in redirect URI: {}", 816 final_location 817 ); 818 let final_code = final_location 819 .split("code=") 820 .nth(1) 821 .unwrap() 822 .split('&') 823 .next() 824 .unwrap(); 825 let token_res = http_client 826 .post(format!("{}/oauth/token", url)) 827 .form(&[ 828 ("grant_type", "authorization_code"), 829 ("code", final_code), 830 ("redirect_uri", redirect_uri), 831 ("code_verifier", &code_verifier2), 832 ("client_id", &client_id), 833 ]) 834 .send() 835 .await 836 .unwrap(); 837 assert_eq!(token_res.status(), StatusCode::OK); 838 let final_token: Value = token_res.json().await.unwrap(); 839 assert_eq!(final_token["sub"], user_did); 840} 841 842#[tokio::test] 843async fn test_oauth_state_encoding() { 844 let url = base_url().await; 845 let http_client = client(); 846 let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 847 let handle = format!("ss{}", suffix); 848 let email = format!("ss{}@example.com", suffix); 849 let password = "State123special!"; 850 let create_res = http_client 851 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 852 .json(&json!({ "handle": handle, "email": email, "password": password })) 853 .send() 854 .await 855 .unwrap(); 856 let account: Value = create_res.json().await.unwrap(); 857 verify_new_account(&http_client, account["did"].as_str().unwrap()).await; 858 let redirect_uri = "https://example.com/state-special-callback"; 859 let mock_client = setup_mock_client_metadata(redirect_uri).await; 860 let client_id = mock_client.uri(); 861 let (_, code_challenge) = generate_pkce(); 862 let special_state = "state=with&special=chars&plus+more"; 863 let par_body: Value = http_client 864 .post(format!("{}/oauth/par", url)) 865 .form(&[ 866 ("response_type", "code"), 867 ("client_id", &client_id), 868 ("redirect_uri", redirect_uri), 869 ("code_challenge", &code_challenge), 870 ("code_challenge_method", "S256"), 871 ("state", special_state), 872 ]) 873 .send() 874 .await 875 .unwrap() 876 .json() 877 .await 878 .unwrap(); 879 let request_uri = par_body["request_uri"].as_str().unwrap(); 880 let auth_res = http_client 881 .post(format!("{}/oauth/authorize", url)) 882 .header("Content-Type", "application/json") 883 .header("Accept", "application/json") 884 .json(&json!({"request_uri": request_uri, "username": &handle, "password": password, "remember_device": false})) 885 .send().await.unwrap(); 886 assert_eq!( 887 auth_res.status(), 888 StatusCode::OK, 889 "Expected OK with JSON response" 890 ); 891 let auth_body: Value = auth_res.json().await.unwrap(); 892 let mut location = auth_body["redirect_uri"] 893 .as_str() 894 .expect("Expected redirect_uri") 895 .to_string(); 896 if location.contains("/oauth/consent") { 897 let consent_res = http_client 898 .post(format!("{}/oauth/authorize/consent", url)) 899 .header("Content-Type", "application/json") 900 .json(&json!({"request_uri": request_uri, "approved_scopes": ["atproto"], "remember": false})) 901 .send().await.unwrap(); 902 assert_eq!( 903 consent_res.status(), 904 StatusCode::OK, 905 "Consent should succeed" 906 ); 907 let consent_body: Value = consent_res.json().await.unwrap(); 908 location = consent_body["redirect_uri"] 909 .as_str() 910 .expect("Expected redirect_uri from consent") 911 .to_string(); 912 } 913 assert!(location.contains("state=")); 914 let encoded_state = urlencoding::encode(special_state); 915 assert!( 916 location.contains(&format!("state={}", encoded_state)), 917 "State should be URL-encoded. Got: {}", 918 location 919 ); 920} 921 922async fn get_oauth_token_with_scope(scope: &str) -> (String, String, String) { 923 let url = base_url().await; 924 let http_client = client(); 925 let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 926 let handle = format!("st{}", suffix); 927 let email = format!("st{}@example.com", suffix); 928 let password = "Scopetest123!"; 929 let create_res = http_client 930 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 931 .json(&json!({ "handle": handle, "email": email, "password": password })) 932 .send() 933 .await 934 .unwrap(); 935 assert_eq!(create_res.status(), StatusCode::OK); 936 let account: Value = create_res.json().await.unwrap(); 937 let user_did = account["did"].as_str().unwrap().to_string(); 938 verify_new_account(&http_client, &user_did).await; 939 let redirect_uri = "https://example.com/scope-callback"; 940 let mock_client = setup_mock_client_metadata(redirect_uri).await; 941 let client_id = mock_client.uri(); 942 let (code_verifier, code_challenge) = generate_pkce(); 943 let par_res = http_client 944 .post(format!("{}/oauth/par", url)) 945 .form(&[ 946 ("response_type", "code"), 947 ("client_id", &client_id), 948 ("redirect_uri", redirect_uri), 949 ("code_challenge", &code_challenge), 950 ("code_challenge_method", "S256"), 951 ("scope", scope), 952 ("state", "test"), 953 ]) 954 .send() 955 .await 956 .unwrap(); 957 assert_eq!( 958 par_res.status(), 959 StatusCode::CREATED, 960 "PAR should succeed for scope: {}", 961 scope 962 ); 963 let par_body: Value = par_res.json().await.unwrap(); 964 let request_uri = par_body["request_uri"].as_str().unwrap(); 965 let auth_res = http_client 966 .post(format!("{}/oauth/authorize", url)) 967 .header("Content-Type", "application/json") 968 .header("Accept", "application/json") 969 .json(&json!({"request_uri": request_uri, "username": &handle, "password": password, "remember_device": false})) 970 .send().await.unwrap(); 971 assert_eq!(auth_res.status(), StatusCode::OK); 972 let auth_body: Value = auth_res.json().await.unwrap(); 973 let mut location = auth_body["redirect_uri"] 974 .as_str() 975 .expect("Expected redirect_uri") 976 .to_string(); 977 if location.contains("/oauth/consent") { 978 let approved_scopes: Vec<&str> = scope.split_whitespace().collect(); 979 let consent_res = http_client 980 .post(format!("{}/oauth/authorize/consent", url)) 981 .header("Content-Type", "application/json") 982 .json(&json!({"request_uri": request_uri, "approved_scopes": approved_scopes, "remember": false})) 983 .send().await.unwrap(); 984 let consent_status = consent_res.status(); 985 let consent_body: Value = consent_res.json().await.unwrap(); 986 assert_eq!( 987 consent_status, 988 StatusCode::OK, 989 "Consent should succeed. Scope: {}, Body: {:?}", 990 scope, 991 consent_body 992 ); 993 location = consent_body["redirect_uri"] 994 .as_str() 995 .expect("Expected redirect_uri from consent") 996 .to_string(); 997 } 998 let code = location 999 .split("code=") 1000 .nth(1) 1001 .unwrap() 1002 .split('&') 1003 .next() 1004 .unwrap(); 1005 let token_res = http_client 1006 .post(format!("{}/oauth/token", url)) 1007 .form(&[ 1008 ("grant_type", "authorization_code"), 1009 ("code", code), 1010 ("redirect_uri", redirect_uri), 1011 ("code_verifier", &code_verifier), 1012 ("client_id", &client_id), 1013 ]) 1014 .send() 1015 .await 1016 .unwrap(); 1017 assert_eq!(token_res.status(), StatusCode::OK, "Token exchange failed"); 1018 let token_body: Value = token_res.json().await.unwrap(); 1019 let access_token = token_body["access_token"].as_str().unwrap().to_string(); 1020 (access_token, user_did, handle) 1021} 1022 1023#[tokio::test] 1024async fn test_granular_scope_repo_create_only() { 1025 let url = base_url().await; 1026 let http_client = client(); 1027 let (token, did, _) = 1028 get_oauth_token_with_scope("repo:app.bsky.feed.post?action=create blob:*/*").await; 1029 let now = chrono::Utc::now().to_rfc3339(); 1030 let create_res = http_client 1031 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 1032 .bearer_auth(&token) 1033 .json(&json!({ 1034 "repo": &did, 1035 "collection": "app.bsky.feed.post", 1036 "record": { "$type": "app.bsky.feed.post", "text": "test post", "createdAt": now } 1037 })) 1038 .send() 1039 .await 1040 .unwrap(); 1041 assert_eq!( 1042 create_res.status(), 1043 StatusCode::OK, 1044 "Should allow creating posts with repo:app.bsky.feed.post?action=create" 1045 ); 1046 let body: Value = create_res.json().await.unwrap(); 1047 let uri = body["uri"].as_str().expect("Should have uri"); 1048 let rkey = uri.split('/').next_back().unwrap(); 1049 let delete_res = http_client 1050 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", url)) 1051 .bearer_auth(&token) 1052 .json(&json!({ "repo": &did, "collection": "app.bsky.feed.post", "rkey": rkey })) 1053 .send() 1054 .await 1055 .unwrap(); 1056 assert_eq!( 1057 delete_res.status(), 1058 StatusCode::FORBIDDEN, 1059 "Should NOT allow deleting with create-only scope" 1060 ); 1061 let like_res = http_client 1062 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 1063 .bearer_auth(&token) 1064 .json(&json!({ 1065 "repo": &did, 1066 "collection": "app.bsky.feed.like", 1067 "record": { "$type": "app.bsky.feed.like", "subject": { "uri": uri, "cid": body["cid"] }, "createdAt": now } 1068 })) 1069 .send().await.unwrap(); 1070 assert_eq!( 1071 like_res.status(), 1072 StatusCode::FORBIDDEN, 1073 "Should NOT allow creating likes (wrong collection)" 1074 ); 1075} 1076 1077#[tokio::test] 1078async fn test_granular_scope_wildcard_collection() { 1079 let url = base_url().await; 1080 let http_client = client(); 1081 let (token, did, _) = get_oauth_token_with_scope( 1082 "repo:app.bsky.*?action=create&action=update&action=delete blob:*/*", 1083 ) 1084 .await; 1085 let now = chrono::Utc::now().to_rfc3339(); 1086 let post_res = http_client 1087 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 1088 .bearer_auth(&token) 1089 .json(&json!({ 1090 "repo": &did, 1091 "collection": "app.bsky.feed.post", 1092 "record": { "$type": "app.bsky.feed.post", "text": "wildcard test", "createdAt": now } 1093 })) 1094 .send() 1095 .await 1096 .unwrap(); 1097 assert_eq!( 1098 post_res.status(), 1099 StatusCode::OK, 1100 "Should allow app.bsky.feed.post with app.bsky.* scope" 1101 ); 1102 let body: Value = post_res.json().await.unwrap(); 1103 let uri = body["uri"].as_str().unwrap(); 1104 let rkey = uri.split('/').next_back().unwrap(); 1105 let delete_res = http_client 1106 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", url)) 1107 .bearer_auth(&token) 1108 .json(&json!({ "repo": &did, "collection": "app.bsky.feed.post", "rkey": rkey })) 1109 .send() 1110 .await 1111 .unwrap(); 1112 assert_eq!( 1113 delete_res.status(), 1114 StatusCode::OK, 1115 "Should allow delete with action=delete" 1116 ); 1117 let other_res = http_client 1118 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 1119 .bearer_auth(&token) 1120 .json(&json!({ 1121 "repo": &did, 1122 "collection": "com.example.record", 1123 "record": { "$type": "com.example.record", "data": "test", "createdAt": now } 1124 })) 1125 .send() 1126 .await 1127 .unwrap(); 1128 assert_eq!( 1129 other_res.status(), 1130 StatusCode::FORBIDDEN, 1131 "Should NOT allow com.example.* with app.bsky.* scope" 1132 ); 1133} 1134 1135#[tokio::test] 1136async fn test_granular_scope_email_read() { 1137 let url = base_url().await; 1138 let http_client = client(); 1139 let (token, did, _) = get_oauth_token_with_scope("account:email?action=read").await; 1140 let session_res = http_client 1141 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1142 .bearer_auth(&token) 1143 .send() 1144 .await 1145 .unwrap(); 1146 assert_eq!(session_res.status(), StatusCode::OK); 1147 let body: Value = session_res.json().await.unwrap(); 1148 assert_eq!(body["did"], did); 1149 assert!( 1150 body["email"].is_string(), 1151 "Email should be visible with account:email?action=read. Got: {:?}", 1152 body 1153 ); 1154} 1155 1156#[tokio::test] 1157async fn test_granular_scope_no_email_access() { 1158 let url = base_url().await; 1159 let http_client = client(); 1160 let (token, did, _) = get_oauth_token_with_scope("repo:*?action=create blob:*/*").await; 1161 let session_res = http_client 1162 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1163 .bearer_auth(&token) 1164 .send() 1165 .await 1166 .unwrap(); 1167 assert_eq!(session_res.status(), StatusCode::OK); 1168 let body: Value = session_res.json().await.unwrap(); 1169 assert_eq!(body["did"], did); 1170 assert!( 1171 body["email"].is_null() || body.get("email").is_none(), 1172 "Email should be hidden without account:email scope. Got: {:?}", 1173 body["email"] 1174 ); 1175} 1176 1177#[tokio::test] 1178async fn test_granular_scope_rpc_specific_method() { 1179 let url = base_url().await; 1180 let http_client = client(); 1181 let (token, _, _) = get_oauth_token_with_scope("rpc:app.bsky.feed.getTimeline?aud=*").await; 1182 let allowed_res = http_client 1183 .get(format!("{}/xrpc/com.atproto.server.getServiceAuth", url)) 1184 .bearer_auth(&token) 1185 .query(&[ 1186 ("aud", "did:web:api.bsky.app"), 1187 ("lxm", "app.bsky.feed.getTimeline"), 1188 ]) 1189 .send() 1190 .await 1191 .unwrap(); 1192 assert_eq!( 1193 allowed_res.status(), 1194 StatusCode::OK, 1195 "Should allow getServiceAuth for app.bsky.feed.getTimeline" 1196 ); 1197 let body: Value = allowed_res.json().await.unwrap(); 1198 assert!(body["token"].is_string(), "Should return service token"); 1199 let blocked_res = http_client 1200 .get(format!("{}/xrpc/com.atproto.server.getServiceAuth", url)) 1201 .bearer_auth(&token) 1202 .query(&[ 1203 ("aud", "did:web:api.bsky.app"), 1204 ("lxm", "app.bsky.feed.getAuthorFeed"), 1205 ]) 1206 .send() 1207 .await 1208 .unwrap(); 1209 assert_eq!( 1210 blocked_res.status(), 1211 StatusCode::FORBIDDEN, 1212 "Should NOT allow getServiceAuth for app.bsky.feed.getAuthorFeed" 1213 ); 1214 let blocked_body: Value = blocked_res.json().await.unwrap(); 1215 assert!( 1216 blocked_body["error"] 1217 .as_str() 1218 .unwrap_or("") 1219 .contains("Scope") 1220 || blocked_body["message"] 1221 .as_str() 1222 .unwrap_or("") 1223 .contains("scope"), 1224 "Should mention scope restriction: {:?}", 1225 blocked_body 1226 ); 1227 let no_lxm_res = http_client 1228 .get(format!("{}/xrpc/com.atproto.server.getServiceAuth", url)) 1229 .bearer_auth(&token) 1230 .query(&[("aud", "did:web:api.bsky.app")]) 1231 .send() 1232 .await 1233 .unwrap(); 1234 assert_eq!( 1235 no_lxm_res.status(), 1236 StatusCode::BAD_REQUEST, 1237 "Should require lxm parameter for granular scopes" 1238 ); 1239}