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