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