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}; 6use helpers::verify_new_account; 7use reqwest::{redirect, StatusCode}; 8use serde_json::{json, Value}; 9use sha2::{Digest, Sha256}; 10use wiremock::{Mock, MockServer, ResponseTemplate}; 11use wiremock::matchers::{method, path}; 12fn generate_pkce() -> (String, String) { 13 let verifier_bytes: [u8; 32] = rand::random(); 14 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes); 15 let mut hasher = Sha256::new(); 16 hasher.update(code_verifier.as_bytes()); 17 let hash = hasher.finalize(); 18 let code_challenge = URL_SAFE_NO_PAD.encode(&hash); 19 (code_verifier, code_challenge) 20} 21fn no_redirect_client() -> reqwest::Client { 22 reqwest::Client::builder() 23 .redirect(redirect::Policy::none()) 24 .build() 25 .unwrap() 26} 27async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer { 28 let mock_server = MockServer::start().await; 29 let client_id = mock_server.uri(); 30 let metadata = json!({ 31 "client_id": client_id, 32 "client_name": "Test OAuth Client", 33 "redirect_uris": [redirect_uri], 34 "grant_types": ["authorization_code", "refresh_token"], 35 "response_types": ["code"], 36 "token_endpoint_auth_method": "none", 37 "dpop_bound_access_tokens": false 38 }); 39 Mock::given(method("GET")) 40 .and(path("/")) 41 .respond_with(ResponseTemplate::new(200).set_body_json(metadata)) 42 .mount(&mock_server) 43 .await; 44 mock_server 45} 46struct OAuthSession { 47 access_token: String, 48 refresh_token: String, 49 did: String, 50 client_id: String, 51} 52async fn create_user_and_oauth_session(handle_prefix: &str, redirect_uri: &str) -> (OAuthSession, MockServer) { 53 let url = base_url().await; 54 let http_client = client(); 55 let ts = Utc::now().timestamp_millis(); 56 let handle = format!("{}-{}", handle_prefix, ts); 57 let email = format!("{}-{}@example.com", handle_prefix, ts); 58 let password = format!("{}-password", handle_prefix); 59 let create_res = http_client 60 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 61 .json(&json!({ 62 "handle": handle, 63 "email": email, 64 "password": password 65 })) 66 .send() 67 .await 68 .expect("Account creation failed"); 69 assert_eq!(create_res.status(), StatusCode::OK); 70 let account: Value = create_res.json().await.unwrap(); 71 let user_did = account["did"].as_str().unwrap().to_string(); 72 let _ = verify_new_account(&http_client, &user_did).await; 73 let mock_client = setup_mock_client_metadata(redirect_uri).await; 74 let client_id = mock_client.uri(); 75 let (code_verifier, code_challenge) = generate_pkce(); 76 let par_res = http_client 77 .post(format!("{}/oauth/par", url)) 78 .form(&[ 79 ("response_type", "code"), 80 ("client_id", &client_id), 81 ("redirect_uri", redirect_uri), 82 ("code_challenge", &code_challenge), 83 ("code_challenge_method", "S256"), 84 ("scope", "atproto"), 85 ]) 86 .send() 87 .await 88 .expect("PAR failed"); 89 assert_eq!(par_res.status(), StatusCode::OK); 90 let par_body: Value = par_res.json().await.unwrap(); 91 let request_uri = par_body["request_uri"].as_str().unwrap(); 92 let auth_client = no_redirect_client(); 93 let auth_res = auth_client 94 .post(format!("{}/oauth/authorize", url)) 95 .form(&[ 96 ("request_uri", request_uri), 97 ("username", &handle), 98 ("password", &password), 99 ("remember_device", "false"), 100 ]) 101 .send() 102 .await 103 .expect("Authorize failed"); 104 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 105 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 106 let token_res = http_client 107 .post(format!("{}/oauth/token", url)) 108 .form(&[ 109 ("grant_type", "authorization_code"), 110 ("code", code), 111 ("redirect_uri", redirect_uri), 112 ("code_verifier", &code_verifier), 113 ("client_id", &client_id), 114 ]) 115 .send() 116 .await 117 .expect("Token request failed"); 118 assert_eq!(token_res.status(), StatusCode::OK); 119 let token_body: Value = token_res.json().await.unwrap(); 120 let session = OAuthSession { 121 access_token: token_body["access_token"].as_str().unwrap().to_string(), 122 refresh_token: token_body["refresh_token"].as_str().unwrap().to_string(), 123 did: user_did, 124 client_id, 125 }; 126 (session, mock_client) 127} 128#[tokio::test] 129async fn test_oauth_token_can_create_and_read_records() { 130 let url = base_url().await; 131 let http_client = client(); 132 let (session, _mock) = create_user_and_oauth_session( 133 "oauth-records", 134 "https://example.com/callback" 135 ).await; 136 let collection = "app.bsky.feed.post"; 137 let post_text = "Hello from OAuth! This post was created with an OAuth access token."; 138 let create_res = http_client 139 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 140 .bearer_auth(&session.access_token) 141 .json(&json!({ 142 "repo": session.did, 143 "collection": collection, 144 "record": { 145 "$type": collection, 146 "text": post_text, 147 "createdAt": Utc::now().to_rfc3339() 148 } 149 })) 150 .send() 151 .await 152 .expect("createRecord failed"); 153 assert_eq!(create_res.status(), StatusCode::OK, "Should create record with OAuth token"); 154 let create_body: Value = create_res.json().await.unwrap(); 155 let uri = create_body["uri"].as_str().unwrap(); 156 let rkey = uri.split('/').last().unwrap(); 157 let get_res = http_client 158 .get(format!("{}/xrpc/com.atproto.repo.getRecord", url)) 159 .bearer_auth(&session.access_token) 160 .query(&[ 161 ("repo", session.did.as_str()), 162 ("collection", collection), 163 ("rkey", rkey), 164 ]) 165 .send() 166 .await 167 .expect("getRecord failed"); 168 assert_eq!(get_res.status(), StatusCode::OK, "Should read record with OAuth token"); 169 let get_body: Value = get_res.json().await.unwrap(); 170 assert_eq!(get_body["value"]["text"], post_text); 171} 172#[tokio::test] 173async fn test_oauth_token_can_upload_blob() { 174 let url = base_url().await; 175 let http_client = client(); 176 let (session, _mock) = create_user_and_oauth_session( 177 "oauth-blob", 178 "https://example.com/callback" 179 ).await; 180 let blob_data = b"This is test blob data uploaded via OAuth"; 181 let upload_res = http_client 182 .post(format!("{}/xrpc/com.atproto.repo.uploadBlob", url)) 183 .bearer_auth(&session.access_token) 184 .header("Content-Type", "text/plain") 185 .body(blob_data.to_vec()) 186 .send() 187 .await 188 .expect("uploadBlob failed"); 189 assert_eq!(upload_res.status(), StatusCode::OK, "Should upload blob with OAuth token"); 190 let upload_body: Value = upload_res.json().await.unwrap(); 191 assert!(upload_body["blob"]["ref"]["$link"].is_string()); 192 assert_eq!(upload_body["blob"]["mimeType"], "text/plain"); 193} 194#[tokio::test] 195async fn test_oauth_token_can_describe_repo() { 196 let url = base_url().await; 197 let http_client = client(); 198 let (session, _mock) = create_user_and_oauth_session( 199 "oauth-describe", 200 "https://example.com/callback" 201 ).await; 202 let describe_res = http_client 203 .get(format!("{}/xrpc/com.atproto.repo.describeRepo", url)) 204 .bearer_auth(&session.access_token) 205 .query(&[("repo", session.did.as_str())]) 206 .send() 207 .await 208 .expect("describeRepo failed"); 209 assert_eq!(describe_res.status(), StatusCode::OK, "Should describe repo with OAuth token"); 210 let describe_body: Value = describe_res.json().await.unwrap(); 211 assert_eq!(describe_body["did"], session.did); 212 assert!(describe_body["handle"].is_string()); 213} 214#[tokio::test] 215async fn test_oauth_full_post_lifecycle_create_edit_delete() { 216 let url = base_url().await; 217 let http_client = client(); 218 let (session, _mock) = create_user_and_oauth_session( 219 "oauth-lifecycle", 220 "https://example.com/callback" 221 ).await; 222 let collection = "app.bsky.feed.post"; 223 let original_text = "Original post content"; 224 let create_res = http_client 225 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 226 .bearer_auth(&session.access_token) 227 .json(&json!({ 228 "repo": session.did, 229 "collection": collection, 230 "record": { 231 "$type": collection, 232 "text": original_text, 233 "createdAt": Utc::now().to_rfc3339() 234 } 235 })) 236 .send() 237 .await 238 .unwrap(); 239 assert_eq!(create_res.status(), StatusCode::OK); 240 let create_body: Value = create_res.json().await.unwrap(); 241 let uri = create_body["uri"].as_str().unwrap(); 242 let rkey = uri.split('/').last().unwrap(); 243 let updated_text = "Updated post content via OAuth putRecord"; 244 let put_res = http_client 245 .post(format!("{}/xrpc/com.atproto.repo.putRecord", url)) 246 .bearer_auth(&session.access_token) 247 .json(&json!({ 248 "repo": session.did, 249 "collection": collection, 250 "rkey": rkey, 251 "record": { 252 "$type": collection, 253 "text": updated_text, 254 "createdAt": Utc::now().to_rfc3339() 255 } 256 })) 257 .send() 258 .await 259 .unwrap(); 260 assert_eq!(put_res.status(), StatusCode::OK, "Should update record with OAuth token"); 261 let get_res = http_client 262 .get(format!("{}/xrpc/com.atproto.repo.getRecord", url)) 263 .bearer_auth(&session.access_token) 264 .query(&[ 265 ("repo", session.did.as_str()), 266 ("collection", collection), 267 ("rkey", rkey), 268 ]) 269 .send() 270 .await 271 .unwrap(); 272 let get_body: Value = get_res.json().await.unwrap(); 273 assert_eq!(get_body["value"]["text"], updated_text, "Record should have updated text"); 274 let delete_res = http_client 275 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", url)) 276 .bearer_auth(&session.access_token) 277 .json(&json!({ 278 "repo": session.did, 279 "collection": collection, 280 "rkey": rkey 281 })) 282 .send() 283 .await 284 .unwrap(); 285 assert_eq!(delete_res.status(), StatusCode::OK, "Should delete record with OAuth token"); 286 let get_deleted_res = http_client 287 .get(format!("{}/xrpc/com.atproto.repo.getRecord", url)) 288 .bearer_auth(&session.access_token) 289 .query(&[ 290 ("repo", session.did.as_str()), 291 ("collection", collection), 292 ("rkey", rkey), 293 ]) 294 .send() 295 .await 296 .unwrap(); 297 assert!( 298 get_deleted_res.status() == StatusCode::BAD_REQUEST || get_deleted_res.status() == StatusCode::NOT_FOUND, 299 "Deleted record should not be found, got {}", 300 get_deleted_res.status() 301 ); 302} 303#[tokio::test] 304async fn test_oauth_batch_operations_apply_writes() { 305 let url = base_url().await; 306 let http_client = client(); 307 let (session, _mock) = create_user_and_oauth_session( 308 "oauth-batch", 309 "https://example.com/callback" 310 ).await; 311 let collection = "app.bsky.feed.post"; 312 let now = Utc::now().to_rfc3339(); 313 let apply_res = http_client 314 .post(format!("{}/xrpc/com.atproto.repo.applyWrites", url)) 315 .bearer_auth(&session.access_token) 316 .json(&json!({ 317 "repo": session.did, 318 "writes": [ 319 { 320 "$type": "com.atproto.repo.applyWrites#create", 321 "collection": collection, 322 "rkey": "batch1", 323 "value": { 324 "$type": collection, 325 "text": "Batch post 1", 326 "createdAt": now 327 } 328 }, 329 { 330 "$type": "com.atproto.repo.applyWrites#create", 331 "collection": collection, 332 "rkey": "batch2", 333 "value": { 334 "$type": collection, 335 "text": "Batch post 2", 336 "createdAt": now 337 } 338 }, 339 { 340 "$type": "com.atproto.repo.applyWrites#create", 341 "collection": collection, 342 "rkey": "batch3", 343 "value": { 344 "$type": collection, 345 "text": "Batch post 3", 346 "createdAt": now 347 } 348 } 349 ] 350 })) 351 .send() 352 .await 353 .unwrap(); 354 assert_eq!(apply_res.status(), StatusCode::OK, "Should apply batch writes with OAuth token"); 355 let list_res = http_client 356 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 357 .bearer_auth(&session.access_token) 358 .query(&[ 359 ("repo", session.did.as_str()), 360 ("collection", collection), 361 ]) 362 .send() 363 .await 364 .unwrap(); 365 assert_eq!(list_res.status(), StatusCode::OK); 366 let list_body: Value = list_res.json().await.unwrap(); 367 let records = list_body["records"].as_array().unwrap(); 368 assert!(records.len() >= 3, "Should have at least 3 records from batch"); 369} 370#[tokio::test] 371async fn test_oauth_token_refresh_maintains_access() { 372 let url = base_url().await; 373 let http_client = client(); 374 let (session, _mock) = create_user_and_oauth_session( 375 "oauth-refresh-access", 376 "https://example.com/callback" 377 ).await; 378 let collection = "app.bsky.feed.post"; 379 let create_res = http_client 380 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 381 .bearer_auth(&session.access_token) 382 .json(&json!({ 383 "repo": session.did, 384 "collection": collection, 385 "record": { 386 "$type": collection, 387 "text": "Post before refresh", 388 "createdAt": Utc::now().to_rfc3339() 389 } 390 })) 391 .send() 392 .await 393 .unwrap(); 394 assert_eq!(create_res.status(), StatusCode::OK, "Original token should work"); 395 let refresh_res = http_client 396 .post(format!("{}/oauth/token", url)) 397 .form(&[ 398 ("grant_type", "refresh_token"), 399 ("refresh_token", &session.refresh_token), 400 ("client_id", &session.client_id), 401 ]) 402 .send() 403 .await 404 .unwrap(); 405 assert_eq!(refresh_res.status(), StatusCode::OK); 406 let refresh_body: Value = refresh_res.json().await.unwrap(); 407 let new_access_token = refresh_body["access_token"].as_str().unwrap(); 408 assert_ne!(new_access_token, session.access_token, "New token should be different"); 409 let create_res2 = http_client 410 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 411 .bearer_auth(new_access_token) 412 .json(&json!({ 413 "repo": session.did, 414 "collection": collection, 415 "record": { 416 "$type": collection, 417 "text": "Post after refresh with new token", 418 "createdAt": Utc::now().to_rfc3339() 419 } 420 })) 421 .send() 422 .await 423 .unwrap(); 424 assert_eq!(create_res2.status(), StatusCode::OK, "New token should work for creating records"); 425 let list_res = http_client 426 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 427 .bearer_auth(new_access_token) 428 .query(&[ 429 ("repo", session.did.as_str()), 430 ("collection", collection), 431 ]) 432 .send() 433 .await 434 .unwrap(); 435 assert_eq!(list_res.status(), StatusCode::OK, "New token should work for listing records"); 436 let list_body: Value = list_res.json().await.unwrap(); 437 let records = list_body["records"].as_array().unwrap(); 438 assert_eq!(records.len(), 2, "Should have both posts"); 439} 440#[tokio::test] 441async fn test_oauth_revoked_token_cannot_access_resources() { 442 let url = base_url().await; 443 let http_client = client(); 444 let (session, _mock) = create_user_and_oauth_session( 445 "oauth-revoke-access", 446 "https://example.com/callback" 447 ).await; 448 let collection = "app.bsky.feed.post"; 449 let create_res = http_client 450 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 451 .bearer_auth(&session.access_token) 452 .json(&json!({ 453 "repo": session.did, 454 "collection": collection, 455 "record": { 456 "$type": collection, 457 "text": "Post before revocation", 458 "createdAt": Utc::now().to_rfc3339() 459 } 460 })) 461 .send() 462 .await 463 .unwrap(); 464 assert_eq!(create_res.status(), StatusCode::OK, "Token should work before revocation"); 465 let revoke_res = http_client 466 .post(format!("{}/oauth/revoke", url)) 467 .form(&[("token", session.refresh_token.as_str())]) 468 .send() 469 .await 470 .unwrap(); 471 assert_eq!(revoke_res.status(), StatusCode::OK, "Revocation should succeed"); 472 let refresh_res = http_client 473 .post(format!("{}/oauth/token", url)) 474 .form(&[ 475 ("grant_type", "refresh_token"), 476 ("refresh_token", &session.refresh_token), 477 ("client_id", &session.client_id), 478 ]) 479 .send() 480 .await 481 .unwrap(); 482 assert_eq!(refresh_res.status(), StatusCode::BAD_REQUEST, "Revoked refresh token should not work"); 483} 484#[tokio::test] 485async fn test_oauth_multiple_clients_same_user() { 486 let url = base_url().await; 487 let http_client = client(); 488 let ts = Utc::now().timestamp_millis(); 489 let handle = format!("multi-client-{}", ts); 490 let email = format!("multi-client-{}@example.com", ts); 491 let password = "multi-client-password"; 492 let create_res = 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 assert_eq!(create_res.status(), StatusCode::OK); 503 let account: Value = create_res.json().await.unwrap(); 504 let user_did = account["did"].as_str().unwrap(); 505 let _ = verify_new_account(&http_client, user_did).await; 506 let mock_client1 = setup_mock_client_metadata("https://client1.example.com/callback").await; 507 let client1_id = mock_client1.uri(); 508 let mock_client2 = setup_mock_client_metadata("https://client2.example.com/callback").await; 509 let client2_id = mock_client2.uri(); 510 let (verifier1, challenge1) = generate_pkce(); 511 let par_res1 = http_client 512 .post(format!("{}/oauth/par", url)) 513 .form(&[ 514 ("response_type", "code"), 515 ("client_id", &client1_id), 516 ("redirect_uri", "https://client1.example.com/callback"), 517 ("code_challenge", &challenge1), 518 ("code_challenge_method", "S256"), 519 ]) 520 .send() 521 .await 522 .unwrap(); 523 let par_body1: Value = par_res1.json().await.unwrap(); 524 let request_uri1 = par_body1["request_uri"].as_str().unwrap(); 525 let auth_client = no_redirect_client(); 526 let auth_res1 = auth_client 527 .post(format!("{}/oauth/authorize", url)) 528 .form(&[ 529 ("request_uri", request_uri1), 530 ("username", &handle), 531 ("password", password), 532 ("remember_device", "false"), 533 ]) 534 .send() 535 .await 536 .unwrap(); 537 let location1 = auth_res1.headers().get("location").unwrap().to_str().unwrap(); 538 let code1 = location1.split("code=").nth(1).unwrap().split('&').next().unwrap(); 539 let token_res1 = http_client 540 .post(format!("{}/oauth/token", url)) 541 .form(&[ 542 ("grant_type", "authorization_code"), 543 ("code", code1), 544 ("redirect_uri", "https://client1.example.com/callback"), 545 ("code_verifier", &verifier1), 546 ("client_id", &client1_id), 547 ]) 548 .send() 549 .await 550 .unwrap(); 551 let token_body1: Value = token_res1.json().await.unwrap(); 552 let token1 = token_body1["access_token"].as_str().unwrap(); 553 let (verifier2, challenge2) = generate_pkce(); 554 let par_res2 = http_client 555 .post(format!("{}/oauth/par", url)) 556 .form(&[ 557 ("response_type", "code"), 558 ("client_id", &client2_id), 559 ("redirect_uri", "https://client2.example.com/callback"), 560 ("code_challenge", &challenge2), 561 ("code_challenge_method", "S256"), 562 ]) 563 .send() 564 .await 565 .unwrap(); 566 let par_body2: Value = par_res2.json().await.unwrap(); 567 let request_uri2 = par_body2["request_uri"].as_str().unwrap(); 568 let auth_res2 = auth_client 569 .post(format!("{}/oauth/authorize", url)) 570 .form(&[ 571 ("request_uri", request_uri2), 572 ("username", &handle), 573 ("password", password), 574 ("remember_device", "false"), 575 ]) 576 .send() 577 .await 578 .unwrap(); 579 let location2 = auth_res2.headers().get("location").unwrap().to_str().unwrap(); 580 let code2 = location2.split("code=").nth(1).unwrap().split('&').next().unwrap(); 581 let token_res2 = http_client 582 .post(format!("{}/oauth/token", url)) 583 .form(&[ 584 ("grant_type", "authorization_code"), 585 ("code", code2), 586 ("redirect_uri", "https://client2.example.com/callback"), 587 ("code_verifier", &verifier2), 588 ("client_id", &client2_id), 589 ]) 590 .send() 591 .await 592 .unwrap(); 593 let token_body2: Value = token_res2.json().await.unwrap(); 594 let token2 = token_body2["access_token"].as_str().unwrap(); 595 assert_ne!(token1, token2, "Different clients should get different tokens"); 596 let collection = "app.bsky.feed.post"; 597 let create_res1 = http_client 598 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 599 .bearer_auth(token1) 600 .json(&json!({ 601 "repo": user_did, 602 "collection": collection, 603 "record": { 604 "$type": collection, 605 "text": "Post from client 1", 606 "createdAt": Utc::now().to_rfc3339() 607 } 608 })) 609 .send() 610 .await 611 .unwrap(); 612 assert_eq!(create_res1.status(), StatusCode::OK, "Client 1 token should work"); 613 let create_res2 = http_client 614 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 615 .bearer_auth(token2) 616 .json(&json!({ 617 "repo": user_did, 618 "collection": collection, 619 "record": { 620 "$type": collection, 621 "text": "Post from client 2", 622 "createdAt": Utc::now().to_rfc3339() 623 } 624 })) 625 .send() 626 .await 627 .unwrap(); 628 assert_eq!(create_res2.status(), StatusCode::OK, "Client 2 token should work"); 629 let list_res = http_client 630 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 631 .bearer_auth(token1) 632 .query(&[ 633 ("repo", user_did), 634 ("collection", collection), 635 ]) 636 .send() 637 .await 638 .unwrap(); 639 let list_body: Value = list_res.json().await.unwrap(); 640 let records = list_body["records"].as_array().unwrap(); 641 assert_eq!(records.len(), 2, "Both posts should be visible to either client"); 642} 643#[tokio::test] 644async fn test_oauth_social_interactions_follow_like_repost() { 645 let url = base_url().await; 646 let http_client = client(); 647 let (alice, _mock_alice) = create_user_and_oauth_session( 648 "alice-social", 649 "https://alice-app.example.com/callback" 650 ).await; 651 let (bob, _mock_bob) = create_user_and_oauth_session( 652 "bob-social", 653 "https://bob-app.example.com/callback" 654 ).await; 655 let post_collection = "app.bsky.feed.post"; 656 let post_res = http_client 657 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 658 .bearer_auth(&alice.access_token) 659 .json(&json!({ 660 "repo": alice.did, 661 "collection": post_collection, 662 "record": { 663 "$type": post_collection, 664 "text": "Hello from Alice! Looking for friends.", 665 "createdAt": Utc::now().to_rfc3339() 666 } 667 })) 668 .send() 669 .await 670 .unwrap(); 671 assert_eq!(post_res.status(), StatusCode::OK); 672 let post_body: Value = post_res.json().await.unwrap(); 673 let post_uri = post_body["uri"].as_str().unwrap(); 674 let post_cid = post_body["cid"].as_str().unwrap(); 675 let follow_collection = "app.bsky.graph.follow"; 676 let follow_res = http_client 677 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 678 .bearer_auth(&bob.access_token) 679 .json(&json!({ 680 "repo": bob.did, 681 "collection": follow_collection, 682 "record": { 683 "$type": follow_collection, 684 "subject": alice.did, 685 "createdAt": Utc::now().to_rfc3339() 686 } 687 })) 688 .send() 689 .await 690 .unwrap(); 691 assert_eq!(follow_res.status(), StatusCode::OK, "Bob should be able to follow Alice via OAuth"); 692 let like_collection = "app.bsky.feed.like"; 693 let like_res = http_client 694 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 695 .bearer_auth(&bob.access_token) 696 .json(&json!({ 697 "repo": bob.did, 698 "collection": like_collection, 699 "record": { 700 "$type": like_collection, 701 "subject": { 702 "uri": post_uri, 703 "cid": post_cid 704 }, 705 "createdAt": Utc::now().to_rfc3339() 706 } 707 })) 708 .send() 709 .await 710 .unwrap(); 711 assert_eq!(like_res.status(), StatusCode::OK, "Bob should be able to like Alice's post via OAuth"); 712 let repost_collection = "app.bsky.feed.repost"; 713 let repost_res = http_client 714 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 715 .bearer_auth(&bob.access_token) 716 .json(&json!({ 717 "repo": bob.did, 718 "collection": repost_collection, 719 "record": { 720 "$type": repost_collection, 721 "subject": { 722 "uri": post_uri, 723 "cid": post_cid 724 }, 725 "createdAt": Utc::now().to_rfc3339() 726 } 727 })) 728 .send() 729 .await 730 .unwrap(); 731 assert_eq!(repost_res.status(), StatusCode::OK, "Bob should be able to repost Alice's post via OAuth"); 732 let bob_follows = http_client 733 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 734 .bearer_auth(&bob.access_token) 735 .query(&[ 736 ("repo", bob.did.as_str()), 737 ("collection", follow_collection), 738 ]) 739 .send() 740 .await 741 .unwrap(); 742 let follows_body: Value = bob_follows.json().await.unwrap(); 743 let follows = follows_body["records"].as_array().unwrap(); 744 assert_eq!(follows.len(), 1, "Bob should have 1 follow"); 745 assert_eq!(follows[0]["value"]["subject"], alice.did); 746 let bob_likes = http_client 747 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 748 .bearer_auth(&bob.access_token) 749 .query(&[ 750 ("repo", bob.did.as_str()), 751 ("collection", like_collection), 752 ]) 753 .send() 754 .await 755 .unwrap(); 756 let likes_body: Value = bob_likes.json().await.unwrap(); 757 let likes = likes_body["records"].as_array().unwrap(); 758 assert_eq!(likes.len(), 1, "Bob should have 1 like"); 759} 760#[tokio::test] 761async fn test_oauth_cannot_modify_other_users_repo() { 762 let url = base_url().await; 763 let http_client = client(); 764 let (alice, _mock_alice) = create_user_and_oauth_session( 765 "alice-boundary", 766 "https://alice.example.com/callback" 767 ).await; 768 let (bob, _mock_bob) = create_user_and_oauth_session( 769 "bob-boundary", 770 "https://bob.example.com/callback" 771 ).await; 772 let collection = "app.bsky.feed.post"; 773 let malicious_res = http_client 774 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 775 .bearer_auth(&bob.access_token) 776 .json(&json!({ 777 "repo": alice.did, 778 "collection": collection, 779 "record": { 780 "$type": collection, 781 "text": "Bob trying to post as Alice!", 782 "createdAt": Utc::now().to_rfc3339() 783 } 784 })) 785 .send() 786 .await 787 .unwrap(); 788 assert_ne!( 789 malicious_res.status(), 790 StatusCode::OK, 791 "Bob should NOT be able to create records in Alice's repo" 792 ); 793 let alice_posts = http_client 794 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 795 .bearer_auth(&alice.access_token) 796 .query(&[ 797 ("repo", alice.did.as_str()), 798 ("collection", collection), 799 ]) 800 .send() 801 .await 802 .unwrap(); 803 let posts_body: Value = alice_posts.json().await.unwrap(); 804 let posts = posts_body["records"].as_array().unwrap(); 805 assert_eq!(posts.len(), 0, "Alice's repo should have no posts from Bob"); 806} 807#[tokio::test] 808async fn test_oauth_session_isolation_between_users() { 809 let url = base_url().await; 810 let http_client = client(); 811 let (alice, _mock_alice) = create_user_and_oauth_session( 812 "alice-isolation", 813 "https://alice.example.com/callback" 814 ).await; 815 let (bob, _mock_bob) = create_user_and_oauth_session( 816 "bob-isolation", 817 "https://bob.example.com/callback" 818 ).await; 819 let collection = "app.bsky.feed.post"; 820 let alice_post = http_client 821 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 822 .bearer_auth(&alice.access_token) 823 .json(&json!({ 824 "repo": alice.did, 825 "collection": collection, 826 "record": { 827 "$type": collection, 828 "text": "Alice's private thoughts", 829 "createdAt": Utc::now().to_rfc3339() 830 } 831 })) 832 .send() 833 .await 834 .unwrap(); 835 assert_eq!(alice_post.status(), StatusCode::OK); 836 let bob_post = http_client 837 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 838 .bearer_auth(&bob.access_token) 839 .json(&json!({ 840 "repo": bob.did, 841 "collection": collection, 842 "record": { 843 "$type": collection, 844 "text": "Bob's different thoughts", 845 "createdAt": Utc::now().to_rfc3339() 846 } 847 })) 848 .send() 849 .await 850 .unwrap(); 851 assert_eq!(bob_post.status(), StatusCode::OK); 852 let alice_list = http_client 853 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 854 .bearer_auth(&alice.access_token) 855 .query(&[ 856 ("repo", alice.did.as_str()), 857 ("collection", collection), 858 ]) 859 .send() 860 .await 861 .unwrap(); 862 let alice_records: Value = alice_list.json().await.unwrap(); 863 let alice_posts = alice_records["records"].as_array().unwrap(); 864 assert_eq!(alice_posts.len(), 1); 865 assert_eq!(alice_posts[0]["value"]["text"], "Alice's private thoughts"); 866 let bob_list = http_client 867 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 868 .bearer_auth(&bob.access_token) 869 .query(&[ 870 ("repo", bob.did.as_str()), 871 ("collection", collection), 872 ]) 873 .send() 874 .await 875 .unwrap(); 876 let bob_records: Value = bob_list.json().await.unwrap(); 877 let bob_posts = bob_records["records"].as_array().unwrap(); 878 assert_eq!(bob_posts.len(), 1); 879 assert_eq!(bob_posts[0]["value"]["text"], "Bob's different thoughts"); 880} 881#[tokio::test] 882async fn test_oauth_token_works_with_sync_endpoints() { 883 let url = base_url().await; 884 let http_client = client(); 885 let (session, _mock) = create_user_and_oauth_session( 886 "oauth-sync", 887 "https://example.com/callback" 888 ).await; 889 let collection = "app.bsky.feed.post"; 890 http_client 891 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 892 .bearer_auth(&session.access_token) 893 .json(&json!({ 894 "repo": session.did, 895 "collection": collection, 896 "record": { 897 "$type": collection, 898 "text": "Post to sync", 899 "createdAt": Utc::now().to_rfc3339() 900 } 901 })) 902 .send() 903 .await 904 .unwrap(); 905 let latest_commit = http_client 906 .get(format!("{}/xrpc/com.atproto.sync.getLatestCommit", url)) 907 .query(&[("did", session.did.as_str())]) 908 .send() 909 .await 910 .unwrap(); 911 assert_eq!(latest_commit.status(), StatusCode::OK); 912 let commit_body: Value = latest_commit.json().await.unwrap(); 913 assert!(commit_body["cid"].is_string()); 914 assert!(commit_body["rev"].is_string()); 915 let repo_status = http_client 916 .get(format!("{}/xrpc/com.atproto.sync.getRepoStatus", url)) 917 .query(&[("did", session.did.as_str())]) 918 .send() 919 .await 920 .unwrap(); 921 assert_eq!(repo_status.status(), StatusCode::OK); 922 let status_body: Value = repo_status.json().await.unwrap(); 923 assert_eq!(status_body["did"], session.did); 924 assert!(status_body["active"].as_bool().unwrap()); 925}