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