this repo has no description
1#![allow(unused_imports)] 2 3mod common; 4 5use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 6use bspds::auth::{ 7 self, create_access_token, create_refresh_token, create_service_token, 8 verify_access_token, verify_refresh_token, verify_token, get_did_from_token, get_jti_from_token, 9 TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, 10 SCOPE_ACCESS, SCOPE_REFRESH, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, 11}; 12use chrono::{Duration, Utc}; 13use common::{base_url, client, create_account_and_login, get_db_connection_string}; 14use k256::SecretKey; 15use k256::ecdsa::{SigningKey, Signature, signature::Signer}; 16use rand::rngs::OsRng; 17use reqwest::StatusCode; 18use serde_json::{json, Value}; 19use sha2::{Digest, Sha256}; 20 21fn generate_user_key() -> Vec<u8> { 22 let secret_key = SecretKey::random(&mut OsRng); 23 secret_key.to_bytes().to_vec() 24} 25 26fn create_custom_jwt(header: &Value, claims: &Value, key_bytes: &[u8]) -> String { 27 let signing_key = SigningKey::from_slice(key_bytes).expect("valid key"); 28 29 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(header).unwrap()); 30 let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(claims).unwrap()); 31 let message = format!("{}.{}", header_b64, claims_b64); 32 33 let signature: Signature = signing_key.sign(message.as_bytes()); 34 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes()); 35 36 format!("{}.{}", message, signature_b64) 37} 38 39fn create_unsigned_jwt(header: &Value, claims: &Value) -> String { 40 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(header).unwrap()); 41 let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(claims).unwrap()); 42 format!("{}.{}.", header_b64, claims_b64) 43} 44 45#[test] 46fn test_jwt_security_forged_signature_rejected() { 47 let key_bytes = generate_user_key(); 48 let did = "did:plc:test"; 49 50 let token = create_access_token(did, &key_bytes).expect("create token"); 51 let parts: Vec<&str> = token.split('.').collect(); 52 53 let forged_signature = URL_SAFE_NO_PAD.encode(&[0u8; 64]); 54 let forged_token = format!("{}.{}.{}", parts[0], parts[1], forged_signature); 55 56 let result = verify_access_token(&forged_token, &key_bytes); 57 assert!(result.is_err(), "Forged signature must be rejected"); 58 let err_msg = result.err().unwrap().to_string(); 59 assert!(err_msg.contains("signature") || err_msg.contains("Signature"), "Error should mention signature: {}", err_msg); 60} 61 62#[test] 63fn test_jwt_security_modified_payload_rejected() { 64 let key_bytes = generate_user_key(); 65 let did = "did:plc:legitimate"; 66 67 let token = create_access_token(did, &key_bytes).expect("create token"); 68 let parts: Vec<&str> = token.split('.').collect(); 69 70 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap(); 71 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap(); 72 payload["sub"] = json!("did:plc:attacker"); 73 let modified_payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 74 let modified_token = format!("{}.{}.{}", parts[0], modified_payload, parts[2]); 75 76 let result = verify_access_token(&modified_token, &key_bytes); 77 assert!(result.is_err(), "Modified payload must be rejected"); 78} 79 80#[test] 81fn test_jwt_security_algorithm_none_attack_rejected() { 82 let key_bytes = generate_user_key(); 83 let did = "did:plc:test"; 84 85 let header = json!({ 86 "alg": "none", 87 "typ": TOKEN_TYPE_ACCESS 88 }); 89 let claims = json!({ 90 "iss": did, 91 "sub": did, 92 "aud": "did:web:test.pds", 93 "iat": Utc::now().timestamp(), 94 "exp": Utc::now().timestamp() + 3600, 95 "jti": "attacker-token-1", 96 "scope": SCOPE_ACCESS 97 }); 98 99 let malicious_token = create_unsigned_jwt(&header, &claims); 100 101 let result = verify_access_token(&malicious_token, &key_bytes); 102 assert!(result.is_err(), "Algorithm 'none' attack must be rejected"); 103} 104 105#[test] 106fn test_jwt_security_algorithm_substitution_hs256_rejected() { 107 let key_bytes = generate_user_key(); 108 let did = "did:plc:test"; 109 110 let header = json!({ 111 "alg": "HS256", 112 "typ": TOKEN_TYPE_ACCESS 113 }); 114 let claims = json!({ 115 "iss": did, 116 "sub": did, 117 "aud": "did:web:test.pds", 118 "iat": Utc::now().timestamp(), 119 "exp": Utc::now().timestamp() + 3600, 120 "jti": "attacker-token-2", 121 "scope": SCOPE_ACCESS 122 }); 123 124 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 125 let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&claims).unwrap()); 126 127 use hmac::{Hmac, Mac}; 128 type HmacSha256 = Hmac<Sha256>; 129 let message = format!("{}.{}", header_b64, claims_b64); 130 let mut mac = HmacSha256::new_from_slice(&key_bytes).unwrap(); 131 mac.update(message.as_bytes()); 132 let hmac_sig = mac.finalize().into_bytes(); 133 let signature_b64 = URL_SAFE_NO_PAD.encode(&hmac_sig); 134 135 let malicious_token = format!("{}.{}", message, signature_b64); 136 137 let result = verify_access_token(&malicious_token, &key_bytes); 138 assert!(result.is_err(), "HS256 algorithm substitution must be rejected"); 139} 140 141#[test] 142fn test_jwt_security_algorithm_substitution_rs256_rejected() { 143 let key_bytes = generate_user_key(); 144 let did = "did:plc:test"; 145 146 let header = json!({ 147 "alg": "RS256", 148 "typ": TOKEN_TYPE_ACCESS 149 }); 150 let claims = json!({ 151 "iss": did, 152 "sub": did, 153 "aud": "did:web:test.pds", 154 "iat": Utc::now().timestamp(), 155 "exp": Utc::now().timestamp() + 3600, 156 "jti": "attacker-token-3", 157 "scope": SCOPE_ACCESS 158 }); 159 160 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 161 let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&claims).unwrap()); 162 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 256]); 163 164 let malicious_token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig); 165 166 let result = verify_access_token(&malicious_token, &key_bytes); 167 assert!(result.is_err(), "RS256 algorithm substitution must be rejected"); 168} 169 170#[test] 171fn test_jwt_security_algorithm_substitution_es256_rejected() { 172 let key_bytes = generate_user_key(); 173 let did = "did:plc:test"; 174 175 let header = json!({ 176 "alg": "ES256", 177 "typ": TOKEN_TYPE_ACCESS 178 }); 179 let claims = json!({ 180 "iss": did, 181 "sub": did, 182 "aud": "did:web:test.pds", 183 "iat": Utc::now().timestamp(), 184 "exp": Utc::now().timestamp() + 3600, 185 "jti": "attacker-token-4", 186 "scope": SCOPE_ACCESS 187 }); 188 189 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 190 let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&claims).unwrap()); 191 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 64]); 192 193 let malicious_token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig); 194 195 let result = verify_access_token(&malicious_token, &key_bytes); 196 assert!(result.is_err(), "ES256 (P-256) algorithm substitution must be rejected (we use ES256K/secp256k1)"); 197} 198 199#[test] 200fn test_jwt_security_token_type_confusion_refresh_as_access() { 201 let key_bytes = generate_user_key(); 202 let did = "did:plc:test"; 203 204 let refresh_token = create_refresh_token(did, &key_bytes).expect("create refresh token"); 205 206 let result = verify_access_token(&refresh_token, &key_bytes); 207 assert!(result.is_err(), "Refresh token must not be accepted as access token"); 208 let err_msg = result.err().unwrap().to_string(); 209 assert!(err_msg.contains("Invalid token type"), "Error: {}", err_msg); 210} 211 212#[test] 213fn test_jwt_security_token_type_confusion_access_as_refresh() { 214 let key_bytes = generate_user_key(); 215 let did = "did:plc:test"; 216 217 let access_token = create_access_token(did, &key_bytes).expect("create access token"); 218 219 let result = verify_refresh_token(&access_token, &key_bytes); 220 assert!(result.is_err(), "Access token must not be accepted as refresh token"); 221 let err_msg = result.err().unwrap().to_string(); 222 assert!(err_msg.contains("Invalid token type"), "Error: {}", err_msg); 223} 224 225#[test] 226fn test_jwt_security_token_type_confusion_service_as_access() { 227 let key_bytes = generate_user_key(); 228 let did = "did:plc:test"; 229 230 let service_token = create_service_token(did, "did:web:target", "com.example.method", &key_bytes) 231 .expect("create service token"); 232 233 let result = verify_access_token(&service_token, &key_bytes); 234 assert!(result.is_err(), "Service token must not be accepted as access token"); 235} 236 237#[test] 238fn test_jwt_security_scope_manipulation_attack() { 239 let key_bytes = generate_user_key(); 240 let did = "did:plc:test"; 241 242 let header = json!({ 243 "alg": "ES256K", 244 "typ": TOKEN_TYPE_ACCESS 245 }); 246 let claims = json!({ 247 "iss": did, 248 "sub": did, 249 "aud": "did:web:test.pds", 250 "iat": Utc::now().timestamp(), 251 "exp": Utc::now().timestamp() + 3600, 252 "jti": "scope-attack-token", 253 "scope": "admin.all" 254 }); 255 256 let malicious_token = create_custom_jwt(&header, &claims, &key_bytes); 257 258 let result = verify_access_token(&malicious_token, &key_bytes); 259 assert!(result.is_err(), "Invalid scope must be rejected"); 260 let err_msg = result.err().unwrap().to_string(); 261 assert!(err_msg.contains("Invalid token scope"), "Error: {}", err_msg); 262} 263 264#[test] 265fn test_jwt_security_empty_scope_rejected() { 266 let key_bytes = generate_user_key(); 267 let did = "did:plc:test"; 268 269 let header = json!({ 270 "alg": "ES256K", 271 "typ": TOKEN_TYPE_ACCESS 272 }); 273 let claims = json!({ 274 "iss": did, 275 "sub": did, 276 "aud": "did:web:test.pds", 277 "iat": Utc::now().timestamp(), 278 "exp": Utc::now().timestamp() + 3600, 279 "jti": "empty-scope-token", 280 "scope": "" 281 }); 282 283 let token = create_custom_jwt(&header, &claims, &key_bytes); 284 285 let result = verify_access_token(&token, &key_bytes); 286 assert!(result.is_err(), "Empty scope must be rejected for access tokens"); 287} 288 289#[test] 290fn test_jwt_security_missing_scope_rejected() { 291 let key_bytes = generate_user_key(); 292 let did = "did:plc:test"; 293 294 let header = json!({ 295 "alg": "ES256K", 296 "typ": TOKEN_TYPE_ACCESS 297 }); 298 let claims = json!({ 299 "iss": did, 300 "sub": did, 301 "aud": "did:web:test.pds", 302 "iat": Utc::now().timestamp(), 303 "exp": Utc::now().timestamp() + 3600, 304 "jti": "no-scope-token" 305 }); 306 307 let token = create_custom_jwt(&header, &claims, &key_bytes); 308 309 let result = verify_access_token(&token, &key_bytes); 310 assert!(result.is_err(), "Missing scope must be rejected for access tokens"); 311} 312 313#[test] 314fn test_jwt_security_expired_token_rejected() { 315 let key_bytes = generate_user_key(); 316 let did = "did:plc:test"; 317 318 let header = json!({ 319 "alg": "ES256K", 320 "typ": TOKEN_TYPE_ACCESS 321 }); 322 let claims = json!({ 323 "iss": did, 324 "sub": did, 325 "aud": "did:web:test.pds", 326 "iat": Utc::now().timestamp() - 7200, 327 "exp": Utc::now().timestamp() - 3600, 328 "jti": "expired-token", 329 "scope": SCOPE_ACCESS 330 }); 331 332 let expired_token = create_custom_jwt(&header, &claims, &key_bytes); 333 334 let result = verify_access_token(&expired_token, &key_bytes); 335 assert!(result.is_err(), "Expired token must be rejected"); 336 let err_msg = result.err().unwrap().to_string(); 337 assert!(err_msg.contains("expired"), "Error: {}", err_msg); 338} 339 340#[test] 341fn test_jwt_security_future_iat_accepted() { 342 let key_bytes = generate_user_key(); 343 let did = "did:plc:test"; 344 345 let header = json!({ 346 "alg": "ES256K", 347 "typ": TOKEN_TYPE_ACCESS 348 }); 349 let claims = json!({ 350 "iss": did, 351 "sub": did, 352 "aud": "did:web:test.pds", 353 "iat": Utc::now().timestamp() + 60, 354 "exp": Utc::now().timestamp() + 7200, 355 "jti": "future-iat-token", 356 "scope": SCOPE_ACCESS 357 }); 358 359 let token = create_custom_jwt(&header, &claims, &key_bytes); 360 361 let result = verify_access_token(&token, &key_bytes); 362 assert!(result.is_ok(), "Slight future iat should be accepted for clock skew tolerance"); 363} 364 365#[test] 366fn test_jwt_security_cross_user_key_attack() { 367 let key_bytes_user1 = generate_user_key(); 368 let key_bytes_user2 = generate_user_key(); 369 370 let did = "did:plc:user1"; 371 let token = create_access_token(did, &key_bytes_user1).expect("create token"); 372 373 let result = verify_access_token(&token, &key_bytes_user2); 374 assert!(result.is_err(), "Token signed by user1's key must not verify with user2's key"); 375} 376 377#[test] 378fn test_jwt_security_signature_truncation_rejected() { 379 let key_bytes = generate_user_key(); 380 let did = "did:plc:test"; 381 382 let token = create_access_token(did, &key_bytes).expect("create token"); 383 let parts: Vec<&str> = token.split('.').collect(); 384 385 let sig_bytes = URL_SAFE_NO_PAD.decode(parts[2]).unwrap(); 386 let truncated_sig = URL_SAFE_NO_PAD.encode(&sig_bytes[..32]); 387 let truncated_token = format!("{}.{}.{}", parts[0], parts[1], truncated_sig); 388 389 let result = verify_access_token(&truncated_token, &key_bytes); 390 assert!(result.is_err(), "Truncated signature must be rejected"); 391} 392 393#[test] 394fn test_jwt_security_signature_extension_rejected() { 395 let key_bytes = generate_user_key(); 396 let did = "did:plc:test"; 397 398 let token = create_access_token(did, &key_bytes).expect("create token"); 399 let parts: Vec<&str> = token.split('.').collect(); 400 401 let mut sig_bytes = URL_SAFE_NO_PAD.decode(parts[2]).unwrap(); 402 sig_bytes.extend_from_slice(&[0u8; 32]); 403 let extended_sig = URL_SAFE_NO_PAD.encode(&sig_bytes); 404 let extended_token = format!("{}.{}.{}", parts[0], parts[1], extended_sig); 405 406 let result = verify_access_token(&extended_token, &key_bytes); 407 assert!(result.is_err(), "Extended signature must be rejected"); 408} 409 410#[test] 411fn test_jwt_security_malformed_tokens_rejected() { 412 let key_bytes = generate_user_key(); 413 414 let malformed_tokens = vec![ 415 "", 416 "not-a-token", 417 "one.two", 418 "one.two.three.four", 419 "....", 420 "eyJhbGciOiJFUzI1NksifQ", 421 "eyJhbGciOiJFUzI1NksifQ.", 422 "eyJhbGciOiJFUzI1NksifQ..", 423 ".eyJzdWIiOiJ0ZXN0In0.", 424 "!!invalid-base64!!.eyJzdWIiOiJ0ZXN0In0.sig", 425 "eyJhbGciOiJFUzI1NksifQ.!!invalid!!.sig", 426 ]; 427 428 for token in malformed_tokens { 429 let result = verify_access_token(token, &key_bytes); 430 assert!(result.is_err(), "Malformed token '{}' must be rejected", 431 if token.len() > 40 { &token[..40] } else { token }); 432 } 433} 434 435#[test] 436fn test_jwt_security_missing_required_claims_rejected() { 437 let key_bytes = generate_user_key(); 438 let did = "did:plc:test"; 439 440 let test_cases = vec![ 441 (json!({ 442 "iss": did, 443 "sub": did, 444 "aud": "did:web:test", 445 "iat": Utc::now().timestamp(), 446 "scope": SCOPE_ACCESS 447 }), "exp"), 448 (json!({ 449 "iss": did, 450 "sub": did, 451 "aud": "did:web:test", 452 "exp": Utc::now().timestamp() + 3600, 453 "scope": SCOPE_ACCESS 454 }), "iat"), 455 (json!({ 456 "iss": did, 457 "aud": "did:web:test", 458 "iat": Utc::now().timestamp(), 459 "exp": Utc::now().timestamp() + 3600, 460 "scope": SCOPE_ACCESS 461 }), "sub"), 462 ]; 463 464 for (claims, missing_claim) in test_cases { 465 let header = json!({ 466 "alg": "ES256K", 467 "typ": TOKEN_TYPE_ACCESS 468 }); 469 470 let token = create_custom_jwt(&header, &claims, &key_bytes); 471 472 let result = verify_access_token(&token, &key_bytes); 473 assert!(result.is_err(), "Token missing '{}' claim must be rejected", missing_claim); 474 } 475} 476 477#[test] 478fn test_jwt_security_invalid_header_json_rejected() { 479 let key_bytes = generate_user_key(); 480 481 let invalid_header = URL_SAFE_NO_PAD.encode("{not valid json}"); 482 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"sub":"test"}"#); 483 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 64]); 484 485 let malicious_token = format!("{}.{}.{}", invalid_header, claims_b64, fake_sig); 486 487 let result = verify_access_token(&malicious_token, &key_bytes); 488 assert!(result.is_err(), "Invalid header JSON must be rejected"); 489} 490 491#[test] 492fn test_jwt_security_invalid_claims_json_rejected() { 493 let key_bytes = generate_user_key(); 494 495 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K","typ":"at+jwt"}"#); 496 let invalid_claims = URL_SAFE_NO_PAD.encode("{not valid json}"); 497 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 64]); 498 499 let malicious_token = format!("{}.{}.{}", header_b64, invalid_claims, fake_sig); 500 501 let result = verify_access_token(&malicious_token, &key_bytes); 502 assert!(result.is_err(), "Invalid claims JSON must be rejected"); 503} 504 505#[test] 506fn test_jwt_security_header_injection_attack() { 507 let key_bytes = generate_user_key(); 508 let did = "did:plc:test"; 509 510 let header = json!({ 511 "alg": "ES256K", 512 "typ": TOKEN_TYPE_ACCESS, 513 "kid": "../../../../../../etc/passwd", 514 "jku": "https://attacker.com/keys" 515 }); 516 let claims = json!({ 517 "iss": did, 518 "sub": did, 519 "aud": "did:web:test.pds", 520 "iat": Utc::now().timestamp(), 521 "exp": Utc::now().timestamp() + 3600, 522 "jti": "header-injection-token", 523 "scope": SCOPE_ACCESS 524 }); 525 526 let token = create_custom_jwt(&header, &claims, &key_bytes); 527 528 let result = verify_access_token(&token, &key_bytes); 529 assert!(result.is_ok(), "Extra header fields should not cause issues (we ignore them)"); 530} 531 532#[test] 533fn test_jwt_security_claims_type_confusion() { 534 let key_bytes = generate_user_key(); 535 536 let header = json!({ 537 "alg": "ES256K", 538 "typ": TOKEN_TYPE_ACCESS 539 }); 540 let claims = json!({ 541 "iss": 12345, 542 "sub": ["did:plc:test"], 543 "aud": {"url": "did:web:test"}, 544 "iat": "not a number", 545 "exp": "also not a number", 546 "jti": null, 547 "scope": SCOPE_ACCESS 548 }); 549 550 let token = create_custom_jwt(&header, &claims, &key_bytes); 551 552 let result = verify_access_token(&token, &key_bytes); 553 assert!(result.is_err(), "Claims with wrong types must be rejected"); 554} 555 556#[test] 557fn test_jwt_security_unicode_injection_in_claims() { 558 let key_bytes = generate_user_key(); 559 560 let header = json!({ 561 "alg": "ES256K", 562 "typ": TOKEN_TYPE_ACCESS 563 }); 564 let claims = json!({ 565 "iss": "did:plc:test\u{0000}attacker", 566 "sub": "did:plc:test\u{202E}rekatta", 567 "aud": "did:web:test.pds", 568 "iat": Utc::now().timestamp(), 569 "exp": Utc::now().timestamp() + 3600, 570 "jti": "unicode-injection", 571 "scope": SCOPE_ACCESS 572 }); 573 574 let token = create_custom_jwt(&header, &claims, &key_bytes); 575 576 let result = verify_access_token(&token, &key_bytes); 577 if result.is_ok() { 578 let data = result.unwrap(); 579 assert!(!data.claims.sub.contains('\0'), "Null bytes in claims should be sanitized or rejected"); 580 } 581} 582 583#[test] 584fn test_jwt_security_signature_verification_is_constant_time() { 585 let key_bytes = generate_user_key(); 586 let did = "did:plc:test"; 587 588 let valid_token = create_access_token(did, &key_bytes).expect("create token"); 589 590 let parts: Vec<&str> = valid_token.split('.').collect(); 591 let mut almost_valid = URL_SAFE_NO_PAD.decode(parts[2]).unwrap(); 592 almost_valid[0] ^= 1; 593 let almost_valid_sig = URL_SAFE_NO_PAD.encode(&almost_valid); 594 let almost_valid_token = format!("{}.{}.{}", parts[0], parts[1], almost_valid_sig); 595 596 let completely_invalid_sig = URL_SAFE_NO_PAD.encode(&[0xFFu8; 64]); 597 let completely_invalid_token = format!("{}.{}.{}", parts[0], parts[1], completely_invalid_sig); 598 599 let _result1 = verify_access_token(&almost_valid_token, &key_bytes); 600 let _result2 = verify_access_token(&completely_invalid_token, &key_bytes); 601 602 assert!(true, "Signature verification should use constant-time comparison (timing attack prevention)"); 603} 604 605#[test] 606fn test_jwt_security_valid_scopes_accepted() { 607 let key_bytes = generate_user_key(); 608 let did = "did:plc:test"; 609 610 let valid_scopes = vec![ 611 SCOPE_ACCESS, 612 SCOPE_APP_PASS, 613 SCOPE_APP_PASS_PRIVILEGED, 614 ]; 615 616 for scope in valid_scopes { 617 let header = json!({ 618 "alg": "ES256K", 619 "typ": TOKEN_TYPE_ACCESS 620 }); 621 let claims = json!({ 622 "iss": did, 623 "sub": did, 624 "aud": "did:web:test.pds", 625 "iat": Utc::now().timestamp(), 626 "exp": Utc::now().timestamp() + 3600, 627 "jti": format!("scope-test-{}", scope), 628 "scope": scope 629 }); 630 631 let token = create_custom_jwt(&header, &claims, &key_bytes); 632 633 let result = verify_access_token(&token, &key_bytes); 634 assert!(result.is_ok(), "Valid scope '{}' should be accepted", scope); 635 } 636} 637 638#[test] 639fn test_jwt_security_refresh_token_scope_rejected_as_access() { 640 let key_bytes = generate_user_key(); 641 let did = "did:plc:test"; 642 643 let header = json!({ 644 "alg": "ES256K", 645 "typ": TOKEN_TYPE_ACCESS 646 }); 647 let claims = json!({ 648 "iss": did, 649 "sub": did, 650 "aud": "did:web:test.pds", 651 "iat": Utc::now().timestamp(), 652 "exp": Utc::now().timestamp() + 3600, 653 "jti": "refresh-scope-access-typ", 654 "scope": SCOPE_REFRESH 655 }); 656 657 let token = create_custom_jwt(&header, &claims, &key_bytes); 658 659 let result = verify_access_token(&token, &key_bytes); 660 assert!(result.is_err(), "Refresh scope with access token type must be rejected"); 661} 662 663#[test] 664fn test_jwt_security_get_did_extraction_safe() { 665 let key_bytes = generate_user_key(); 666 let did = "did:plc:legitimate"; 667 668 let token = create_access_token(did, &key_bytes).expect("create token"); 669 let extracted = get_did_from_token(&token).expect("extract did"); 670 assert_eq!(extracted, did); 671 672 assert!(get_did_from_token("invalid").is_err()); 673 assert!(get_did_from_token("a.b").is_err()); 674 assert!(get_did_from_token("").is_err()); 675 676 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K"}"#); 677 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:iss","sub":"did:plc:sub"}"#); 678 let fake_sig = URL_SAFE_NO_PAD.encode(&[0u8; 64]); 679 let unverified_token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig); 680 681 let extracted_unsafe = get_did_from_token(&unverified_token).expect("extract unsafe"); 682 assert_eq!(extracted_unsafe, "did:plc:sub", "get_did_from_token extracts sub without verification (by design for lookup)"); 683} 684 685#[test] 686fn test_jwt_security_get_jti_extraction_safe() { 687 let key_bytes = generate_user_key(); 688 let did = "did:plc:test"; 689 690 let token = create_access_token(did, &key_bytes).expect("create token"); 691 let jti = get_jti_from_token(&token).expect("extract jti"); 692 assert!(!jti.is_empty()); 693 694 assert!(get_jti_from_token("invalid").is_err()); 695 assert!(get_jti_from_token("a.b").is_err()); 696 697 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K"}"#); 698 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:test"}"#); 699 let fake_sig = URL_SAFE_NO_PAD.encode(&[0u8; 64]); 700 let no_jti_token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig); 701 702 assert!(get_jti_from_token(&no_jti_token).is_err(), "Missing jti should error"); 703} 704 705#[test] 706fn test_jwt_security_key_from_invalid_bytes_rejected() { 707 let invalid_keys: Vec<&[u8]> = vec![ 708 &[], 709 &[0u8; 31], 710 &[0u8; 33], 711 &[0xFFu8; 32], 712 ]; 713 714 for key in invalid_keys { 715 let result = create_access_token("did:plc:test", key); 716 if result.is_ok() { 717 let token = result.unwrap(); 718 let verify_result = verify_access_token(&token, key); 719 if verify_result.is_err() { 720 continue; 721 } 722 } 723 } 724} 725 726#[test] 727fn test_jwt_security_boundary_exp_values() { 728 let key_bytes = generate_user_key(); 729 let did = "did:plc:test"; 730 731 let header = json!({ 732 "alg": "ES256K", 733 "typ": TOKEN_TYPE_ACCESS 734 }); 735 736 let now = Utc::now().timestamp(); 737 let just_expired = json!({ 738 "iss": did, 739 "sub": did, 740 "aud": "did:web:test.pds", 741 "iat": now - 10, 742 "exp": now - 1, 743 "jti": "just-expired", 744 "scope": SCOPE_ACCESS 745 }); 746 747 let token1 = create_custom_jwt(&header, &just_expired, &key_bytes); 748 assert!(verify_access_token(&token1, &key_bytes).is_err(), "Just expired token must be rejected"); 749 750 let expires_exactly_now = json!({ 751 "iss": did, 752 "sub": did, 753 "aud": "did:web:test.pds", 754 "iat": now - 10, 755 "exp": now, 756 "jti": "expires-now", 757 "scope": SCOPE_ACCESS 758 }); 759 760 let token2 = create_custom_jwt(&header, &expires_exactly_now, &key_bytes); 761 let result2 = verify_access_token(&token2, &key_bytes); 762 assert!(result2.is_err() || result2.is_ok(), "Token expiring exactly now is a boundary case - either behavior is acceptable"); 763} 764 765#[test] 766fn test_jwt_security_very_long_exp_handled() { 767 let key_bytes = generate_user_key(); 768 let did = "did:plc:test"; 769 770 let header = json!({ 771 "alg": "ES256K", 772 "typ": TOKEN_TYPE_ACCESS 773 }); 774 let claims = json!({ 775 "iss": did, 776 "sub": did, 777 "aud": "did:web:test.pds", 778 "iat": Utc::now().timestamp(), 779 "exp": i64::MAX, 780 "jti": "far-future", 781 "scope": SCOPE_ACCESS 782 }); 783 784 let token = create_custom_jwt(&header, &claims, &key_bytes); 785 786 let _result = verify_access_token(&token, &key_bytes); 787} 788 789#[test] 790fn test_jwt_security_negative_timestamps_handled() { 791 let key_bytes = generate_user_key(); 792 let did = "did:plc:test"; 793 794 let header = json!({ 795 "alg": "ES256K", 796 "typ": TOKEN_TYPE_ACCESS 797 }); 798 let claims = json!({ 799 "iss": did, 800 "sub": did, 801 "aud": "did:web:test.pds", 802 "iat": -1000000000i64, 803 "exp": Utc::now().timestamp() + 3600, 804 "jti": "negative-iat", 805 "scope": SCOPE_ACCESS 806 }); 807 808 let token = create_custom_jwt(&header, &claims, &key_bytes); 809 810 let _result = verify_access_token(&token, &key_bytes); 811} 812 813#[tokio::test] 814async fn test_jwt_security_server_rejects_forged_session_token() { 815 let url = base_url().await; 816 let http_client = client(); 817 818 let key_bytes = generate_user_key(); 819 let did = "did:plc:fake-user"; 820 821 let forged_token = create_access_token(did, &key_bytes).expect("create forged token"); 822 823 let res = http_client 824 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 825 .header("Authorization", format!("Bearer {}", forged_token)) 826 .send() 827 .await 828 .unwrap(); 829 830 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Forged session token must be rejected"); 831} 832 833#[tokio::test] 834async fn test_jwt_security_server_rejects_expired_token() { 835 let url = base_url().await; 836 let http_client = client(); 837 838 let (access_jwt, _did) = create_account_and_login(&http_client).await; 839 840 let parts: Vec<&str> = access_jwt.split('.').collect(); 841 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap(); 842 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap(); 843 844 payload["exp"] = json!(Utc::now().timestamp() - 3600); 845 846 let modified_payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 847 let tampered_token = format!("{}.{}.{}", parts[0], modified_payload, parts[2]); 848 849 let res = http_client 850 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 851 .header("Authorization", format!("Bearer {}", tampered_token)) 852 .send() 853 .await 854 .unwrap(); 855 856 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Tampered/expired token must be rejected"); 857} 858 859#[tokio::test] 860async fn test_jwt_security_server_rejects_tampered_did() { 861 let url = base_url().await; 862 let http_client = client(); 863 864 let (access_jwt, _did) = create_account_and_login(&http_client).await; 865 866 let parts: Vec<&str> = access_jwt.split('.').collect(); 867 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap(); 868 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap(); 869 870 payload["sub"] = json!("did:plc:attacker"); 871 payload["iss"] = json!("did:plc:attacker"); 872 873 let modified_payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 874 let tampered_token = format!("{}.{}.{}", parts[0], modified_payload, parts[2]); 875 876 let res = http_client 877 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 878 .header("Authorization", format!("Bearer {}", tampered_token)) 879 .send() 880 .await 881 .unwrap(); 882 883 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "DID-tampered token must be rejected"); 884} 885 886#[tokio::test] 887async fn test_jwt_security_refresh_token_replay_protection() { 888 let url = base_url().await; 889 let http_client = client(); 890 891 let ts = Utc::now().timestamp_millis(); 892 let handle = format!("rt-replay-jwt-{}", ts); 893 let email = format!("rt-replay-jwt-{}@example.com", ts); 894 let password = "test-password-123"; 895 896 let create_res = http_client 897 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 898 .json(&json!({ 899 "handle": handle, 900 "email": email, 901 "password": password 902 })) 903 .send() 904 .await 905 .unwrap(); 906 907 assert_eq!(create_res.status(), StatusCode::OK); 908 let account: Value = create_res.json().await.unwrap(); 909 let did = account["did"].as_str().unwrap(); 910 911 let conn_str = get_db_connection_string().await; 912 let pool = sqlx::postgres::PgPoolOptions::new() 913 .max_connections(2) 914 .connect(&conn_str) 915 .await 916 .expect("Failed to connect to test database"); 917 918 let verification_code: String = sqlx::query_scalar!( 919 "SELECT email_confirmation_code FROM users WHERE did = $1", 920 did 921 ) 922 .fetch_one(&pool) 923 .await 924 .expect("Failed to get verification code") 925 .expect("No verification code found"); 926 927 let confirm_res = http_client 928 .post(format!("{}/xrpc/com.atproto.server.confirmSignup", url)) 929 .json(&json!({ 930 "did": did, 931 "verificationCode": verification_code 932 })) 933 .send() 934 .await 935 .unwrap(); 936 937 assert_eq!(confirm_res.status(), StatusCode::OK); 938 let confirmed: Value = confirm_res.json().await.unwrap(); 939 let refresh_jwt = confirmed["refreshJwt"].as_str().unwrap().to_string(); 940 941 let first_refresh = http_client 942 .post(format!("{}/xrpc/com.atproto.server.refreshSession", url)) 943 .header("Authorization", format!("Bearer {}", refresh_jwt)) 944 .send() 945 .await 946 .unwrap(); 947 948 assert_eq!(first_refresh.status(), StatusCode::OK, "First refresh should succeed"); 949 950 let replay_res = http_client 951 .post(format!("{}/xrpc/com.atproto.server.refreshSession", url)) 952 .header("Authorization", format!("Bearer {}", refresh_jwt)) 953 .send() 954 .await 955 .unwrap(); 956 957 assert_eq!(replay_res.status(), StatusCode::UNAUTHORIZED, "Refresh token replay must be rejected"); 958} 959 960#[tokio::test] 961async fn test_jwt_security_authorization_header_formats() { 962 let url = base_url().await; 963 let http_client = client(); 964 965 let (access_jwt, _did) = create_account_and_login(&http_client).await; 966 967 let valid_res = http_client 968 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 969 .header("Authorization", format!("Bearer {}", access_jwt)) 970 .send() 971 .await 972 .unwrap(); 973 assert_eq!(valid_res.status(), StatusCode::OK, "Valid Bearer format should work"); 974 975 let lowercase_res = http_client 976 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 977 .header("Authorization", format!("bearer {}", access_jwt)) 978 .send() 979 .await 980 .unwrap(); 981 assert_eq!(lowercase_res.status(), StatusCode::OK, "Lowercase 'bearer' should be accepted (RFC 7235 case-insensitivity)"); 982 983 let basic_res = http_client 984 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 985 .header("Authorization", format!("Basic {}", access_jwt)) 986 .send() 987 .await 988 .unwrap(); 989 assert_eq!(basic_res.status(), StatusCode::UNAUTHORIZED, "Basic scheme must be rejected"); 990 991 let no_scheme_res = http_client 992 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 993 .header("Authorization", &access_jwt) 994 .send() 995 .await 996 .unwrap(); 997 assert_eq!(no_scheme_res.status(), StatusCode::UNAUTHORIZED, "Missing scheme must be rejected"); 998 999 let empty_token_res = http_client 1000 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1001 .header("Authorization", "Bearer ") 1002 .send() 1003 .await 1004 .unwrap(); 1005 assert_eq!(empty_token_res.status(), StatusCode::UNAUTHORIZED, "Empty token must be rejected"); 1006} 1007 1008#[tokio::test] 1009async fn test_jwt_security_deleted_session_rejected() { 1010 let url = base_url().await; 1011 let http_client = client(); 1012 1013 let (access_jwt, _did) = create_account_and_login(&http_client).await; 1014 1015 let get_res = http_client 1016 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1017 .header("Authorization", format!("Bearer {}", access_jwt)) 1018 .send() 1019 .await 1020 .unwrap(); 1021 assert_eq!(get_res.status(), StatusCode::OK, "Token should work before logout"); 1022 1023 let logout_res = http_client 1024 .post(format!("{}/xrpc/com.atproto.server.deleteSession", url)) 1025 .header("Authorization", format!("Bearer {}", access_jwt)) 1026 .send() 1027 .await 1028 .unwrap(); 1029 assert_eq!(logout_res.status(), StatusCode::OK); 1030 1031 let after_logout_res = http_client 1032 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1033 .header("Authorization", format!("Bearer {}", access_jwt)) 1034 .send() 1035 .await 1036 .unwrap(); 1037 assert_eq!(after_logout_res.status(), StatusCode::UNAUTHORIZED, "Token must be rejected after logout"); 1038} 1039 1040#[tokio::test] 1041async fn test_jwt_security_deactivated_account_rejected() { 1042 let url = base_url().await; 1043 let http_client = client(); 1044 1045 let (access_jwt, _did) = create_account_and_login(&http_client).await; 1046 1047 let deact_res = http_client 1048 .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url)) 1049 .header("Authorization", format!("Bearer {}", access_jwt)) 1050 .json(&json!({})) 1051 .send() 1052 .await 1053 .unwrap(); 1054 assert_eq!(deact_res.status(), StatusCode::OK); 1055 1056 let get_res = http_client 1057 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 1058 .header("Authorization", format!("Bearer {}", access_jwt)) 1059 .send() 1060 .await 1061 .unwrap(); 1062 assert_eq!(get_res.status(), StatusCode::UNAUTHORIZED, "Deactivated account token must be rejected"); 1063 1064 let body: Value = get_res.json().await.unwrap(); 1065 assert_eq!(body["error"], "AccountDeactivated"); 1066}