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