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