this repo has no description
1mod common;
2mod helpers;
3use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
4use chrono::Utc;
5use common::{base_url, client, get_db_connection_string};
6use helpers::verify_new_account;
7use reqwest::{StatusCode, redirect};
8use serde_json::{Value, json};
9use sha2::{Digest, Sha256};
10use wiremock::matchers::{method, path};
11use wiremock::{Mock, MockServer, ResponseTemplate};
12
13fn no_redirect_client() -> reqwest::Client {
14 reqwest::Client::builder().redirect(redirect::Policy::none()).build().unwrap()
15}
16
17fn generate_pkce() -> (String, String) {
18 let verifier_bytes: [u8; 32] = rand::random();
19 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
20 let mut hasher = Sha256::new();
21 hasher.update(code_verifier.as_bytes());
22 let code_challenge = URL_SAFE_NO_PAD.encode(&hasher.finalize());
23 (code_verifier, code_challenge)
24}
25
26async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer {
27 let mock_server = MockServer::start().await;
28 let client_id = mock_server.uri();
29 let metadata = json!({
30 "client_id": client_id,
31 "client_name": "Test OAuth Client",
32 "redirect_uris": [redirect_uri],
33 "grant_types": ["authorization_code", "refresh_token"],
34 "response_types": ["code"],
35 "token_endpoint_auth_method": "none",
36 "dpop_bound_access_tokens": false
37 });
38 Mock::given(method("GET"))
39 .and(path("/"))
40 .respond_with(ResponseTemplate::new(200).set_body_json(metadata))
41 .mount(&mock_server)
42 .await;
43 mock_server
44}
45
46#[tokio::test]
47async fn test_oauth_metadata_endpoints() {
48 let url = base_url().await;
49 let client = client();
50 let pr_res = client.get(format!("{}/.well-known/oauth-protected-resource", url)).send().await.unwrap();
51 assert_eq!(pr_res.status(), StatusCode::OK);
52 let pr_body: Value = pr_res.json().await.unwrap();
53 assert!(pr_body["resource"].is_string());
54 assert!(pr_body["authorization_servers"].is_array());
55 assert!(pr_body["bearer_methods_supported"].as_array().unwrap().contains(&json!("header")));
56 let as_res = client.get(format!("{}/.well-known/oauth-authorization-server", url)).send().await.unwrap();
57 assert_eq!(as_res.status(), StatusCode::OK);
58 let as_body: Value = as_res.json().await.unwrap();
59 assert!(as_body["issuer"].is_string());
60 assert!(as_body["authorization_endpoint"].is_string());
61 assert!(as_body["token_endpoint"].is_string());
62 assert!(as_body["jwks_uri"].is_string());
63 assert!(as_body["response_types_supported"].as_array().unwrap().contains(&json!("code")));
64 assert!(as_body["grant_types_supported"].as_array().unwrap().contains(&json!("authorization_code")));
65 assert!(as_body["code_challenge_methods_supported"].as_array().unwrap().contains(&json!("S256")));
66 assert_eq!(as_body["require_pushed_authorization_requests"], json!(true));
67 assert!(as_body["dpop_signing_alg_values_supported"].as_array().unwrap().contains(&json!("ES256")));
68 let jwks_res = client.get(format!("{}/oauth/jwks", url)).send().await.unwrap();
69 assert_eq!(jwks_res.status(), StatusCode::OK);
70 let jwks_body: Value = jwks_res.json().await.unwrap();
71 assert!(jwks_body["keys"].is_array());
72}
73
74#[tokio::test]
75async fn test_par_and_authorize() {
76 let url = base_url().await;
77 let client = client();
78 let redirect_uri = "https://example.com/callback";
79 let mock_client = setup_mock_client_metadata(redirect_uri).await;
80 let client_id = mock_client.uri();
81 let (_, code_challenge) = generate_pkce();
82 let par_res = client
83 .post(format!("{}/oauth/par", url))
84 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri),
85 ("code_challenge", &code_challenge), ("code_challenge_method", "S256"), ("scope", "atproto"), ("state", "test-state")])
86 .send().await.unwrap();
87 assert_eq!(par_res.status(), StatusCode::CREATED, "PAR should succeed");
88 let par_body: Value = par_res.json().await.unwrap();
89 assert!(par_body["request_uri"].is_string());
90 assert!(par_body["expires_in"].is_number());
91 let request_uri = par_body["request_uri"].as_str().unwrap();
92 assert!(request_uri.starts_with("urn:ietf:params:oauth:request_uri:"));
93 let auth_res = client
94 .get(format!("{}/oauth/authorize", url))
95 .header("Accept", "application/json")
96 .query(&[("request_uri", request_uri)])
97 .send().await.unwrap();
98 assert_eq!(auth_res.status(), StatusCode::OK);
99 let auth_body: Value = auth_res.json().await.unwrap();
100 assert_eq!(auth_body["client_id"], client_id);
101 assert_eq!(auth_body["redirect_uri"], redirect_uri);
102 assert_eq!(auth_body["scope"], "atproto");
103 let invalid_res = client
104 .get(format!("{}/oauth/authorize", url))
105 .header("Accept", "application/json")
106 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:nonexistent")])
107 .send().await.unwrap();
108 assert_eq!(invalid_res.status(), StatusCode::BAD_REQUEST);
109 let missing_res = client.get(format!("{}/oauth/authorize", url)).send().await.unwrap();
110 assert_eq!(missing_res.status(), StatusCode::BAD_REQUEST);
111}
112
113#[tokio::test]
114async fn test_full_oauth_flow() {
115 let url = base_url().await;
116 let http_client = client();
117 let ts = Utc::now().timestamp_millis();
118 let handle = format!("oauth-test-{}", ts);
119 let email = format!("oauth-test-{}@example.com", ts);
120 let password = "oauth-test-password";
121 let create_res = http_client
122 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
123 .json(&json!({ "handle": handle, "email": email, "password": password }))
124 .send().await.unwrap();
125 assert_eq!(create_res.status(), StatusCode::OK);
126 let account: Value = create_res.json().await.unwrap();
127 let user_did = account["did"].as_str().unwrap();
128 verify_new_account(&http_client, user_did).await;
129 let redirect_uri = "https://example.com/oauth/callback";
130 let mock_client = setup_mock_client_metadata(redirect_uri).await;
131 let client_id = mock_client.uri();
132 let (code_verifier, code_challenge) = generate_pkce();
133 let state = format!("state-{}", ts);
134 let par_res = http_client
135 .post(format!("{}/oauth/par", url))
136 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri),
137 ("code_challenge", &code_challenge), ("code_challenge_method", "S256"), ("scope", "atproto"), ("state", &state)])
138 .send().await.unwrap();
139 let par_body: Value = par_res.json().await.unwrap();
140 let request_uri = par_body["request_uri"].as_str().unwrap();
141 let auth_client = no_redirect_client();
142 let auth_res = auth_client
143 .post(format!("{}/oauth/authorize", url))
144 .form(&[("request_uri", request_uri), ("username", &handle), ("password", password), ("remember_device", "false")])
145 .send().await.unwrap();
146 assert!(auth_res.status().is_redirection(), "Expected redirect, got {}", auth_res.status());
147 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
148 assert!(location.starts_with(redirect_uri), "Redirect to wrong URI");
149 assert!(location.contains("code="), "No code in redirect");
150 assert!(location.contains(&format!("state={}", state)), "Wrong state");
151 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
152 let token_res = http_client
153 .post(format!("{}/oauth/token", url))
154 .form(&[("grant_type", "authorization_code"), ("code", code), ("redirect_uri", redirect_uri),
155 ("code_verifier", &code_verifier), ("client_id", &client_id)])
156 .send().await.unwrap();
157 assert_eq!(token_res.status(), StatusCode::OK, "Token exchange failed");
158 let token_body: Value = token_res.json().await.unwrap();
159 assert!(token_body["access_token"].is_string());
160 assert!(token_body["refresh_token"].is_string());
161 assert_eq!(token_body["token_type"], "Bearer");
162 assert!(token_body["expires_in"].is_number());
163 assert_eq!(token_body["sub"], user_did);
164 let access_token = token_body["access_token"].as_str().unwrap();
165 let refresh_token = token_body["refresh_token"].as_str().unwrap();
166 let refresh_res = http_client
167 .post(format!("{}/oauth/token", url))
168 .form(&[("grant_type", "refresh_token"), ("refresh_token", refresh_token), ("client_id", &client_id)])
169 .send().await.unwrap();
170 assert_eq!(refresh_res.status(), StatusCode::OK);
171 let refresh_body: Value = refresh_res.json().await.unwrap();
172 assert_ne!(refresh_body["access_token"].as_str().unwrap(), access_token);
173 assert_ne!(refresh_body["refresh_token"].as_str().unwrap(), refresh_token);
174 let introspect_res = http_client
175 .post(format!("{}/oauth/introspect", url))
176 .form(&[("token", refresh_body["access_token"].as_str().unwrap())])
177 .send().await.unwrap();
178 assert_eq!(introspect_res.status(), StatusCode::OK);
179 let introspect_body: Value = introspect_res.json().await.unwrap();
180 assert_eq!(introspect_body["active"], true);
181 let revoke_res = http_client
182 .post(format!("{}/oauth/revoke", url))
183 .form(&[("token", refresh_body["refresh_token"].as_str().unwrap())])
184 .send().await.unwrap();
185 assert_eq!(revoke_res.status(), StatusCode::OK);
186 let introspect_after = http_client
187 .post(format!("{}/oauth/introspect", url))
188 .form(&[("token", refresh_body["access_token"].as_str().unwrap())])
189 .send().await.unwrap();
190 let after_body: Value = introspect_after.json().await.unwrap();
191 assert_eq!(after_body["active"], false, "Revoked token should be inactive");
192}
193
194#[tokio::test]
195async fn test_oauth_error_cases() {
196 let url = base_url().await;
197 let http_client = client();
198 let ts = Utc::now().timestamp_millis();
199 let handle = format!("wrong-creds-{}", ts);
200 let email = format!("wrong-creds-{}@example.com", ts);
201 http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
202 .json(&json!({ "handle": handle, "email": email, "password": "correct-password" }))
203 .send().await.unwrap();
204 let redirect_uri = "https://example.com/callback";
205 let mock_client = setup_mock_client_metadata(redirect_uri).await;
206 let client_id = mock_client.uri();
207 let (_, code_challenge) = generate_pkce();
208 let par_body: Value = http_client
209 .post(format!("{}/oauth/par", url))
210 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri),
211 ("code_challenge", &code_challenge), ("code_challenge_method", "S256")])
212 .send().await.unwrap().json().await.unwrap();
213 let request_uri = par_body["request_uri"].as_str().unwrap();
214 let auth_res = http_client
215 .post(format!("{}/oauth/authorize", url))
216 .header("Accept", "application/json")
217 .form(&[("request_uri", request_uri), ("username", &handle), ("password", "wrong-password"), ("remember_device", "false")])
218 .send().await.unwrap();
219 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN);
220 let error_body: Value = auth_res.json().await.unwrap();
221 assert_eq!(error_body["error"], "access_denied");
222 let unsupported = http_client
223 .post(format!("{}/oauth/token", url))
224 .form(&[("grant_type", "client_credentials"), ("client_id", "https://example.com")])
225 .send().await.unwrap();
226 assert_eq!(unsupported.status(), StatusCode::BAD_REQUEST);
227 let body: Value = unsupported.json().await.unwrap();
228 assert_eq!(body["error"], "unsupported_grant_type");
229 let invalid_refresh = http_client
230 .post(format!("{}/oauth/token", url))
231 .form(&[("grant_type", "refresh_token"), ("refresh_token", "invalid-token"), ("client_id", "https://example.com")])
232 .send().await.unwrap();
233 assert_eq!(invalid_refresh.status(), StatusCode::BAD_REQUEST);
234 let body: Value = invalid_refresh.json().await.unwrap();
235 assert_eq!(body["error"], "invalid_grant");
236 let invalid_introspect = http_client
237 .post(format!("{}/oauth/introspect", url))
238 .form(&[("token", "invalid.token.here")])
239 .send().await.unwrap();
240 assert_eq!(invalid_introspect.status(), StatusCode::OK);
241 let body: Value = invalid_introspect.json().await.unwrap();
242 assert_eq!(body["active"], false);
243 let expired_res = http_client
244 .get(format!("{}/oauth/authorize", url))
245 .header("Accept", "application/json")
246 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:expired")])
247 .send().await.unwrap();
248 assert_eq!(expired_res.status(), StatusCode::BAD_REQUEST);
249}
250
251#[tokio::test]
252async fn test_oauth_2fa_flow() {
253 let url = base_url().await;
254 let http_client = client();
255 let ts = Utc::now().timestamp_millis();
256 let handle = format!("2fa-test-{}", ts);
257 let email = format!("2fa-test-{}@example.com", ts);
258 let password = "2fa-test-password";
259 let create_res = http_client
260 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
261 .json(&json!({ "handle": handle, "email": email, "password": password }))
262 .send().await.unwrap();
263 assert_eq!(create_res.status(), StatusCode::OK);
264 let account: Value = create_res.json().await.unwrap();
265 let user_did = account["did"].as_str().unwrap();
266 verify_new_account(&http_client, user_did).await;
267 let db_url = get_db_connection_string().await;
268 let pool = sqlx::postgres::PgPoolOptions::new().max_connections(1).connect(&db_url).await.unwrap();
269 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
270 .bind(user_did).execute(&pool).await.unwrap();
271 let redirect_uri = "https://example.com/2fa-callback";
272 let mock_client = setup_mock_client_metadata(redirect_uri).await;
273 let client_id = mock_client.uri();
274 let (code_verifier, code_challenge) = generate_pkce();
275 let par_body: Value = http_client
276 .post(format!("{}/oauth/par", url))
277 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri),
278 ("code_challenge", &code_challenge), ("code_challenge_method", "S256")])
279 .send().await.unwrap().json().await.unwrap();
280 let request_uri = par_body["request_uri"].as_str().unwrap();
281 let auth_client = no_redirect_client();
282 let auth_res = auth_client
283 .post(format!("{}/oauth/authorize", url))
284 .form(&[("request_uri", request_uri), ("username", &handle), ("password", password), ("remember_device", "false")])
285 .send().await.unwrap();
286 assert!(auth_res.status().is_redirection(), "Should redirect to 2FA page");
287 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
288 assert!(location.contains("/oauth/authorize/2fa"), "Should redirect to 2FA page, got: {}", location);
289 let twofa_invalid = http_client
290 .post(format!("{}/oauth/authorize/2fa", url))
291 .form(&[("request_uri", request_uri), ("code", "000000")])
292 .send().await.unwrap();
293 assert_eq!(twofa_invalid.status(), StatusCode::OK);
294 let body = twofa_invalid.text().await.unwrap();
295 assert!(body.contains("Invalid verification code") || body.contains("invalid"));
296 let twofa_code: String = sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1")
297 .bind(request_uri).fetch_one(&pool).await.unwrap();
298 let twofa_res = auth_client
299 .post(format!("{}/oauth/authorize/2fa", url))
300 .form(&[("request_uri", request_uri), ("code", &twofa_code)])
301 .send().await.unwrap();
302 assert!(twofa_res.status().is_redirection(), "Valid 2FA code should redirect");
303 let final_location = twofa_res.headers().get("location").unwrap().to_str().unwrap();
304 assert!(final_location.starts_with(redirect_uri) && final_location.contains("code="));
305 let auth_code = final_location.split("code=").nth(1).unwrap().split('&').next().unwrap();
306 let token_res = http_client
307 .post(format!("{}/oauth/token", url))
308 .form(&[("grant_type", "authorization_code"), ("code", auth_code), ("redirect_uri", redirect_uri),
309 ("code_verifier", &code_verifier), ("client_id", &client_id)])
310 .send().await.unwrap();
311 assert_eq!(token_res.status(), StatusCode::OK);
312 let token_body: Value = token_res.json().await.unwrap();
313 assert_eq!(token_body["sub"], user_did);
314}
315
316#[tokio::test]
317async fn test_oauth_2fa_lockout() {
318 let url = base_url().await;
319 let http_client = client();
320 let ts = Utc::now().timestamp_millis();
321 let handle = format!("2fa-lockout-{}", ts);
322 let email = format!("2fa-lockout-{}@example.com", ts);
323 let password = "2fa-test-password";
324 let create_res = http_client
325 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
326 .json(&json!({ "handle": handle, "email": email, "password": password }))
327 .send().await.unwrap();
328 let account: Value = create_res.json().await.unwrap();
329 let user_did = account["did"].as_str().unwrap();
330 verify_new_account(&http_client, user_did).await;
331 let db_url = get_db_connection_string().await;
332 let pool = sqlx::postgres::PgPoolOptions::new().max_connections(1).connect(&db_url).await.unwrap();
333 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
334 .bind(user_did).execute(&pool).await.unwrap();
335 let redirect_uri = "https://example.com/2fa-lockout-callback";
336 let mock_client = setup_mock_client_metadata(redirect_uri).await;
337 let client_id = mock_client.uri();
338 let (_, code_challenge) = generate_pkce();
339 let par_body: Value = http_client
340 .post(format!("{}/oauth/par", url))
341 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri),
342 ("code_challenge", &code_challenge), ("code_challenge_method", "S256")])
343 .send().await.unwrap().json().await.unwrap();
344 let request_uri = par_body["request_uri"].as_str().unwrap();
345 let auth_client = no_redirect_client();
346 let auth_res = auth_client
347 .post(format!("{}/oauth/authorize", url))
348 .form(&[("request_uri", request_uri), ("username", &handle), ("password", password), ("remember_device", "false")])
349 .send().await.unwrap();
350 assert!(auth_res.status().is_redirection());
351 for i in 0..5 {
352 let res = http_client
353 .post(format!("{}/oauth/authorize/2fa", url))
354 .form(&[("request_uri", request_uri), ("code", "999999")])
355 .send().await.unwrap();
356 if i < 4 {
357 assert_eq!(res.status(), StatusCode::OK);
358 }
359 }
360 let lockout_res = http_client
361 .post(format!("{}/oauth/authorize/2fa", url))
362 .form(&[("request_uri", request_uri), ("code", "999999")])
363 .send().await.unwrap();
364 let body = lockout_res.text().await.unwrap();
365 assert!(body.contains("Too many failed attempts") || body.contains("No 2FA challenge found"));
366}
367
368#[tokio::test]
369async fn test_account_selector_with_2fa() {
370 let url = base_url().await;
371 let http_client = client();
372 let ts = Utc::now().timestamp_millis();
373 let handle = format!("selector-2fa-{}", ts);
374 let email = format!("selector-2fa-{}@example.com", ts);
375 let password = "selector-2fa-password";
376 let create_res = http_client
377 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
378 .json(&json!({ "handle": handle, "email": email, "password": password }))
379 .send().await.unwrap();
380 let account: Value = create_res.json().await.unwrap();
381 let user_did = account["did"].as_str().unwrap().to_string();
382 verify_new_account(&http_client, &user_did).await;
383 let redirect_uri = "https://example.com/selector-2fa-callback";
384 let mock_client = setup_mock_client_metadata(redirect_uri).await;
385 let client_id = mock_client.uri();
386 let (code_verifier, code_challenge) = generate_pkce();
387 let par_body: Value = http_client
388 .post(format!("{}/oauth/par", url))
389 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri),
390 ("code_challenge", &code_challenge), ("code_challenge_method", "S256")])
391 .send().await.unwrap().json().await.unwrap();
392 let request_uri = par_body["request_uri"].as_str().unwrap();
393 let auth_client = no_redirect_client();
394 let auth_res = auth_client
395 .post(format!("{}/oauth/authorize", url))
396 .form(&[("request_uri", request_uri), ("username", &handle), ("password", password), ("remember_device", "true")])
397 .send().await.unwrap();
398 assert!(auth_res.status().is_redirection());
399 let device_cookie = auth_res.headers().get("set-cookie")
400 .and_then(|v| v.to_str().ok())
401 .map(|s| s.split(';').next().unwrap_or("").to_string())
402 .expect("Should have device cookie");
403 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
404 assert!(location.contains("code="));
405 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
406 let _ = http_client
407 .post(format!("{}/oauth/token", url))
408 .form(&[("grant_type", "authorization_code"), ("code", code), ("redirect_uri", redirect_uri),
409 ("code_verifier", &code_verifier), ("client_id", &client_id)])
410 .send().await.unwrap().json::<Value>().await.unwrap();
411 let db_url = get_db_connection_string().await;
412 let pool = sqlx::postgres::PgPoolOptions::new().max_connections(1).connect(&db_url).await.unwrap();
413 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
414 .bind(&user_did).execute(&pool).await.unwrap();
415 let (code_verifier2, code_challenge2) = generate_pkce();
416 let par_body2: Value = http_client
417 .post(format!("{}/oauth/par", url))
418 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri),
419 ("code_challenge", &code_challenge2), ("code_challenge_method", "S256")])
420 .send().await.unwrap().json().await.unwrap();
421 let request_uri2 = par_body2["request_uri"].as_str().unwrap();
422 let select_res = auth_client
423 .post(format!("{}/oauth/authorize/select", url))
424 .header("cookie", &device_cookie)
425 .form(&[("request_uri", request_uri2), ("did", &user_did)])
426 .send().await.unwrap();
427 assert!(select_res.status().is_redirection());
428 let select_location = select_res.headers().get("location").unwrap().to_str().unwrap();
429 assert!(select_location.contains("/oauth/authorize/2fa"), "Should redirect to 2FA page");
430 let twofa_code: String = sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1")
431 .bind(request_uri2).fetch_one(&pool).await.unwrap();
432 let twofa_res = auth_client
433 .post(format!("{}/oauth/authorize/2fa", url))
434 .header("cookie", &device_cookie)
435 .form(&[("request_uri", request_uri2), ("code", &twofa_code)])
436 .send().await.unwrap();
437 assert!(twofa_res.status().is_redirection());
438 let final_location = twofa_res.headers().get("location").unwrap().to_str().unwrap();
439 assert!(final_location.starts_with(redirect_uri) && final_location.contains("code="));
440 let final_code = final_location.split("code=").nth(1).unwrap().split('&').next().unwrap();
441 let token_res = http_client
442 .post(format!("{}/oauth/token", url))
443 .form(&[("grant_type", "authorization_code"), ("code", final_code), ("redirect_uri", redirect_uri),
444 ("code_verifier", &code_verifier2), ("client_id", &client_id)])
445 .send().await.unwrap();
446 assert_eq!(token_res.status(), StatusCode::OK);
447 let final_token: Value = token_res.json().await.unwrap();
448 assert_eq!(final_token["sub"], user_did);
449}
450
451#[tokio::test]
452async fn test_oauth_state_encoding() {
453 let url = base_url().await;
454 let http_client = client();
455 let ts = Utc::now().timestamp_millis();
456 let handle = format!("state-special-{}", ts);
457 let email = format!("state-special-{}@example.com", ts);
458 let password = "state-special-password";
459 let create_res = http_client
460 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
461 .json(&json!({ "handle": handle, "email": email, "password": password }))
462 .send().await.unwrap();
463 let account: Value = create_res.json().await.unwrap();
464 verify_new_account(&http_client, account["did"].as_str().unwrap()).await;
465 let redirect_uri = "https://example.com/state-special-callback";
466 let mock_client = setup_mock_client_metadata(redirect_uri).await;
467 let client_id = mock_client.uri();
468 let (_, code_challenge) = generate_pkce();
469 let special_state = "state=with&special=chars&plus+more";
470 let par_body: Value = http_client
471 .post(format!("{}/oauth/par", url))
472 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri),
473 ("code_challenge", &code_challenge), ("code_challenge_method", "S256"), ("state", special_state)])
474 .send().await.unwrap().json().await.unwrap();
475 let request_uri = par_body["request_uri"].as_str().unwrap();
476 let auth_client = no_redirect_client();
477 let auth_res = auth_client
478 .post(format!("{}/oauth/authorize", url))
479 .form(&[("request_uri", request_uri), ("username", &handle), ("password", password), ("remember_device", "false")])
480 .send().await.unwrap();
481 assert!(auth_res.status().is_redirection());
482 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
483 assert!(location.contains("state="));
484 let encoded_state = urlencoding::encode(special_state);
485 assert!(location.contains(&format!("state={}", encoded_state)), "State should be URL-encoded. Got: {}", location);
486}