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