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_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!(result.err().unwrap().to_string().to_lowercase().contains("signature"));
52
53 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap();
54 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
55 payload["sub"] = json!("did:plc:attacker");
56 let modified_payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
57 let modified_token = format!("{}.{}.{}", parts[0], modified_payload, parts[2]);
58 assert!(verify_access_token(&modified_token, &key_bytes).is_err(), "Modified payload must be rejected");
59
60 let sig_bytes = URL_SAFE_NO_PAD.decode(parts[2]).unwrap();
61 let truncated_sig = URL_SAFE_NO_PAD.encode(&sig_bytes[..32]);
62 let truncated_token = format!("{}.{}.{}", parts[0], parts[1], truncated_sig);
63 assert!(verify_access_token(&truncated_token, &key_bytes).is_err(), "Truncated signature must be rejected");
64
65 let mut extended_sig = sig_bytes.clone();
66 extended_sig.extend_from_slice(&[0u8; 32]);
67 let extended_token = format!("{}.{}.{}", parts[0], parts[1], URL_SAFE_NO_PAD.encode(&extended_sig));
68 assert!(verify_access_token(&extended_token, &key_bytes).is_err(), "Extended signature must be rejected");
69
70 let key_bytes_user2 = generate_user_key();
71 assert!(verify_access_token(&token, &key_bytes_user2).is_err(), "Token signed with different key must be rejected");
72}
73
74#[test]
75fn test_algorithm_substitution_attacks() {
76 let key_bytes = generate_user_key();
77 let did = "did:plc:test";
78
79 let none_header = json!({ "alg": "none", "typ": TOKEN_TYPE_ACCESS });
80 let claims = json!({
81 "iss": did, "sub": did, "aud": "did:web:test.pds",
82 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
83 "jti": "attack-token", "scope": SCOPE_ACCESS
84 });
85 let none_token = create_unsigned_jwt(&none_header, &claims);
86 assert!(verify_access_token(&none_token, &key_bytes).is_err(), "Algorithm 'none' must be rejected");
87
88 let hs256_header = json!({ "alg": "HS256", "typ": TOKEN_TYPE_ACCESS });
89 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&hs256_header).unwrap());
90 let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&claims).unwrap());
91 use hmac::{Hmac, Mac};
92 type HmacSha256 = Hmac<Sha256>;
93 let message = format!("{}.{}", header_b64, claims_b64);
94 let mut mac = HmacSha256::new_from_slice(&key_bytes).unwrap();
95 mac.update(message.as_bytes());
96 let hmac_sig = mac.finalize().into_bytes();
97 let hs256_token = format!("{}.{}", message, URL_SAFE_NO_PAD.encode(&hmac_sig));
98 assert!(verify_access_token(&hs256_token, &key_bytes).is_err(), "HS256 substitution must be rejected");
99
100 for (alg, sig_len) in [("RS256", 256), ("ES256", 64)] {
101 let header = json!({ "alg": alg, "typ": TOKEN_TYPE_ACCESS });
102 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
103 let fake_sig = URL_SAFE_NO_PAD.encode(&vec![1u8; sig_len]);
104 let token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
105 assert!(verify_access_token(&token, &key_bytes).is_err(), "{} substitution must be rejected", alg);
106 }
107}
108
109#[test]
110fn test_token_type_confusion() {
111 let key_bytes = generate_user_key();
112 let did = "did:plc:test";
113
114 let refresh_token = create_refresh_token(did, &key_bytes).expect("create refresh token");
115 let result = verify_access_token(&refresh_token, &key_bytes);
116 assert!(result.is_err(), "Refresh token as access must be rejected");
117 assert!(result.err().unwrap().to_string().contains("Invalid token type"));
118
119 let access_token = create_access_token(did, &key_bytes).expect("create access token");
120 let result = verify_refresh_token(&access_token, &key_bytes);
121 assert!(result.is_err(), "Access token as refresh must be rejected");
122 assert!(result.err().unwrap().to_string().contains("Invalid token type"));
123
124 let service_token = create_service_token(did, "did:web:target", "com.example.method", &key_bytes).unwrap();
125 assert!(verify_access_token(&service_token, &key_bytes).is_err(), "Service token as access must be rejected");
126}
127
128#[test]
129fn test_scope_validation() {
130 let key_bytes = generate_user_key();
131 let did = "did:plc:test";
132 let header = json!({ "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS });
133
134 let invalid_scope = json!({
135 "iss": did, "sub": did, "aud": "did:web:test.pds",
136 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
137 "jti": "test", "scope": "admin.all"
138 });
139 let result = verify_access_token(&create_custom_jwt(&header, &invalid_scope, &key_bytes), &key_bytes);
140 assert!(result.is_err() && result.err().unwrap().to_string().contains("Invalid token scope"));
141
142 let empty_scope = json!({
143 "iss": did, "sub": did, "aud": "did:web:test.pds",
144 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
145 "jti": "test", "scope": ""
146 });
147 assert!(verify_access_token(&create_custom_jwt(&header, &empty_scope, &key_bytes), &key_bytes).is_err());
148
149 let missing_scope = json!({
150 "iss": did, "sub": did, "aud": "did:web:test.pds",
151 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
152 "jti": "test"
153 });
154 assert!(verify_access_token(&create_custom_jwt(&header, &missing_scope, &key_bytes), &key_bytes).is_err());
155
156 for scope in [SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED] {
157 let claims = json!({
158 "iss": did, "sub": did, "aud": "did:web:test.pds",
159 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
160 "jti": "test", "scope": scope
161 });
162 assert!(verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes).is_ok());
163 }
164
165 let refresh_scope = json!({
166 "iss": did, "sub": did, "aud": "did:web:test.pds",
167 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
168 "jti": "test", "scope": SCOPE_REFRESH
169 });
170 assert!(verify_access_token(&create_custom_jwt(&header, &refresh_scope, &key_bytes), &key_bytes).is_err());
171}
172
173#[test]
174fn test_expiration_and_timing() {
175 let key_bytes = generate_user_key();
176 let did = "did:plc:test";
177 let header = json!({ "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS });
178 let now = Utc::now().timestamp();
179
180 let expired = json!({
181 "iss": did, "sub": did, "aud": "did:web:test.pds",
182 "iat": now - 7200, "exp": now - 3600, "jti": "test", "scope": SCOPE_ACCESS
183 });
184 let result = verify_access_token(&create_custom_jwt(&header, &expired, &key_bytes), &key_bytes);
185 assert!(result.is_err() && result.err().unwrap().to_string().contains("expired"));
186
187 let future_iat = json!({
188 "iss": did, "sub": did, "aud": "did:web:test.pds",
189 "iat": now + 60, "exp": now + 7200, "jti": "test", "scope": SCOPE_ACCESS
190 });
191 assert!(verify_access_token(&create_custom_jwt(&header, &future_iat, &key_bytes), &key_bytes).is_ok());
192
193 let just_expired = json!({
194 "iss": did, "sub": did, "aud": "did:web:test.pds",
195 "iat": now - 10, "exp": now - 1, "jti": "test", "scope": SCOPE_ACCESS
196 });
197 assert!(verify_access_token(&create_custom_jwt(&header, &just_expired, &key_bytes), &key_bytes).is_err());
198
199 let far_future = json!({
200 "iss": did, "sub": did, "aud": "did:web:test.pds",
201 "iat": now, "exp": i64::MAX, "jti": "test", "scope": SCOPE_ACCESS
202 });
203 let _ = verify_access_token(&create_custom_jwt(&header, &far_future, &key_bytes), &key_bytes);
204
205 let negative_iat = json!({
206 "iss": did, "sub": did, "aud": "did:web:test.pds",
207 "iat": -1000000000i64, "exp": now + 3600, "jti": "test", "scope": SCOPE_ACCESS
208 });
209 let _ = verify_access_token(&create_custom_jwt(&header, &negative_iat, &key_bytes), &key_bytes);
210}
211
212#[test]
213fn test_malformed_tokens() {
214 let key_bytes = generate_user_key();
215
216 for token in ["", "not-a-token", "one.two", "one.two.three.four", "....",
217 "eyJhbGciOiJFUzI1NksifQ", "eyJhbGciOiJFUzI1NksifQ.", "eyJhbGciOiJFUzI1NksifQ..",
218 ".eyJzdWIiOiJ0ZXN0In0.", "!!invalid-base64!!.eyJzdWIiOiJ0ZXN0In0.sig"] {
219 assert!(verify_access_token(token, &key_bytes).is_err(), "Malformed token must be rejected");
220 }
221
222 let invalid_header = URL_SAFE_NO_PAD.encode("{not valid json}");
223 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"sub":"test"}"#);
224 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 64]);
225 assert!(verify_access_token(&format!("{}.{}.{}", invalid_header, claims_b64, fake_sig), &key_bytes).is_err());
226
227 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K","typ":"at+jwt"}"#);
228 let invalid_claims = URL_SAFE_NO_PAD.encode("{not valid json}");
229 assert!(verify_access_token(&format!("{}.{}.{}", header_b64, invalid_claims, fake_sig), &key_bytes).is_err());
230}
231
232#[test]
233fn test_claim_validation() {
234 let key_bytes = generate_user_key();
235 let did = "did:plc:test";
236 let header = json!({ "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS });
237
238 let missing_exp = json!({
239 "iss": did, "sub": did, "aud": "did:web:test",
240 "iat": Utc::now().timestamp(), "scope": SCOPE_ACCESS
241 });
242 assert!(verify_access_token(&create_custom_jwt(&header, &missing_exp, &key_bytes), &key_bytes).is_err());
243
244 let missing_iat = json!({
245 "iss": did, "sub": did, "aud": "did:web:test",
246 "exp": Utc::now().timestamp() + 3600, "scope": SCOPE_ACCESS
247 });
248 assert!(verify_access_token(&create_custom_jwt(&header, &missing_iat, &key_bytes), &key_bytes).is_err());
249
250 let missing_sub = json!({
251 "iss": did, "aud": "did:web:test",
252 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, "scope": SCOPE_ACCESS
253 });
254 assert!(verify_access_token(&create_custom_jwt(&header, &missing_sub, &key_bytes), &key_bytes).is_err());
255
256 let wrong_types = json!({
257 "iss": 12345, "sub": ["did:plc:test"], "aud": {"url": "did:web:test"},
258 "iat": "not a number", "exp": "also not a number", "jti": null, "scope": SCOPE_ACCESS
259 });
260 assert!(verify_access_token(&create_custom_jwt(&header, &wrong_types, &key_bytes), &key_bytes).is_err());
261
262 let unicode_injection = json!({
263 "iss": "did:plc:test\u{0000}attacker", "sub": "did:plc:test\u{202E}rekatta",
264 "aud": "did:web:test.pds", "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
265 "jti": "test", "scope": SCOPE_ACCESS
266 });
267 if let Ok(data) = verify_access_token(&create_custom_jwt(&header, &unicode_injection, &key_bytes), &key_bytes) {
268 assert!(!data.claims.sub.contains('\0'));
269 }
270}
271
272#[test]
273fn test_did_and_jti_extraction() {
274 let key_bytes = generate_user_key();
275 let did = "did:plc:legitimate";
276 let token = create_access_token(did, &key_bytes).expect("create token");
277
278 assert_eq!(get_did_from_token(&token).unwrap(), did);
279 assert!(get_did_from_token("invalid").is_err());
280 assert!(get_did_from_token("a.b").is_err());
281 assert!(get_did_from_token("").is_err());
282
283 let jti = get_jti_from_token(&token).unwrap();
284 assert!(!jti.is_empty());
285 assert!(get_jti_from_token("invalid").is_err());
286
287 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K"}"#);
288 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:iss","sub":"did:plc:sub"}"#);
289 let fake_sig = URL_SAFE_NO_PAD.encode(&[0u8; 64]);
290 let unverified = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
291 assert_eq!(get_did_from_token(&unverified).unwrap(), "did:plc:sub");
292
293 let no_jti_claims = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:test"}"#);
294 assert!(get_jti_from_token(&format!("{}.{}.{}", header_b64, no_jti_claims, fake_sig)).is_err());
295}
296
297#[test]
298fn test_header_injection_and_constant_time() {
299 let key_bytes = generate_user_key();
300 let did = "did:plc:test";
301
302 let header = json!({
303 "alg": "ES256K", "typ": TOKEN_TYPE_ACCESS,
304 "kid": "../../../../../../etc/passwd", "jku": "https://attacker.com/keys"
305 });
306 let claims = json!({
307 "iss": did, "sub": did, "aud": "did:web:test.pds",
308 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
309 "jti": "test", "scope": SCOPE_ACCESS
310 });
311 assert!(verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes).is_ok());
312
313 let valid_token = create_access_token(did, &key_bytes).expect("create token");
314 let parts: Vec<&str> = valid_token.split('.').collect();
315 let mut almost_valid = URL_SAFE_NO_PAD.decode(parts[2]).unwrap();
316 almost_valid[0] ^= 1;
317 let almost_valid_token = format!("{}.{}.{}", parts[0], parts[1], URL_SAFE_NO_PAD.encode(&almost_valid));
318 let completely_invalid_token = format!("{}.{}.{}", parts[0], parts[1], URL_SAFE_NO_PAD.encode(&[0xFFu8; 64]));
319 let _ = verify_access_token(&almost_valid_token, &key_bytes);
320 let _ = verify_access_token(&completely_invalid_token, &key_bytes);
321}
322
323#[tokio::test]
324async fn test_server_rejects_invalid_tokens() {
325 let url = base_url().await;
326 let http_client = client();
327
328 let key_bytes = generate_user_key();
329 let forged_token = create_access_token("did:plc:fake-user", &key_bytes).unwrap();
330 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
331 .header("Authorization", format!("Bearer {}", forged_token))
332 .send().await.unwrap();
333 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Forged token must be rejected");
334
335 let (access_jwt, _did) = create_account_and_login(&http_client).await;
336 let parts: Vec<&str> = access_jwt.split('.').collect();
337 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap();
338 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
339
340 payload["exp"] = json!(Utc::now().timestamp() - 3600);
341 let expired_token = format!("{}.{}.{}", parts[0], URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()), parts[2]);
342 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
343 .header("Authorization", format!("Bearer {}", expired_token))
344 .send().await.unwrap();
345 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
346
347 let mut tampered_payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
348 tampered_payload["sub"] = json!("did:plc:attacker");
349 tampered_payload["iss"] = json!("did:plc:attacker");
350 let tampered_token = format!("{}.{}.{}", parts[0], URL_SAFE_NO_PAD.encode(serde_json::to_string(&tampered_payload).unwrap()), parts[2]);
351 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
352 .header("Authorization", format!("Bearer {}", tampered_token))
353 .send().await.unwrap();
354 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
355}
356
357#[tokio::test]
358async fn test_authorization_header_formats() {
359 let url = base_url().await;
360 let http_client = client();
361 let (access_jwt, _did) = create_account_and_login(&http_client).await;
362
363 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
364 .header("Authorization", format!("Bearer {}", access_jwt))
365 .send().await.unwrap();
366 assert_eq!(res.status(), StatusCode::OK);
367
368 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
369 .header("Authorization", format!("bearer {}", access_jwt))
370 .send().await.unwrap();
371 assert_eq!(res.status(), StatusCode::OK);
372
373 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
374 .header("Authorization", format!("Basic {}", access_jwt))
375 .send().await.unwrap();
376 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
377
378 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
379 .header("Authorization", &access_jwt)
380 .send().await.unwrap();
381 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
382
383 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
384 .header("Authorization", "Bearer ")
385 .send().await.unwrap();
386 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
387}
388
389#[tokio::test]
390async fn test_session_lifecycle_security() {
391 let url = base_url().await;
392 let http_client = client();
393 let (access_jwt, _did) = create_account_and_login(&http_client).await;
394
395 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
396 .header("Authorization", format!("Bearer {}", access_jwt))
397 .send().await.unwrap();
398 assert_eq!(res.status(), StatusCode::OK);
399
400 let logout = http_client.post(format!("{}/xrpc/com.atproto.server.deleteSession", url))
401 .header("Authorization", format!("Bearer {}", access_jwt))
402 .send().await.unwrap();
403 assert_eq!(logout.status(), StatusCode::OK);
404
405 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
406 .header("Authorization", format!("Bearer {}", access_jwt))
407 .send().await.unwrap();
408 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
409}
410
411#[tokio::test]
412async fn test_deactivated_account_rejected() {
413 let url = base_url().await;
414 let http_client = client();
415 let (access_jwt, _did) = create_account_and_login(&http_client).await;
416
417 let deact = http_client.post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url))
418 .header("Authorization", format!("Bearer {}", access_jwt))
419 .json(&json!({}))
420 .send().await.unwrap();
421 assert_eq!(deact.status(), StatusCode::OK);
422
423 let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
424 .header("Authorization", format!("Bearer {}", access_jwt))
425 .send().await.unwrap();
426 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
427 let body: Value = res.json().await.unwrap();
428 assert_eq!(body["error"], "AccountDeactivated");
429}
430
431#[tokio::test]
432async fn test_refresh_token_replay_protection() {
433 let url = base_url().await;
434 let http_client = client();
435 let ts = Utc::now().timestamp_millis();
436 let handle = format!("rt-replay-jwt-{}", ts);
437 let email = format!("rt-replay-jwt-{}@example.com", ts);
438
439 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
440 .json(&json!({ "handle": handle, "email": email, "password": "test-password-123" }))
441 .send().await.unwrap();
442 assert_eq!(create_res.status(), StatusCode::OK);
443 let account: Value = create_res.json().await.unwrap();
444 let did = account["did"].as_str().unwrap();
445
446 let pool = sqlx::postgres::PgPoolOptions::new()
447 .max_connections(2)
448 .connect(&get_db_connection_string().await)
449 .await.unwrap();
450 let code: String = sqlx::query_scalar!(
451 "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
452 did
453 ).fetch_one(&pool).await.unwrap();
454
455 let confirm = http_client.post(format!("{}/xrpc/com.atproto.server.confirmSignup", url))
456 .json(&json!({ "did": did, "verificationCode": code }))
457 .send().await.unwrap();
458 assert_eq!(confirm.status(), StatusCode::OK);
459 let confirmed: Value = confirm.json().await.unwrap();
460 let refresh_jwt = confirmed["refreshJwt"].as_str().unwrap().to_string();
461
462 let first = http_client.post(format!("{}/xrpc/com.atproto.server.refreshSession", url))
463 .header("Authorization", format!("Bearer {}", refresh_jwt))
464 .send().await.unwrap();
465 assert_eq!(first.status(), StatusCode::OK);
466
467 let replay = http_client.post(format!("{}/xrpc/com.atproto.server.refreshSession", url))
468 .header("Authorization", format!("Bearer {}", refresh_jwt))
469 .send().await.unwrap();
470 assert_eq!(replay.status(), StatusCode::UNAUTHORIZED);
471}