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