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