PDS software with bells & whistles you didn’t even know you needed. will move this to its own account when ready.
at main 25 kB view raw
1#![allow(unused_imports)] 2mod common; 3use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 4use chrono::{Duration, Utc}; 5use common::{base_url, client, create_account_and_login, get_test_db_pool}; 6use k256::SecretKey; 7use k256::ecdsa::{Signature, SigningKey, signature::Signer}; 8use rand::rngs::OsRng; 9use reqwest::StatusCode; 10use serde_json::{Value, json}; 11use sha2::{Digest, Sha256}; 12use tranquil_pds::auth::{ 13 self, SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH, 14 TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, create_access_token, 15 create_refresh_token, create_service_token, get_did_from_token, get_jti_from_token, 16 verify_access_token, verify_refresh_token, verify_token, 17}; 18 19fn generate_user_key() -> Vec<u8> { 20 let secret_key = SecretKey::random(&mut OsRng); 21 secret_key.to_bytes().to_vec() 22} 23 24fn create_custom_jwt(header: &Value, claims: &Value, key_bytes: &[u8]) -> String { 25 let signing_key = SigningKey::from_slice(key_bytes).expect("valid key"); 26 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(header).unwrap()); 27 let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(claims).unwrap()); 28 let message = format!("{}.{}", header_b64, claims_b64); 29 let signature: Signature = signing_key.sign(message.as_bytes()); 30 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes()); 31 format!("{}.{}", message, signature_b64) 32} 33 34fn create_unsigned_jwt(header: &Value, claims: &Value) -> String { 35 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(header).unwrap()); 36 let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(claims).unwrap()); 37 format!("{}.{}.", header_b64, claims_b64) 38} 39 40#[test] 41fn test_signature_attacks() { 42 let key_bytes = generate_user_key(); 43 let did = "did:plc:test"; 44 let token = create_access_token(did, &key_bytes).expect("create token"); 45 let parts: Vec<&str> = token.split('.').collect(); 46 47 let forged_signature = URL_SAFE_NO_PAD.encode(&[0u8; 64]); 48 let forged_token = format!("{}.{}.{}", parts[0], parts[1], forged_signature); 49 let result = verify_access_token(&forged_token, &key_bytes); 50 assert!(result.is_err(), "Forged signature must be rejected"); 51 assert!( 52 result 53 .err() 54 .unwrap() 55 .to_string() 56 .to_lowercase() 57 .contains("signature") 58 ); 59 60 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap(); 61 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap(); 62 payload["sub"] = json!("did:plc:attacker"); 63 let modified_payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 64 let modified_token = format!("{}.{}.{}", parts[0], modified_payload, parts[2]); 65 assert!( 66 verify_access_token(&modified_token, &key_bytes).is_err(), 67 "Modified payload must be rejected" 68 ); 69 70 let sig_bytes = URL_SAFE_NO_PAD.decode(parts[2]).unwrap(); 71 let truncated_sig = URL_SAFE_NO_PAD.encode(&sig_bytes[..32]); 72 let truncated_token = format!("{}.{}.{}", parts[0], parts[1], truncated_sig); 73 assert!( 74 verify_access_token(&truncated_token, &key_bytes).is_err(), 75 "Truncated signature must be rejected" 76 ); 77 78 let mut extended_sig = sig_bytes.clone(); 79 extended_sig.extend_from_slice(&[0u8; 32]); 80 let extended_token = format!( 81 "{}.{}.{}", 82 parts[0], 83 parts[1], 84 URL_SAFE_NO_PAD.encode(&extended_sig) 85 ); 86 assert!( 87 verify_access_token(&extended_token, &key_bytes).is_err(), 88 "Extended signature must be rejected" 89 ); 90 91 let key_bytes_user2 = generate_user_key(); 92 assert!( 93 verify_access_token(&token, &key_bytes_user2).is_err(), 94 "Token signed with different key must be rejected" 95 ); 96} 97 98#[test] 99fn test_algorithm_substitution_attacks() { 100 let key_bytes = generate_user_key(); 101 let did = "did:plc:test"; 102 103 let none_header = json!({ "alg": "none", "typ": TOKEN_TYPE_ACCESS }); 104 let claims = json!({ 105 "iss": did, "sub": did, "aud": "did:web:test.pds", 106 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, 107 "jti": "attack-token", "scope": SCOPE_ACCESS 108 }); 109 let none_token = create_unsigned_jwt(&none_header, &claims); 110 assert!( 111 verify_access_token(&none_token, &key_bytes).is_err(), 112 "Algorithm 'none' must be rejected" 113 ); 114 115 let hs256_header = json!({ "alg": "HS256", "typ": TOKEN_TYPE_ACCESS }); 116 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&hs256_header).unwrap()); 117 let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&claims).unwrap()); 118 use hmac::{Hmac, Mac}; 119 type HmacSha256 = Hmac<Sha256>; 120 let message = format!("{}.{}", header_b64, claims_b64); 121 let mut mac = HmacSha256::new_from_slice(&key_bytes).unwrap(); 122 mac.update(message.as_bytes()); 123 let hmac_sig = mac.finalize().into_bytes(); 124 let hs256_token = format!("{}.{}", message, URL_SAFE_NO_PAD.encode(&hmac_sig)); 125 assert!( 126 verify_access_token(&hs256_token, &key_bytes).is_err(), 127 "HS256 substitution must be rejected" 128 ); 129 130 for (alg, sig_len) in [("RS256", 256), ("ES256", 64)] { 131 let header = json!({ "alg": alg, "typ": TOKEN_TYPE_ACCESS }); 132 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 133 let fake_sig = URL_SAFE_NO_PAD.encode(&vec![1u8; sig_len]); 134 let token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig); 135 assert!( 136 verify_access_token(&token, &key_bytes).is_err(), 137 "{} substitution must be rejected", 138 alg 139 ); 140 } 141} 142 143#[test] 144fn test_token_type_confusion() { 145 let key_bytes = generate_user_key(); 146 let did = "did:plc:test"; 147 148 let refresh_token = create_refresh_token(did, &key_bytes).expect("create refresh token"); 149 let result = verify_access_token(&refresh_token, &key_bytes); 150 assert!(result.is_err(), "Refresh token as access must be rejected"); 151 assert!( 152 result 153 .err() 154 .unwrap() 155 .to_string() 156 .contains("Invalid token type") 157 ); 158 159 let access_token = create_access_token(did, &key_bytes).expect("create access token"); 160 let result = verify_refresh_token(&access_token, &key_bytes); 161 assert!(result.is_err(), "Access token as refresh must be rejected"); 162 assert!( 163 result 164 .err() 165 .unwrap() 166 .to_string() 167 .contains("Invalid token type") 168 ); 169 170 let service_token = 171 create_service_token(did, "did:web:target", "com.example.method", &key_bytes).unwrap(); 172 assert!( 173 verify_access_token(&service_token, &key_bytes).is_err(), 174 "Service token as access must be rejected" 175 ); 176} 177 178#[test] 179fn test_scope_validation() { 180 let key_bytes = generate_user_key(); 181 let did = "did:plc:test"; 182 let header = json!({ "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS }); 183 184 let invalid_scope = json!({ 185 "iss": did, "sub": did, "aud": "did:web:test.pds", 186 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, 187 "jti": "test", "scope": "admin.all" 188 }); 189 let result = verify_access_token( 190 &create_custom_jwt(&header, &invalid_scope, &key_bytes), 191 &key_bytes, 192 ); 193 assert!( 194 result.is_err() 195 && result 196 .err() 197 .unwrap() 198 .to_string() 199 .contains("Invalid token scope") 200 ); 201 202 let empty_scope = json!({ 203 "iss": did, "sub": did, "aud": "did:web:test.pds", 204 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, 205 "jti": "test", "scope": "" 206 }); 207 assert!( 208 verify_access_token( 209 &create_custom_jwt(&header, &empty_scope, &key_bytes), 210 &key_bytes 211 ) 212 .is_err() 213 ); 214 215 let missing_scope = json!({ 216 "iss": did, "sub": did, "aud": "did:web:test.pds", 217 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, 218 "jti": "test" 219 }); 220 assert!( 221 verify_access_token( 222 &create_custom_jwt(&header, &missing_scope, &key_bytes), 223 &key_bytes 224 ) 225 .is_err() 226 ); 227 228 for scope in [SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED] { 229 let claims = json!({ 230 "iss": did, "sub": did, "aud": "did:web:test.pds", 231 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, 232 "jti": "test", "scope": scope 233 }); 234 assert!( 235 verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes) 236 .is_ok() 237 ); 238 } 239 240 let refresh_scope = json!({ 241 "iss": did, "sub": did, "aud": "did:web:test.pds", 242 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, 243 "jti": "test", "scope": SCOPE_REFRESH 244 }); 245 assert!( 246 verify_access_token( 247 &create_custom_jwt(&header, &refresh_scope, &key_bytes), 248 &key_bytes 249 ) 250 .is_err() 251 ); 252} 253 254#[test] 255fn test_expiration_and_timing() { 256 let key_bytes = generate_user_key(); 257 let did = "did:plc:test"; 258 let header = json!({ "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS }); 259 let now = Utc::now().timestamp(); 260 261 let expired = json!({ 262 "iss": did, "sub": did, "aud": "did:web:test.pds", 263 "iat": now - 7200, "exp": now - 3600, "jti": "test", "scope": SCOPE_ACCESS 264 }); 265 let result = verify_access_token( 266 &create_custom_jwt(&header, &expired, &key_bytes), 267 &key_bytes, 268 ); 269 assert!(result.is_err() && result.err().unwrap().to_string().contains("expired")); 270 271 let future_iat = json!({ 272 "iss": did, "sub": did, "aud": "did:web:test.pds", 273 "iat": now + 60, "exp": now + 7200, "jti": "test", "scope": SCOPE_ACCESS 274 }); 275 assert!( 276 verify_access_token( 277 &create_custom_jwt(&header, &future_iat, &key_bytes), 278 &key_bytes 279 ) 280 .is_ok() 281 ); 282 283 let just_expired = json!({ 284 "iss": did, "sub": did, "aud": "did:web:test.pds", 285 "iat": now - 10, "exp": now - 1, "jti": "test", "scope": SCOPE_ACCESS 286 }); 287 assert!( 288 verify_access_token( 289 &create_custom_jwt(&header, &just_expired, &key_bytes), 290 &key_bytes 291 ) 292 .is_err() 293 ); 294 295 let far_future = json!({ 296 "iss": did, "sub": did, "aud": "did:web:test.pds", 297 "iat": now, "exp": i64::MAX, "jti": "test", "scope": SCOPE_ACCESS 298 }); 299 let _ = verify_access_token( 300 &create_custom_jwt(&header, &far_future, &key_bytes), 301 &key_bytes, 302 ); 303 304 let negative_iat = json!({ 305 "iss": did, "sub": did, "aud": "did:web:test.pds", 306 "iat": -1000000000i64, "exp": now + 3600, "jti": "test", "scope": SCOPE_ACCESS 307 }); 308 let _ = verify_access_token( 309 &create_custom_jwt(&header, &negative_iat, &key_bytes), 310 &key_bytes, 311 ); 312} 313 314#[test] 315fn test_malformed_tokens() { 316 let key_bytes = generate_user_key(); 317 318 for token in [ 319 "", 320 "not-a-token", 321 "one.two", 322 "one.two.three.four", 323 "....", 324 "eyJhbGciOiJFUzI1NksifQ", 325 "eyJhbGciOiJFUzI1NksifQ.", 326 "eyJhbGciOiJFUzI1NksifQ..", 327 ".eyJzdWIiOiJ0ZXN0In0.", 328 "!!invalid-base64!!.eyJzdWIiOiJ0ZXN0In0.sig", 329 ] { 330 assert!( 331 verify_access_token(token, &key_bytes).is_err(), 332 "Malformed token must be rejected" 333 ); 334 } 335 336 let invalid_header = URL_SAFE_NO_PAD.encode("{not valid json}"); 337 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"sub":"test"}"#); 338 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 64]); 339 assert!( 340 verify_access_token( 341 &format!("{}.{}.{}", invalid_header, claims_b64, fake_sig), 342 &key_bytes 343 ) 344 .is_err() 345 ); 346 347 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K","typ":"at+jwt"}"#); 348 let invalid_claims = URL_SAFE_NO_PAD.encode("{not valid json}"); 349 assert!( 350 verify_access_token( 351 &format!("{}.{}.{}", header_b64, invalid_claims, fake_sig), 352 &key_bytes 353 ) 354 .is_err() 355 ); 356} 357 358#[test] 359fn test_claim_validation() { 360 let key_bytes = generate_user_key(); 361 let did = "did:plc:test"; 362 let header = json!({ "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS }); 363 364 let missing_exp = json!({ 365 "iss": did, "sub": did, "aud": "did:web:test", 366 "iat": Utc::now().timestamp(), "scope": SCOPE_ACCESS 367 }); 368 assert!( 369 verify_access_token( 370 &create_custom_jwt(&header, &missing_exp, &key_bytes), 371 &key_bytes 372 ) 373 .is_err() 374 ); 375 376 let missing_iat = json!({ 377 "iss": did, "sub": did, "aud": "did:web:test", 378 "exp": Utc::now().timestamp() + 3600, "scope": SCOPE_ACCESS 379 }); 380 assert!( 381 verify_access_token( 382 &create_custom_jwt(&header, &missing_iat, &key_bytes), 383 &key_bytes 384 ) 385 .is_err() 386 ); 387 388 let missing_sub = json!({ 389 "iss": did, "aud": "did:web:test", 390 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, "scope": SCOPE_ACCESS 391 }); 392 assert!( 393 verify_access_token( 394 &create_custom_jwt(&header, &missing_sub, &key_bytes), 395 &key_bytes 396 ) 397 .is_err() 398 ); 399 400 let wrong_types = json!({ 401 "iss": 12345, "sub": ["did:plc:test"], "aud": {"url": "did:web:test"}, 402 "iat": "not a number", "exp": "also not a number", "jti": null, "scope": SCOPE_ACCESS 403 }); 404 assert!( 405 verify_access_token( 406 &create_custom_jwt(&header, &wrong_types, &key_bytes), 407 &key_bytes 408 ) 409 .is_err() 410 ); 411 412 let unicode_injection = json!({ 413 "iss": "did:plc:test\u{0000}attacker", "sub": "did:plc:test\u{202E}rekatta", 414 "aud": "did:web:test.pds", "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, 415 "jti": "test", "scope": SCOPE_ACCESS 416 }); 417 if let Ok(data) = verify_access_token( 418 &create_custom_jwt(&header, &unicode_injection, &key_bytes), 419 &key_bytes, 420 ) { 421 assert!(!data.claims.sub.contains('\0')); 422 } 423} 424 425#[test] 426fn test_did_and_jti_extraction() { 427 let key_bytes = generate_user_key(); 428 let did = "did:plc:legitimate"; 429 let token = create_access_token(did, &key_bytes).expect("create token"); 430 431 assert_eq!(get_did_from_token(&token).unwrap(), did); 432 assert!(get_did_from_token("invalid").is_err()); 433 assert!(get_did_from_token("a.b").is_err()); 434 assert!(get_did_from_token("").is_err()); 435 436 let jti = get_jti_from_token(&token).unwrap(); 437 assert!(!jti.is_empty()); 438 assert!(get_jti_from_token("invalid").is_err()); 439 440 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K"}"#); 441 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:iss","sub":"did:plc:sub"}"#); 442 let fake_sig = URL_SAFE_NO_PAD.encode(&[0u8; 64]); 443 let unverified = format!("{}.{}.{}", header_b64, claims_b64, fake_sig); 444 assert_eq!(get_did_from_token(&unverified).unwrap(), "did:plc:sub"); 445 446 let no_jti_claims = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:test"}"#); 447 assert!(get_jti_from_token(&format!("{}.{}.{}", header_b64, no_jti_claims, fake_sig)).is_err()); 448} 449 450#[test] 451fn test_header_injection_and_constant_time() { 452 let key_bytes = generate_user_key(); 453 let did = "did:plc:test"; 454 455 let header = json!({ 456 "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS, 457 "kid": "../../../../../../etc/passwd", "jku": "https://attacker.com/keys" 458 }); 459 let claims = json!({ 460 "iss": did, "sub": did, "aud": "did:web:test.pds", 461 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, 462 "jti": "test", "scope": SCOPE_ACCESS 463 }); 464 assert!( 465 verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes).is_ok() 466 ); 467 468 let valid_token = create_access_token(did, &key_bytes).expect("create token"); 469 let parts: Vec<&str> = valid_token.split('.').collect(); 470 let mut almost_valid = URL_SAFE_NO_PAD.decode(parts[2]).unwrap(); 471 almost_valid[0] ^= 1; 472 let almost_valid_token = format!( 473 "{}.{}.{}", 474 parts[0], 475 parts[1], 476 URL_SAFE_NO_PAD.encode(&almost_valid) 477 ); 478 let completely_invalid_token = format!( 479 "{}.{}.{}", 480 parts[0], 481 parts[1], 482 URL_SAFE_NO_PAD.encode(&[0xFFu8; 64]) 483 ); 484 let _ = verify_access_token(&almost_valid_token, &key_bytes); 485 let _ = verify_access_token(&completely_invalid_token, &key_bytes); 486} 487 488#[tokio::test] 489async fn test_server_rejects_invalid_tokens() { 490 let url = base_url().await; 491 let http_client = client(); 492 493 let key_bytes = generate_user_key(); 494 let forged_token = create_access_token("did:plc:fake-user", &key_bytes).unwrap(); 495 let res = http_client 496 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 497 .header("Authorization", format!("Bearer {}", forged_token)) 498 .send() 499 .await 500 .unwrap(); 501 assert_eq!( 502 res.status(), 503 StatusCode::UNAUTHORIZED, 504 "Forged token must be rejected" 505 ); 506 507 let (access_jwt, _did) = create_account_and_login(&http_client).await; 508 let parts: Vec<&str> = access_jwt.split('.').collect(); 509 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap(); 510 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap(); 511 512 payload["exp"] = json!(Utc::now().timestamp() - 3600); 513 let expired_token = format!( 514 "{}.{}.{}", 515 parts[0], 516 URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()), 517 parts[2] 518 ); 519 let res = http_client 520 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 521 .header("Authorization", format!("Bearer {}", expired_token)) 522 .send() 523 .await 524 .unwrap(); 525 assert_eq!(res.status(), StatusCode::UNAUTHORIZED); 526 527 let mut tampered_payload: Value = serde_json::from_slice(&payload_bytes).unwrap(); 528 tampered_payload["sub"] = json!("did:plc:attacker"); 529 tampered_payload["iss"] = json!("did:plc:attacker"); 530 let tampered_token = format!( 531 "{}.{}.{}", 532 parts[0], 533 URL_SAFE_NO_PAD.encode(serde_json::to_string(&tampered_payload).unwrap()), 534 parts[2] 535 ); 536 let res = http_client 537 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 538 .header("Authorization", format!("Bearer {}", tampered_token)) 539 .send() 540 .await 541 .unwrap(); 542 assert_eq!(res.status(), StatusCode::UNAUTHORIZED); 543} 544 545#[tokio::test] 546async fn test_authorization_header_formats() { 547 let url = base_url().await; 548 let http_client = client(); 549 let (access_jwt, _did) = create_account_and_login(&http_client).await; 550 551 let res = http_client 552 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 553 .header("Authorization", format!("Bearer {}", access_jwt)) 554 .send() 555 .await 556 .unwrap(); 557 assert_eq!(res.status(), StatusCode::OK); 558 559 let res = http_client 560 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 561 .header("Authorization", format!("bearer {}", access_jwt)) 562 .send() 563 .await 564 .unwrap(); 565 assert_eq!(res.status(), StatusCode::OK); 566 567 let res = http_client 568 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 569 .header("Authorization", format!("Basic {}", access_jwt)) 570 .send() 571 .await 572 .unwrap(); 573 assert_eq!(res.status(), StatusCode::UNAUTHORIZED); 574 575 let res = http_client 576 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 577 .header("Authorization", &access_jwt) 578 .send() 579 .await 580 .unwrap(); 581 assert_eq!(res.status(), StatusCode::UNAUTHORIZED); 582 583 let res = http_client 584 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 585 .header("Authorization", "Bearer ") 586 .send() 587 .await 588 .unwrap(); 589 assert_eq!(res.status(), StatusCode::UNAUTHORIZED); 590} 591 592#[tokio::test] 593async fn test_session_lifecycle_security() { 594 let url = base_url().await; 595 let http_client = client(); 596 let (access_jwt, _did) = create_account_and_login(&http_client).await; 597 598 let res = http_client 599 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 600 .header("Authorization", format!("Bearer {}", access_jwt)) 601 .send() 602 .await 603 .unwrap(); 604 assert_eq!(res.status(), StatusCode::OK); 605 606 let logout = http_client 607 .post(format!("{}/xrpc/com.atproto.server.deleteSession", url)) 608 .header("Authorization", format!("Bearer {}", access_jwt)) 609 .send() 610 .await 611 .unwrap(); 612 assert_eq!(logout.status(), StatusCode::OK); 613 614 let res = http_client 615 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 616 .header("Authorization", format!("Bearer {}", access_jwt)) 617 .send() 618 .await 619 .unwrap(); 620 assert_eq!(res.status(), StatusCode::UNAUTHORIZED); 621} 622 623#[tokio::test] 624async fn test_deactivated_account_behavior() { 625 let url = base_url().await; 626 let http_client = client(); 627 let (access_jwt, _did) = create_account_and_login(&http_client).await; 628 629 let deact = http_client 630 .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url)) 631 .header("Authorization", format!("Bearer {}", access_jwt)) 632 .json(&json!({})) 633 .send() 634 .await 635 .unwrap(); 636 assert_eq!(deact.status(), StatusCode::OK); 637 638 let res = http_client 639 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 640 .header("Authorization", format!("Bearer {}", access_jwt)) 641 .send() 642 .await 643 .unwrap(); 644 assert_eq!(res.status(), StatusCode::OK); 645 let body: Value = res.json().await.unwrap(); 646 assert_eq!(body["active"], false); 647 648 let post_res = http_client 649 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 650 .header("Authorization", format!("Bearer {}", access_jwt)) 651 .json(&json!({ 652 "repo": _did, 653 "collection": "app.bsky.feed.post", 654 "record": { 655 "$type": "app.bsky.feed.post", 656 "text": "test", 657 "createdAt": "2024-01-01T00:00:00Z" 658 } 659 })) 660 .send() 661 .await 662 .unwrap(); 663 assert_eq!(post_res.status(), StatusCode::UNAUTHORIZED); 664 let post_body: Value = post_res.json().await.unwrap(); 665 assert_eq!(post_body["error"], "AccountDeactivated"); 666} 667 668#[tokio::test] 669async fn test_refresh_token_replay_protection() { 670 let url = base_url().await; 671 let http_client = client(); 672 let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 673 let handle = format!("rr{}", suffix); 674 let email = format!("rr{}@example.com", suffix); 675 676 let create_res = http_client 677 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 678 .json(&json!({ "handle": handle, "email": email, "password": "Testpass123!" })) 679 .send() 680 .await 681 .unwrap(); 682 assert_eq!(create_res.status(), StatusCode::OK); 683 let account: Value = create_res.json().await.unwrap(); 684 let did = account["did"].as_str().unwrap(); 685 686 let pool = get_test_db_pool().await; 687 let body_text: String = sqlx::query_scalar!( 688 "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1", 689 did 690 ).fetch_one(pool).await.unwrap(); 691 let lines: Vec<&str> = body_text.lines().collect(); 692 let code = lines 693 .iter() 694 .enumerate() 695 .find(|(_, line)| line.contains("verification code is:") || line.contains("code is:")) 696 .and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string())) 697 .or_else(|| { 698 body_text 699 .split_whitespace() 700 .find(|word| word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3) 701 .map(|s| s.to_string()) 702 }) 703 .unwrap_or_else(|| body_text.clone()); 704 705 let confirm = http_client 706 .post(format!("{}/xrpc/com.atproto.server.confirmSignup", url)) 707 .json(&json!({ "did": did, "verificationCode": code })) 708 .send() 709 .await 710 .unwrap(); 711 assert_eq!(confirm.status(), StatusCode::OK); 712 let confirmed: Value = confirm.json().await.unwrap(); 713 let refresh_jwt = confirmed["refreshJwt"].as_str().unwrap().to_string(); 714 715 let first = http_client 716 .post(format!("{}/xrpc/com.atproto.server.refreshSession", url)) 717 .header("Authorization", format!("Bearer {}", refresh_jwt)) 718 .send() 719 .await 720 .unwrap(); 721 assert_eq!(first.status(), StatusCode::OK); 722 723 let replay = http_client 724 .post(format!("{}/xrpc/com.atproto.server.refreshSession", url)) 725 .header("Authorization", format!("Bearer {}", refresh_jwt)) 726 .send() 727 .await 728 .unwrap(); 729 assert_eq!(replay.status(), StatusCode::UNAUTHORIZED); 730}