forked from
lewis.moe/bspds-sandbox
I've been saying "PDSes seem easy enough, they're what, some CRUD to a db? I can do that in my sleep". well i'm sleeping rn so let's go
1#![allow(unused_imports)]
2mod common;
3use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
4use chrono::{Duration, Utc};
5use common::{base_url, client, create_account_and_login, get_test_db_pool};
6use k256::SecretKey;
7use k256::ecdsa::{Signature, SigningKey, signature::Signer};
8use rand::rngs::OsRng;
9use reqwest::StatusCode;
10use serde_json::{Value, json};
11use sha2::{Digest, Sha256};
12use tranquil_pds::auth::{
13 self, SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH,
14 TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, create_access_token,
15 create_refresh_token, create_service_token, get_did_from_token, get_jti_from_token,
16 verify_access_token, verify_refresh_token, verify_token,
17};
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_signature_attacks() {
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
47 let forged_signature = URL_SAFE_NO_PAD.encode([0u8; 64]);
48 let forged_token = format!("{}.{}.{}", parts[0], parts[1], forged_signature);
49 let result = verify_access_token(&forged_token, &key_bytes);
50 assert!(result.is_err(), "Forged signature must be rejected");
51 assert!(
52 result
53 .err()
54 .unwrap()
55 .to_string()
56 .to_lowercase()
57 .contains("signature")
58 );
59
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 assert!(
66 verify_access_token(&modified_token, &key_bytes).is_err(),
67 "Modified payload must be rejected"
68 );
69
70 let sig_bytes = URL_SAFE_NO_PAD.decode(parts[2]).unwrap();
71 let truncated_sig = URL_SAFE_NO_PAD.encode(&sig_bytes[..32]);
72 let truncated_token = format!("{}.{}.{}", parts[0], parts[1], truncated_sig);
73 assert!(
74 verify_access_token(&truncated_token, &key_bytes).is_err(),
75 "Truncated signature must be rejected"
76 );
77
78 let mut extended_sig = sig_bytes.clone();
79 extended_sig.extend_from_slice(&[0u8; 32]);
80 let extended_token = format!(
81 "{}.{}.{}",
82 parts[0],
83 parts[1],
84 URL_SAFE_NO_PAD.encode(&extended_sig)
85 );
86 assert!(
87 verify_access_token(&extended_token, &key_bytes).is_err(),
88 "Extended signature must be rejected"
89 );
90
91 let key_bytes_user2 = generate_user_key();
92 assert!(
93 verify_access_token(&token, &key_bytes_user2).is_err(),
94 "Token signed with different key must be rejected"
95 );
96}
97
98#[test]
99fn test_algorithm_substitution_attacks() {
100 let key_bytes = generate_user_key();
101 let did = "did:plc:test";
102
103 let none_header = json!({ "alg": "none", "typ": TOKEN_TYPE_ACCESS });
104 let claims = json!({
105 "iss": did, "sub": did, "aud": "did:web:test.pds",
106 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
107 "jti": "attack-token", "scope": SCOPE_ACCESS
108 });
109 let none_token = create_unsigned_jwt(&none_header, &claims);
110 assert!(
111 verify_access_token(&none_token, &key_bytes).is_err(),
112 "Algorithm 'none' must be rejected"
113 );
114
115 let hs256_header = json!({ "alg": "HS256", "typ": TOKEN_TYPE_ACCESS });
116 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&hs256_header).unwrap());
117 let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&claims).unwrap());
118 use hmac::{Hmac, Mac};
119 type HmacSha256 = Hmac<Sha256>;
120 let message = format!("{}.{}", header_b64, claims_b64);
121 let mut mac = HmacSha256::new_from_slice(&key_bytes).unwrap();
122 mac.update(message.as_bytes());
123 let hmac_sig = mac.finalize().into_bytes();
124 let hs256_token = format!("{}.{}", message, URL_SAFE_NO_PAD.encode(hmac_sig));
125 assert!(
126 verify_access_token(&hs256_token, &key_bytes).is_err(),
127 "HS256 substitution must be rejected"
128 );
129
130 for (alg, sig_len) in [("RS256", 256), ("ES256", 64)] {
131 let header = json!({ "alg": alg, "typ": TOKEN_TYPE_ACCESS });
132 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
133 let fake_sig = URL_SAFE_NO_PAD.encode(vec![1u8; sig_len]);
134 let token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
135 assert!(
136 verify_access_token(&token, &key_bytes).is_err(),
137 "{} substitution must be rejected",
138 alg
139 );
140 }
141}
142
143#[test]
144fn test_token_type_confusion() {
145 let key_bytes = generate_user_key();
146 let did = "did:plc:test";
147
148 let refresh_token = create_refresh_token(did, &key_bytes).expect("create refresh token");
149 let result = verify_access_token(&refresh_token, &key_bytes);
150 assert!(result.is_err(), "Refresh token as access must be rejected");
151 assert!(
152 result
153 .err()
154 .unwrap()
155 .to_string()
156 .contains("Invalid token type")
157 );
158
159 let access_token = create_access_token(did, &key_bytes).expect("create access token");
160 let result = verify_refresh_token(&access_token, &key_bytes);
161 assert!(result.is_err(), "Access token as refresh must be rejected");
162 assert!(
163 result
164 .err()
165 .unwrap()
166 .to_string()
167 .contains("Invalid token type")
168 );
169
170 let service_token =
171 create_service_token(did, "did:web:target", "com.example.method", &key_bytes).unwrap();
172 assert!(
173 verify_access_token(&service_token, &key_bytes).is_err(),
174 "Service token as access must be rejected"
175 );
176}
177
178#[test]
179fn test_scope_validation() {
180 let key_bytes = generate_user_key();
181 let did = "did:plc:test";
182 let header = json!({ "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS });
183
184 let invalid_scope = json!({
185 "iss": did, "sub": did, "aud": "did:web:test.pds",
186 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
187 "jti": "test", "scope": "admin.all"
188 });
189 let result = verify_access_token(
190 &create_custom_jwt(&header, &invalid_scope, &key_bytes),
191 &key_bytes,
192 );
193 assert!(
194 result.is_err()
195 && result
196 .err()
197 .unwrap()
198 .to_string()
199 .contains("Invalid token scope")
200 );
201
202 let empty_scope = json!({
203 "iss": did, "sub": did, "aud": "did:web:test.pds",
204 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
205 "jti": "test", "scope": ""
206 });
207 assert!(
208 verify_access_token(
209 &create_custom_jwt(&header, &empty_scope, &key_bytes),
210 &key_bytes
211 )
212 .is_err()
213 );
214
215 let missing_scope = json!({
216 "iss": did, "sub": did, "aud": "did:web:test.pds",
217 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
218 "jti": "test"
219 });
220 assert!(
221 verify_access_token(
222 &create_custom_jwt(&header, &missing_scope, &key_bytes),
223 &key_bytes
224 )
225 .is_err()
226 );
227
228 for scope in [SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED] {
229 let claims = json!({
230 "iss": did, "sub": did, "aud": "did:web:test.pds",
231 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
232 "jti": "test", "scope": scope
233 });
234 assert!(
235 verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes)
236 .is_ok()
237 );
238 }
239
240 let refresh_scope = json!({
241 "iss": did, "sub": did, "aud": "did:web:test.pds",
242 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
243 "jti": "test", "scope": SCOPE_REFRESH
244 });
245 assert!(
246 verify_access_token(
247 &create_custom_jwt(&header, &refresh_scope, &key_bytes),
248 &key_bytes
249 )
250 .is_err()
251 );
252}
253
254#[test]
255fn test_expiration_and_timing() {
256 let key_bytes = generate_user_key();
257 let did = "did:plc:test";
258 let header = json!({ "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS });
259 let now = Utc::now().timestamp();
260
261 let expired = json!({
262 "iss": did, "sub": did, "aud": "did:web:test.pds",
263 "iat": now - 7200, "exp": now - 3600, "jti": "test", "scope": SCOPE_ACCESS
264 });
265 let result = verify_access_token(
266 &create_custom_jwt(&header, &expired, &key_bytes),
267 &key_bytes,
268 );
269 assert!(result.is_err() && result.err().unwrap().to_string().contains("expired"));
270
271 let future_iat = json!({
272 "iss": did, "sub": did, "aud": "did:web:test.pds",
273 "iat": now + 60, "exp": now + 7200, "jti": "test", "scope": SCOPE_ACCESS
274 });
275 assert!(
276 verify_access_token(
277 &create_custom_jwt(&header, &future_iat, &key_bytes),
278 &key_bytes
279 )
280 .is_ok()
281 );
282
283 let just_expired = json!({
284 "iss": did, "sub": did, "aud": "did:web:test.pds",
285 "iat": now - 10, "exp": now - 1, "jti": "test", "scope": SCOPE_ACCESS
286 });
287 assert!(
288 verify_access_token(
289 &create_custom_jwt(&header, &just_expired, &key_bytes),
290 &key_bytes
291 )
292 .is_err()
293 );
294
295 let far_future = json!({
296 "iss": did, "sub": did, "aud": "did:web:test.pds",
297 "iat": now, "exp": i64::MAX, "jti": "test", "scope": SCOPE_ACCESS
298 });
299 let _ = verify_access_token(
300 &create_custom_jwt(&header, &far_future, &key_bytes),
301 &key_bytes,
302 );
303
304 let negative_iat = json!({
305 "iss": did, "sub": did, "aud": "did:web:test.pds",
306 "iat": -1000000000i64, "exp": now + 3600, "jti": "test", "scope": SCOPE_ACCESS
307 });
308 let _ = verify_access_token(
309 &create_custom_jwt(&header, &negative_iat, &key_bytes),
310 &key_bytes,
311 );
312}
313
314#[test]
315fn test_malformed_tokens() {
316 let key_bytes = generate_user_key();
317
318 for token in [
319 "",
320 "not-a-token",
321 "one.two",
322 "one.two.three.four",
323 "....",
324 "eyJhbGciOiJFUzI1NksifQ",
325 "eyJhbGciOiJFUzI1NksifQ.",
326 "eyJhbGciOiJFUzI1NksifQ..",
327 ".eyJzdWIiOiJ0ZXN0In0.",
328 "!!invalid-base64!!.eyJzdWIiOiJ0ZXN0In0.sig",
329 ] {
330 assert!(
331 verify_access_token(token, &key_bytes).is_err(),
332 "Malformed token must be rejected"
333 );
334 }
335
336 let invalid_header = URL_SAFE_NO_PAD.encode("{not valid json}");
337 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"sub":"test"}"#);
338 let fake_sig = URL_SAFE_NO_PAD.encode([1u8; 64]);
339 assert!(
340 verify_access_token(
341 &format!("{}.{}.{}", invalid_header, claims_b64, fake_sig),
342 &key_bytes
343 )
344 .is_err()
345 );
346
347 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K","typ":"at+jwt"}"#);
348 let invalid_claims = URL_SAFE_NO_PAD.encode("{not valid json}");
349 assert!(
350 verify_access_token(
351 &format!("{}.{}.{}", header_b64, invalid_claims, fake_sig),
352 &key_bytes
353 )
354 .is_err()
355 );
356}
357
358#[test]
359fn test_claim_validation() {
360 let key_bytes = generate_user_key();
361 let did = "did:plc:test";
362 let header = json!({ "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS });
363
364 let missing_exp = json!({
365 "iss": did, "sub": did, "aud": "did:web:test",
366 "iat": Utc::now().timestamp(), "scope": SCOPE_ACCESS
367 });
368 assert!(
369 verify_access_token(
370 &create_custom_jwt(&header, &missing_exp, &key_bytes),
371 &key_bytes
372 )
373 .is_err()
374 );
375
376 let missing_iat = json!({
377 "iss": did, "sub": did, "aud": "did:web:test",
378 "exp": Utc::now().timestamp() + 3600, "scope": SCOPE_ACCESS
379 });
380 assert!(
381 verify_access_token(
382 &create_custom_jwt(&header, &missing_iat, &key_bytes),
383 &key_bytes
384 )
385 .is_err()
386 );
387
388 let missing_sub = json!({
389 "iss": did, "aud": "did:web:test",
390 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, "scope": SCOPE_ACCESS
391 });
392 assert!(
393 verify_access_token(
394 &create_custom_jwt(&header, &missing_sub, &key_bytes),
395 &key_bytes
396 )
397 .is_err()
398 );
399
400 let wrong_types = json!({
401 "iss": 12345, "sub": ["did:plc:test"], "aud": {"url": "did:web:test"},
402 "iat": "not a number", "exp": "also not a number", "jti": null, "scope": SCOPE_ACCESS
403 });
404 assert!(
405 verify_access_token(
406 &create_custom_jwt(&header, &wrong_types, &key_bytes),
407 &key_bytes
408 )
409 .is_err()
410 );
411
412 let unicode_injection = json!({
413 "iss": "did:plc:test\u{0000}attacker", "sub": "did:plc:test\u{202E}rekatta",
414 "aud": "did:web:test.pds", "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
415 "jti": "test", "scope": SCOPE_ACCESS
416 });
417 if let Ok(data) = verify_access_token(
418 &create_custom_jwt(&header, &unicode_injection, &key_bytes),
419 &key_bytes,
420 ) {
421 assert!(!data.claims.sub.contains('\0'));
422 }
423}
424
425#[test]
426fn test_did_and_jti_extraction() {
427 let key_bytes = generate_user_key();
428 let did = "did:plc:legitimate";
429 let token = create_access_token(did, &key_bytes).expect("create token");
430
431 assert_eq!(get_did_from_token(&token).unwrap(), did);
432 assert!(get_did_from_token("invalid").is_err());
433 assert!(get_did_from_token("a.b").is_err());
434 assert!(get_did_from_token("").is_err());
435
436 let jti = get_jti_from_token(&token).unwrap();
437 assert!(!jti.is_empty());
438 assert!(get_jti_from_token("invalid").is_err());
439
440 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K"}"#);
441 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:iss","sub":"did:plc:sub"}"#);
442 let fake_sig = URL_SAFE_NO_PAD.encode([0u8; 64]);
443 let unverified = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
444 assert_eq!(get_did_from_token(&unverified).unwrap(), "did:plc:sub");
445
446 let no_jti_claims = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:test"}"#);
447 assert!(get_jti_from_token(&format!("{}.{}.{}", header_b64, no_jti_claims, fake_sig)).is_err());
448}
449
450#[test]
451fn test_header_injection_and_constant_time() {
452 let key_bytes = generate_user_key();
453 let did = "did:plc:test";
454
455 let header = json!({
456 "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS,
457 "kid": "../../../../../../etc/passwd", "jku": "https://attacker.com/keys"
458 });
459 let claims = json!({
460 "iss": did, "sub": did, "aud": "did:web:test.pds",
461 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
462 "jti": "test", "scope": SCOPE_ACCESS
463 });
464 assert!(
465 verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes).is_ok()
466 );
467
468 let valid_token = create_access_token(did, &key_bytes).expect("create token");
469 let parts: Vec<&str> = valid_token.split('.').collect();
470 let mut almost_valid = URL_SAFE_NO_PAD.decode(parts[2]).unwrap();
471 almost_valid[0] ^= 1;
472 let almost_valid_token = format!(
473 "{}.{}.{}",
474 parts[0],
475 parts[1],
476 URL_SAFE_NO_PAD.encode(&almost_valid)
477 );
478 let completely_invalid_token = format!(
479 "{}.{}.{}",
480 parts[0],
481 parts[1],
482 URL_SAFE_NO_PAD.encode([0xFFu8; 64])
483 );
484 let _ = verify_access_token(&almost_valid_token, &key_bytes);
485 let _ = verify_access_token(&completely_invalid_token, &key_bytes);
486}
487
488#[tokio::test]
489async fn test_server_rejects_invalid_tokens() {
490 let url = base_url().await;
491 let http_client = client();
492
493 let key_bytes = generate_user_key();
494 let forged_token = create_access_token("did:plc:fake-user", &key_bytes).unwrap();
495 let res = http_client
496 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
497 .header("Authorization", format!("Bearer {}", forged_token))
498 .send()
499 .await
500 .unwrap();
501 assert_eq!(
502 res.status(),
503 StatusCode::UNAUTHORIZED,
504 "Forged token must be rejected"
505 );
506
507 let (access_jwt, _did) = create_account_and_login(&http_client).await;
508 let parts: Vec<&str> = access_jwt.split('.').collect();
509 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap();
510 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
511
512 payload["exp"] = json!(Utc::now().timestamp() - 3600);
513 let expired_token = format!(
514 "{}.{}.{}",
515 parts[0],
516 URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()),
517 parts[2]
518 );
519 let res = http_client
520 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
521 .header("Authorization", format!("Bearer {}", expired_token))
522 .send()
523 .await
524 .unwrap();
525 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
526
527 let mut tampered_payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
528 tampered_payload["sub"] = json!("did:plc:attacker");
529 tampered_payload["iss"] = json!("did:plc:attacker");
530 let tampered_token = format!(
531 "{}.{}.{}",
532 parts[0],
533 URL_SAFE_NO_PAD.encode(serde_json::to_string(&tampered_payload).unwrap()),
534 parts[2]
535 );
536 let res = http_client
537 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
538 .header("Authorization", format!("Bearer {}", tampered_token))
539 .send()
540 .await
541 .unwrap();
542 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
543}
544
545#[tokio::test]
546async fn test_authorization_header_formats() {
547 let url = base_url().await;
548 let http_client = client();
549 let (access_jwt, _did) = create_account_and_login(&http_client).await;
550
551 let res = http_client
552 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
553 .header("Authorization", format!("Bearer {}", access_jwt))
554 .send()
555 .await
556 .unwrap();
557 assert_eq!(res.status(), StatusCode::OK);
558
559 let res = http_client
560 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
561 .header("Authorization", format!("bearer {}", access_jwt))
562 .send()
563 .await
564 .unwrap();
565 assert_eq!(res.status(), StatusCode::OK);
566
567 let res = http_client
568 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
569 .header("Authorization", format!("Basic {}", access_jwt))
570 .send()
571 .await
572 .unwrap();
573 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
574
575 let res = http_client
576 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
577 .header("Authorization", &access_jwt)
578 .send()
579 .await
580 .unwrap();
581 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
582
583 let res = http_client
584 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
585 .header("Authorization", "Bearer ")
586 .send()
587 .await
588 .unwrap();
589 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
590}
591
592#[tokio::test]
593async fn test_session_lifecycle_security() {
594 let url = base_url().await;
595 let http_client = client();
596 let (access_jwt, _did) = create_account_and_login(&http_client).await;
597
598 let res = http_client
599 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
600 .header("Authorization", format!("Bearer {}", access_jwt))
601 .send()
602 .await
603 .unwrap();
604 assert_eq!(res.status(), StatusCode::OK);
605
606 let logout = http_client
607 .post(format!("{}/xrpc/com.atproto.server.deleteSession", url))
608 .header("Authorization", format!("Bearer {}", access_jwt))
609 .send()
610 .await
611 .unwrap();
612 assert_eq!(logout.status(), StatusCode::OK);
613
614 let res = http_client
615 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
616 .header("Authorization", format!("Bearer {}", access_jwt))
617 .send()
618 .await
619 .unwrap();
620 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
621}
622
623#[tokio::test]
624async fn test_deactivated_account_behavior() {
625 let url = base_url().await;
626 let http_client = client();
627 let (access_jwt, _did) = create_account_and_login(&http_client).await;
628
629 let deact = http_client
630 .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url))
631 .header("Authorization", format!("Bearer {}", access_jwt))
632 .json(&json!({}))
633 .send()
634 .await
635 .unwrap();
636 assert_eq!(deact.status(), StatusCode::OK);
637
638 let res = http_client
639 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
640 .header("Authorization", format!("Bearer {}", access_jwt))
641 .send()
642 .await
643 .unwrap();
644 assert_eq!(res.status(), StatusCode::OK);
645 let body: Value = res.json().await.unwrap();
646 assert_eq!(body["active"], false);
647
648 let post_res = http_client
649 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
650 .header("Authorization", format!("Bearer {}", access_jwt))
651 .json(&json!({
652 "repo": _did,
653 "collection": "app.bsky.feed.post",
654 "record": {
655 "$type": "app.bsky.feed.post",
656 "text": "test",
657 "createdAt": "2024-01-01T00:00:00Z"
658 }
659 }))
660 .send()
661 .await
662 .unwrap();
663 assert_eq!(post_res.status(), StatusCode::UNAUTHORIZED);
664 let post_body: Value = post_res.json().await.unwrap();
665 assert_eq!(post_body["error"], "AccountDeactivated");
666}
667
668#[tokio::test]
669async fn test_refresh_token_replay_protection() {
670 let url = base_url().await;
671 let http_client = client();
672 let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
673 let handle = format!("rr{}", suffix);
674 let email = format!("rr{}@example.com", suffix);
675
676 let create_res = http_client
677 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
678 .json(&json!({ "handle": handle, "email": email, "password": "Testpass123!" }))
679 .send()
680 .await
681 .unwrap();
682 assert_eq!(create_res.status(), StatusCode::OK);
683 let account: Value = create_res.json().await.unwrap();
684 let did = account["did"].as_str().unwrap();
685
686 let pool = get_test_db_pool().await;
687 let body_text: String = sqlx::query_scalar!(
688 "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
689 did
690 ).fetch_one(pool).await.unwrap();
691 let lines: Vec<&str> = body_text.lines().collect();
692 let code = lines
693 .iter()
694 .enumerate()
695 .find(|(_, line)| line.contains("verification code is:") || line.contains("code is:"))
696 .and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string()))
697 .or_else(|| {
698 body_text
699 .split_whitespace()
700 .find(|word| word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3)
701 .map(|s| s.to_string())
702 })
703 .unwrap_or_else(|| body_text.clone());
704
705 let confirm = http_client
706 .post(format!("{}/xrpc/com.atproto.server.confirmSignup", url))
707 .json(&json!({ "did": did, "verificationCode": code }))
708 .send()
709 .await
710 .unwrap();
711 assert_eq!(confirm.status(), StatusCode::OK);
712 let confirmed: Value = confirm.json().await.unwrap();
713 let refresh_jwt = confirmed["refreshJwt"].as_str().unwrap().to_string();
714
715 let first = http_client
716 .post(format!("{}/xrpc/com.atproto.server.refreshSession", url))
717 .header("Authorization", format!("Bearer {}", refresh_jwt))
718 .send()
719 .await
720 .unwrap();
721 assert_eq!(first.status(), StatusCode::OK);
722
723 let replay = http_client
724 .post(format!("{}/xrpc/com.atproto.server.refreshSession", url))
725 .header("Authorization", format!("Bearer {}", refresh_jwt))
726 .send()
727 .await
728 .unwrap();
729 assert_eq!(replay.status(), StatusCode::UNAUTHORIZED);
730}