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