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};
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()
14 .redirect(redirect::Policy::none())
15 .build()
16 .unwrap()
17}
18
19fn generate_pkce() -> (String, String) {
20 let verifier_bytes: [u8; 32] = rand::random();
21 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
22 let mut hasher = Sha256::new();
23 hasher.update(code_verifier.as_bytes());
24 let hash = hasher.finalize();
25 let code_challenge = URL_SAFE_NO_PAD.encode(&hash);
26 (code_verifier, code_challenge)
27}
28
29async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer {
30 let mock_server = MockServer::start().await;
31 let client_id = mock_server.uri();
32 let metadata = json!({
33 "client_id": client_id,
34 "client_name": "Test OAuth Client",
35 "redirect_uris": [redirect_uri],
36 "grant_types": ["authorization_code", "refresh_token"],
37 "response_types": ["code"],
38 "token_endpoint_auth_method": "none",
39 "dpop_bound_access_tokens": false
40 });
41 Mock::given(method("GET"))
42 .and(path("/"))
43 .respond_with(ResponseTemplate::new(200).set_body_json(metadata))
44 .mount(&mock_server)
45 .await;
46 mock_server
47}
48#[allow(dead_code)]
49async fn setup_mock_dpop_client(redirect_uri: &str) -> MockServer {
50 let mock_server = MockServer::start().await;
51 let client_id = mock_server.uri();
52 let metadata = json!({
53 "client_id": client_id,
54 "client_name": "DPoP Test Client",
55 "redirect_uris": [redirect_uri],
56 "grant_types": ["authorization_code", "refresh_token"],
57 "response_types": ["code"],
58 "token_endpoint_auth_method": "none",
59 "dpop_bound_access_tokens": true
60 });
61 Mock::given(method("GET"))
62 .and(path("/"))
63 .respond_with(ResponseTemplate::new(200).set_body_json(metadata))
64 .mount(&mock_server)
65 .await;
66 mock_server
67}
68#[tokio::test]
69async fn test_oauth_protected_resource_metadata() {
70 let url = base_url().await;
71 let client = client();
72 let res = client
73 .get(format!("{}/.well-known/oauth-protected-resource", url))
74 .send()
75 .await
76 .expect("Failed to fetch protected resource metadata");
77 assert_eq!(res.status(), StatusCode::OK);
78 let body: Value = res.json().await.expect("Invalid JSON");
79 assert!(body["resource"].is_string());
80 assert!(body["authorization_servers"].is_array());
81 assert!(body["bearer_methods_supported"].is_array());
82 let bearer_methods = body["bearer_methods_supported"].as_array().unwrap();
83 assert!(bearer_methods.contains(&json!("header")));
84}
85#[tokio::test]
86async fn test_oauth_authorization_server_metadata() {
87 let url = base_url().await;
88 let client = client();
89 let res = client
90 .get(format!("{}/.well-known/oauth-authorization-server", url))
91 .send()
92 .await
93 .expect("Failed to fetch authorization server metadata");
94 assert_eq!(res.status(), StatusCode::OK);
95 let body: Value = res.json().await.expect("Invalid JSON");
96 assert!(body["issuer"].is_string());
97 assert!(body["authorization_endpoint"].is_string());
98 assert!(body["token_endpoint"].is_string());
99 assert!(body["jwks_uri"].is_string());
100 let response_types = body["response_types_supported"].as_array().unwrap();
101 assert!(response_types.contains(&json!("code")));
102 let grant_types = body["grant_types_supported"].as_array().unwrap();
103 assert!(grant_types.contains(&json!("authorization_code")));
104 assert!(grant_types.contains(&json!("refresh_token")));
105 let code_challenge_methods = body["code_challenge_methods_supported"].as_array().unwrap();
106 assert!(code_challenge_methods.contains(&json!("S256")));
107 assert_eq!(body["require_pushed_authorization_requests"], json!(true));
108 let dpop_algs = body["dpop_signing_alg_values_supported"]
109 .as_array()
110 .unwrap();
111 assert!(dpop_algs.contains(&json!("ES256")));
112}
113#[tokio::test]
114async fn test_oauth_jwks_endpoint() {
115 let url = base_url().await;
116 let client = client();
117 let res = client
118 .get(format!("{}/oauth/jwks", url))
119 .send()
120 .await
121 .expect("Failed to fetch JWKS");
122 assert_eq!(res.status(), StatusCode::OK);
123 let body: Value = res.json().await.expect("Invalid JSON");
124 assert!(body["keys"].is_array());
125}
126#[tokio::test]
127async fn test_par_success() {
128 let url = base_url().await;
129 let client = client();
130 let redirect_uri = "https://example.com/callback";
131 let mock_client = setup_mock_client_metadata(redirect_uri).await;
132 let client_id = mock_client.uri();
133 let (_code_verifier, code_challenge) = generate_pkce();
134 let res = client
135 .post(format!("{}/oauth/par", url))
136 .form(&[
137 ("response_type", "code"),
138 ("client_id", &client_id),
139 ("redirect_uri", redirect_uri),
140 ("code_challenge", &code_challenge),
141 ("code_challenge_method", "S256"),
142 ("scope", "atproto"),
143 ("state", "test-state-123"),
144 ])
145 .send()
146 .await
147 .expect("Failed to send PAR request");
148 assert_eq!(
149 res.status(),
150 StatusCode::CREATED,
151 "PAR should succeed: {:?}",
152 res.text().await
153 );
154 let body: Value = client
155 .post(format!("{}/oauth/par", url))
156 .form(&[
157 ("response_type", "code"),
158 ("client_id", &client_id),
159 ("redirect_uri", redirect_uri),
160 ("code_challenge", &code_challenge),
161 ("code_challenge_method", "S256"),
162 ("scope", "atproto"),
163 ("state", "test-state-123"),
164 ])
165 .send()
166 .await
167 .unwrap()
168 .json()
169 .await
170 .expect("Invalid JSON");
171 assert!(body["request_uri"].is_string());
172 assert!(body["expires_in"].is_number());
173 let request_uri = body["request_uri"].as_str().unwrap();
174 assert!(request_uri.starts_with("urn:ietf:params:oauth:request_uri:"));
175}
176#[tokio::test]
177async fn test_authorize_get_with_valid_request_uri() {
178 let url = base_url().await;
179 let client = client();
180 let redirect_uri = "https://example.com/callback";
181 let mock_client = setup_mock_client_metadata(redirect_uri).await;
182 let client_id = mock_client.uri();
183 let (_, code_challenge) = generate_pkce();
184 let par_res = client
185 .post(format!("{}/oauth/par", url))
186 .form(&[
187 ("response_type", "code"),
188 ("client_id", &client_id),
189 ("redirect_uri", redirect_uri),
190 ("code_challenge", &code_challenge),
191 ("code_challenge_method", "S256"),
192 ("scope", "atproto"),
193 ("state", "test-state"),
194 ])
195 .send()
196 .await
197 .expect("PAR failed");
198 let par_body: Value = par_res.json().await.expect("Invalid PAR JSON");
199 let request_uri = par_body["request_uri"].as_str().unwrap();
200 let auth_res = client
201 .get(format!("{}/oauth/authorize", url))
202 .header("Accept", "application/json")
203 .query(&[("request_uri", request_uri)])
204 .send()
205 .await
206 .expect("Authorize GET failed");
207 assert_eq!(auth_res.status(), StatusCode::OK);
208 let auth_body: Value = auth_res.json().await.expect("Invalid auth JSON");
209 assert_eq!(auth_body["client_id"], client_id);
210 assert_eq!(auth_body["redirect_uri"], redirect_uri);
211 assert_eq!(auth_body["scope"], "atproto");
212 assert_eq!(auth_body["state"], "test-state");
213}
214#[tokio::test]
215async fn test_authorize_rejects_invalid_request_uri() {
216 let url = base_url().await;
217 let client = client();
218 let res = client
219 .get(format!("{}/oauth/authorize", url))
220 .header("Accept", "application/json")
221 .query(&[(
222 "request_uri",
223 "urn:ietf:params:oauth:request_uri:nonexistent",
224 )])
225 .send()
226 .await
227 .expect("Request failed");
228 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
229 let body: Value = res.json().await.expect("Invalid JSON");
230 assert_eq!(body["error"], "invalid_request");
231}
232#[tokio::test]
233async fn test_authorize_requires_request_uri() {
234 let url = base_url().await;
235 let client = client();
236 let res = client
237 .get(format!("{}/oauth/authorize", url))
238 .send()
239 .await
240 .expect("Request failed");
241 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
242}
243#[tokio::test]
244async fn test_full_oauth_flow_without_dpop() {
245 let url = base_url().await;
246 let http_client = client();
247 let (_, _user_did) = create_account_and_login(&http_client).await;
248 let ts = Utc::now().timestamp_millis();
249 let handle = format!("oauth-test-{}", ts);
250 let email = format!("oauth-test-{}@example.com", ts);
251 let password = "oauth-test-password";
252 let create_res = http_client
253 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
254 .json(&json!({
255 "handle": handle,
256 "email": email,
257 "password": password
258 }))
259 .send()
260 .await
261 .expect("Account creation failed");
262 assert_eq!(create_res.status(), StatusCode::OK);
263 let account: Value = create_res.json().await.unwrap();
264 let user_did = account["did"].as_str().unwrap();
265 let redirect_uri = "https://example.com/oauth/callback";
266 let mock_client = setup_mock_client_metadata(redirect_uri).await;
267 let client_id = mock_client.uri();
268 let (code_verifier, code_challenge) = generate_pkce();
269 let state = format!("state-{}", ts);
270 let par_res = http_client
271 .post(format!("{}/oauth/par", url))
272 .form(&[
273 ("response_type", "code"),
274 ("client_id", &client_id),
275 ("redirect_uri", redirect_uri),
276 ("code_challenge", &code_challenge),
277 ("code_challenge_method", "S256"),
278 ("scope", "atproto"),
279 ("state", &state),
280 ])
281 .send()
282 .await
283 .expect("PAR failed");
284 let par_status = par_res.status();
285 let par_text = par_res.text().await.unwrap_or_default();
286 if par_status != StatusCode::OK && par_status != StatusCode::CREATED {
287 panic!("PAR failed with status {}: {}", par_status, par_text);
288 }
289 let par_body: Value = serde_json::from_str(&par_text).unwrap();
290 let request_uri = par_body["request_uri"].as_str().unwrap();
291 let auth_client = no_redirect_client();
292 let auth_res = auth_client
293 .post(format!("{}/oauth/authorize", url))
294 .form(&[
295 ("request_uri", request_uri),
296 ("username", &handle),
297 ("password", password),
298 ("remember_device", "false"),
299 ])
300 .send()
301 .await
302 .expect("Authorize POST failed");
303 let auth_status = auth_res.status();
304 if auth_status != StatusCode::TEMPORARY_REDIRECT
305 && auth_status != StatusCode::SEE_OTHER
306 && auth_status != StatusCode::FOUND
307 {
308 let auth_text = auth_res.text().await.unwrap_or_default();
309 panic!("Expected redirect, got {}: {}", auth_status, auth_text);
310 }
311 let location = auth_res
312 .headers()
313 .get("location")
314 .expect("No Location header")
315 .to_str()
316 .unwrap();
317 assert!(
318 location.starts_with(redirect_uri),
319 "Redirect to wrong URI: {}",
320 location
321 );
322 assert!(
323 location.contains("code="),
324 "No code in redirect: {}",
325 location
326 );
327 assert!(
328 location.contains(&format!("state={}", state)),
329 "Wrong state in redirect"
330 );
331 let code = location
332 .split("code=")
333 .nth(1)
334 .unwrap()
335 .split('&')
336 .next()
337 .unwrap();
338 let token_res = http_client
339 .post(format!("{}/oauth/token", url))
340 .form(&[
341 ("grant_type", "authorization_code"),
342 ("code", code),
343 ("redirect_uri", redirect_uri),
344 ("code_verifier", &code_verifier),
345 ("client_id", &client_id),
346 ])
347 .send()
348 .await
349 .expect("Token request failed");
350 let token_status = token_res.status();
351 let token_text = token_res.text().await.unwrap_or_default();
352 if token_status != StatusCode::OK {
353 panic!(
354 "Token request failed with status {}: {}",
355 token_status, token_text
356 );
357 }
358 let token_body: Value = serde_json::from_str(&token_text).unwrap();
359 assert!(token_body["access_token"].is_string());
360 assert!(token_body["refresh_token"].is_string());
361 assert_eq!(token_body["token_type"], "Bearer");
362 assert!(token_body["expires_in"].is_number());
363 assert_eq!(token_body["sub"], user_did);
364}
365#[tokio::test]
366async fn test_token_refresh_flow() {
367 let url = base_url().await;
368 let http_client = client();
369 let ts = Utc::now().timestamp_millis();
370 let handle = format!("refresh-test-{}", ts);
371 let email = format!("refresh-test-{}@example.com", ts);
372 let password = "refresh-test-password";
373 http_client
374 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
375 .json(&json!({
376 "handle": handle,
377 "email": email,
378 "password": password
379 }))
380 .send()
381 .await
382 .expect("Account creation failed");
383 let redirect_uri = "https://example.com/refresh-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(&[
390 ("response_type", "code"),
391 ("client_id", &client_id),
392 ("redirect_uri", redirect_uri),
393 ("code_challenge", &code_challenge),
394 ("code_challenge_method", "S256"),
395 ])
396 .send()
397 .await
398 .unwrap()
399 .json()
400 .await
401 .unwrap();
402 let request_uri = par_body["request_uri"].as_str().unwrap();
403 let auth_client = no_redirect_client();
404 let auth_res = auth_client
405 .post(format!("{}/oauth/authorize", url))
406 .form(&[
407 ("request_uri", request_uri),
408 ("username", &handle),
409 ("password", password),
410 ("remember_device", "false"),
411 ])
412 .send()
413 .await
414 .unwrap();
415 let location = auth_res
416 .headers()
417 .get("location")
418 .unwrap()
419 .to_str()
420 .unwrap();
421 let code = location
422 .split("code=")
423 .nth(1)
424 .unwrap()
425 .split('&')
426 .next()
427 .unwrap();
428 let token_body: Value = http_client
429 .post(format!("{}/oauth/token", url))
430 .form(&[
431 ("grant_type", "authorization_code"),
432 ("code", code),
433 ("redirect_uri", redirect_uri),
434 ("code_verifier", &code_verifier),
435 ("client_id", &client_id),
436 ])
437 .send()
438 .await
439 .unwrap()
440 .json()
441 .await
442 .unwrap();
443 let refresh_token = token_body["refresh_token"].as_str().unwrap();
444 let original_access_token = token_body["access_token"].as_str().unwrap();
445 let refresh_res = http_client
446 .post(format!("{}/oauth/token", url))
447 .form(&[
448 ("grant_type", "refresh_token"),
449 ("refresh_token", refresh_token),
450 ("client_id", &client_id),
451 ])
452 .send()
453 .await
454 .expect("Refresh request failed");
455 assert_eq!(refresh_res.status(), StatusCode::OK);
456 let refresh_body: Value = refresh_res.json().await.unwrap();
457 assert!(refresh_body["access_token"].is_string());
458 assert!(refresh_body["refresh_token"].is_string());
459 let new_access_token = refresh_body["access_token"].as_str().unwrap();
460 let new_refresh_token = refresh_body["refresh_token"].as_str().unwrap();
461 assert_ne!(
462 new_access_token, original_access_token,
463 "Access token should rotate"
464 );
465 assert_ne!(
466 new_refresh_token, refresh_token,
467 "Refresh token should rotate"
468 );
469}
470#[tokio::test]
471async fn test_wrong_credentials_denied() {
472 let url = base_url().await;
473 let http_client = client();
474 let ts = Utc::now().timestamp_millis();
475 let handle = format!("wrong-creds-{}", ts);
476 let email = format!("wrong-creds-{}@example.com", ts);
477 let password = "correct-password";
478 http_client
479 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
480 .json(&json!({
481 "handle": handle,
482 "email": email,
483 "password": password
484 }))
485 .send()
486 .await
487 .unwrap();
488 let redirect_uri = "https://example.com/wrong-creds-callback";
489 let mock_client = setup_mock_client_metadata(redirect_uri).await;
490 let client_id = mock_client.uri();
491 let (_, code_challenge) = generate_pkce();
492 let par_body: Value = http_client
493 .post(format!("{}/oauth/par", url))
494 .form(&[
495 ("response_type", "code"),
496 ("client_id", &client_id),
497 ("redirect_uri", redirect_uri),
498 ("code_challenge", &code_challenge),
499 ("code_challenge_method", "S256"),
500 ])
501 .send()
502 .await
503 .unwrap()
504 .json()
505 .await
506 .unwrap();
507 let request_uri = par_body["request_uri"].as_str().unwrap();
508 let auth_res = http_client
509 .post(format!("{}/oauth/authorize", url))
510 .header("Accept", "application/json")
511 .form(&[
512 ("request_uri", request_uri),
513 ("username", &handle),
514 ("password", "wrong-password"),
515 ("remember_device", "false"),
516 ])
517 .send()
518 .await
519 .unwrap();
520 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN);
521 let error_body: Value = auth_res.json().await.unwrap();
522 assert_eq!(error_body["error"], "access_denied");
523}
524#[tokio::test]
525async fn test_token_revocation() {
526 let url = base_url().await;
527 let http_client = client();
528 let ts = Utc::now().timestamp_millis();
529 let handle = format!("revoke-test-{}", ts);
530 let email = format!("revoke-test-{}@example.com", ts);
531 let password = "revoke-test-password";
532 http_client
533 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
534 .json(&json!({
535 "handle": handle,
536 "email": email,
537 "password": password
538 }))
539 .send()
540 .await
541 .unwrap();
542 let redirect_uri = "https://example.com/revoke-callback";
543 let mock_client = setup_mock_client_metadata(redirect_uri).await;
544 let client_id = mock_client.uri();
545 let (code_verifier, code_challenge) = generate_pkce();
546 let par_body: Value = http_client
547 .post(format!("{}/oauth/par", url))
548 .form(&[
549 ("response_type", "code"),
550 ("client_id", &client_id),
551 ("redirect_uri", redirect_uri),
552 ("code_challenge", &code_challenge),
553 ("code_challenge_method", "S256"),
554 ])
555 .send()
556 .await
557 .unwrap()
558 .json()
559 .await
560 .unwrap();
561 let request_uri = par_body["request_uri"].as_str().unwrap();
562 let auth_client = no_redirect_client();
563 let auth_res = auth_client
564 .post(format!("{}/oauth/authorize", url))
565 .form(&[
566 ("request_uri", request_uri),
567 ("username", &handle),
568 ("password", password),
569 ("remember_device", "false"),
570 ])
571 .send()
572 .await
573 .unwrap();
574 let location = auth_res
575 .headers()
576 .get("location")
577 .unwrap()
578 .to_str()
579 .unwrap();
580 let code = location
581 .split("code=")
582 .nth(1)
583 .unwrap()
584 .split('&')
585 .next()
586 .unwrap();
587 let token_body: Value = http_client
588 .post(format!("{}/oauth/token", url))
589 .form(&[
590 ("grant_type", "authorization_code"),
591 ("code", code),
592 ("redirect_uri", redirect_uri),
593 ("code_verifier", &code_verifier),
594 ("client_id", &client_id),
595 ])
596 .send()
597 .await
598 .unwrap()
599 .json()
600 .await
601 .unwrap();
602 let refresh_token = token_body["refresh_token"].as_str().unwrap();
603 let revoke_res = http_client
604 .post(format!("{}/oauth/revoke", url))
605 .form(&[("token", refresh_token)])
606 .send()
607 .await
608 .unwrap();
609 assert_eq!(revoke_res.status(), StatusCode::OK);
610 let refresh_after_revoke = http_client
611 .post(format!("{}/oauth/token", url))
612 .form(&[
613 ("grant_type", "refresh_token"),
614 ("refresh_token", refresh_token),
615 ("client_id", &client_id),
616 ])
617 .send()
618 .await
619 .unwrap();
620 assert_eq!(refresh_after_revoke.status(), StatusCode::BAD_REQUEST);
621}
622#[tokio::test]
623async fn test_unsupported_grant_type() {
624 let url = base_url().await;
625 let http_client = client();
626 let res = http_client
627 .post(format!("{}/oauth/token", url))
628 .form(&[
629 ("grant_type", "client_credentials"),
630 ("client_id", "https://example.com"),
631 ])
632 .send()
633 .await
634 .unwrap();
635 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
636 let body: Value = res.json().await.unwrap();
637 assert_eq!(body["error"], "unsupported_grant_type");
638}
639#[tokio::test]
640async fn test_invalid_refresh_token() {
641 let url = base_url().await;
642 let http_client = client();
643 let res = http_client
644 .post(format!("{}/oauth/token", url))
645 .form(&[
646 ("grant_type", "refresh_token"),
647 ("refresh_token", "invalid-refresh-token"),
648 ("client_id", "https://example.com"),
649 ])
650 .send()
651 .await
652 .unwrap();
653 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
654 let body: Value = res.json().await.unwrap();
655 assert_eq!(body["error"], "invalid_grant");
656}
657#[tokio::test]
658async fn test_expired_authorization_request() {
659 let url = base_url().await;
660 let http_client = client();
661 let res = http_client
662 .get(format!("{}/oauth/authorize", url))
663 .header("Accept", "application/json")
664 .query(&[(
665 "request_uri",
666 "urn:ietf:params:oauth:request_uri:expired-or-nonexistent",
667 )])
668 .send()
669 .await
670 .unwrap();
671 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
672 let body: Value = res.json().await.unwrap();
673 assert_eq!(body["error"], "invalid_request");
674}
675#[tokio::test]
676async fn test_token_introspection() {
677 let url = base_url().await;
678 let http_client = client();
679 let ts = Utc::now().timestamp_millis();
680 let handle = format!("introspect-{}", ts);
681 let email = format!("introspect-{}@example.com", ts);
682 let password = "introspect-password";
683 http_client
684 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
685 .json(&json!({
686 "handle": handle,
687 "email": email,
688 "password": password
689 }))
690 .send()
691 .await
692 .unwrap();
693 let redirect_uri = "https://example.com/introspect-callback";
694 let mock_client = setup_mock_client_metadata(redirect_uri).await;
695 let client_id = mock_client.uri();
696 let (code_verifier, code_challenge) = generate_pkce();
697 let par_body: Value = http_client
698 .post(format!("{}/oauth/par", url))
699 .form(&[
700 ("response_type", "code"),
701 ("client_id", &client_id),
702 ("redirect_uri", redirect_uri),
703 ("code_challenge", &code_challenge),
704 ("code_challenge_method", "S256"),
705 ])
706 .send()
707 .await
708 .unwrap()
709 .json()
710 .await
711 .unwrap();
712 let request_uri = par_body["request_uri"].as_str().unwrap();
713 let auth_client = no_redirect_client();
714 let auth_res = auth_client
715 .post(format!("{}/oauth/authorize", url))
716 .form(&[
717 ("request_uri", request_uri),
718 ("username", &handle),
719 ("password", password),
720 ("remember_device", "false"),
721 ])
722 .send()
723 .await
724 .unwrap();
725 let location = auth_res
726 .headers()
727 .get("location")
728 .unwrap()
729 .to_str()
730 .unwrap();
731 let code = location
732 .split("code=")
733 .nth(1)
734 .unwrap()
735 .split('&')
736 .next()
737 .unwrap();
738 let token_body: Value = http_client
739 .post(format!("{}/oauth/token", url))
740 .form(&[
741 ("grant_type", "authorization_code"),
742 ("code", code),
743 ("redirect_uri", redirect_uri),
744 ("code_verifier", &code_verifier),
745 ("client_id", &client_id),
746 ])
747 .send()
748 .await
749 .unwrap()
750 .json()
751 .await
752 .unwrap();
753 let access_token = token_body["access_token"].as_str().unwrap();
754 let introspect_res = http_client
755 .post(format!("{}/oauth/introspect", url))
756 .form(&[("token", access_token)])
757 .send()
758 .await
759 .unwrap();
760 assert_eq!(introspect_res.status(), StatusCode::OK);
761 let introspect_body: Value = introspect_res.json().await.unwrap();
762 assert_eq!(introspect_body["active"], true);
763 assert!(introspect_body["client_id"].is_string());
764 assert!(introspect_body["exp"].is_number());
765}
766#[tokio::test]
767async fn test_introspect_invalid_token() {
768 let url = base_url().await;
769 let http_client = client();
770 let res = http_client
771 .post(format!("{}/oauth/introspect", url))
772 .form(&[("token", "invalid.token.here")])
773 .send()
774 .await
775 .unwrap();
776 assert_eq!(res.status(), StatusCode::OK);
777 let body: Value = res.json().await.unwrap();
778 assert_eq!(body["active"], false);
779}
780#[tokio::test]
781async fn test_introspect_revoked_token() {
782 let url = base_url().await;
783 let http_client = client();
784 let ts = Utc::now().timestamp_millis();
785 let handle = format!("introspect-revoked-{}", ts);
786 let email = format!("introspect-revoked-{}@example.com", ts);
787 let password = "introspect-revoked-password";
788 http_client
789 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
790 .json(&json!({
791 "handle": handle,
792 "email": email,
793 "password": password
794 }))
795 .send()
796 .await
797 .unwrap();
798 let redirect_uri = "https://example.com/introspect-revoked-callback";
799 let mock_client = setup_mock_client_metadata(redirect_uri).await;
800 let client_id = mock_client.uri();
801 let (code_verifier, code_challenge) = generate_pkce();
802 let par_body: Value = http_client
803 .post(format!("{}/oauth/par", url))
804 .form(&[
805 ("response_type", "code"),
806 ("client_id", &client_id),
807 ("redirect_uri", redirect_uri),
808 ("code_challenge", &code_challenge),
809 ("code_challenge_method", "S256"),
810 ])
811 .send()
812 .await
813 .unwrap()
814 .json()
815 .await
816 .unwrap();
817 let request_uri = par_body["request_uri"].as_str().unwrap();
818 let auth_client = no_redirect_client();
819 let auth_res = auth_client
820 .post(format!("{}/oauth/authorize", url))
821 .form(&[
822 ("request_uri", request_uri),
823 ("username", &handle),
824 ("password", password),
825 ("remember_device", "false"),
826 ])
827 .send()
828 .await
829 .unwrap();
830 let location = auth_res
831 .headers()
832 .get("location")
833 .unwrap()
834 .to_str()
835 .unwrap();
836 let code = location
837 .split("code=")
838 .nth(1)
839 .unwrap()
840 .split('&')
841 .next()
842 .unwrap();
843 let token_body: Value = http_client
844 .post(format!("{}/oauth/token", url))
845 .form(&[
846 ("grant_type", "authorization_code"),
847 ("code", code),
848 ("redirect_uri", redirect_uri),
849 ("code_verifier", &code_verifier),
850 ("client_id", &client_id),
851 ])
852 .send()
853 .await
854 .unwrap()
855 .json()
856 .await
857 .unwrap();
858 let access_token = token_body["access_token"].as_str().unwrap();
859 let refresh_token = token_body["refresh_token"].as_str().unwrap();
860 http_client
861 .post(format!("{}/oauth/revoke", url))
862 .form(&[("token", refresh_token)])
863 .send()
864 .await
865 .unwrap();
866 let introspect_res = http_client
867 .post(format!("{}/oauth/introspect", url))
868 .form(&[("token", access_token)])
869 .send()
870 .await
871 .unwrap();
872 assert_eq!(introspect_res.status(), StatusCode::OK);
873 let body: Value = introspect_res.json().await.unwrap();
874 assert_eq!(body["active"], false, "Revoked token should be inactive");
875}
876#[tokio::test]
877async fn test_state_with_special_chars() {
878 let url = base_url().await;
879 let http_client = client();
880 let ts = Utc::now().timestamp_millis();
881 let handle = format!("state-special-{}", ts);
882 let email = format!("state-special-{}@example.com", ts);
883 let password = "state-special-password";
884 http_client
885 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
886 .json(&json!({
887 "handle": handle,
888 "email": email,
889 "password": password
890 }))
891 .send()
892 .await
893 .unwrap();
894 let redirect_uri = "https://example.com/state-special-callback";
895 let mock_client = setup_mock_client_metadata(redirect_uri).await;
896 let client_id = mock_client.uri();
897 let (_code_verifier, code_challenge) = generate_pkce();
898 let special_state = "state=with&special=chars&plus+more";
899 let par_body: Value = http_client
900 .post(format!("{}/oauth/par", url))
901 .form(&[
902 ("response_type", "code"),
903 ("client_id", &client_id),
904 ("redirect_uri", redirect_uri),
905 ("code_challenge", &code_challenge),
906 ("code_challenge_method", "S256"),
907 ("state", special_state),
908 ])
909 .send()
910 .await
911 .unwrap()
912 .json()
913 .await
914 .unwrap();
915 let request_uri = par_body["request_uri"].as_str().unwrap();
916 let auth_client = no_redirect_client();
917 let auth_res = auth_client
918 .post(format!("{}/oauth/authorize", url))
919 .form(&[
920 ("request_uri", request_uri),
921 ("username", &handle),
922 ("password", password),
923 ("remember_device", "false"),
924 ])
925 .send()
926 .await
927 .unwrap();
928 assert!(
929 auth_res.status().is_redirection(),
930 "Should redirect even with special chars in state"
931 );
932 let location = auth_res
933 .headers()
934 .get("location")
935 .unwrap()
936 .to_str()
937 .unwrap();
938 assert!(
939 location.contains("state="),
940 "State should be in redirect URL"
941 );
942 let encoded_state = urlencoding::encode(special_state);
943 assert!(
944 location.contains(&format!("state={}", encoded_state)),
945 "State should be URL-encoded. Got: {}",
946 location
947 );
948}
949#[tokio::test]
950async fn test_2fa_required_when_enabled() {
951 let url = base_url().await;
952 let http_client = client();
953 let ts = Utc::now().timestamp_millis();
954 let handle = format!("2fa-required-{}", ts);
955 let email = format!("2fa-required-{}@example.com", ts);
956 let password = "2fa-test-password";
957 let create_res = http_client
958 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
959 .json(&json!({
960 "handle": handle,
961 "email": email,
962 "password": password
963 }))
964 .send()
965 .await
966 .unwrap();
967 assert_eq!(create_res.status(), StatusCode::OK);
968 let account: Value = create_res.json().await.unwrap();
969 let user_did = account["did"].as_str().unwrap();
970 let db_url = common::get_db_connection_string().await;
971 let pool = sqlx::postgres::PgPoolOptions::new()
972 .max_connections(1)
973 .connect(&db_url)
974 .await
975 .expect("Failed to connect to database");
976 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
977 .bind(user_did)
978 .execute(&pool)
979 .await
980 .expect("Failed to enable 2FA");
981 let redirect_uri = "https://example.com/2fa-callback";
982 let mock_client = setup_mock_client_metadata(redirect_uri).await;
983 let client_id = mock_client.uri();
984 let (_, code_challenge) = generate_pkce();
985 let par_body: Value = http_client
986 .post(format!("{}/oauth/par", url))
987 .form(&[
988 ("response_type", "code"),
989 ("client_id", &client_id),
990 ("redirect_uri", redirect_uri),
991 ("code_challenge", &code_challenge),
992 ("code_challenge_method", "S256"),
993 ])
994 .send()
995 .await
996 .unwrap()
997 .json()
998 .await
999 .unwrap();
1000 let request_uri = par_body["request_uri"].as_str().unwrap();
1001 let auth_client = no_redirect_client();
1002 let auth_res = auth_client
1003 .post(format!("{}/oauth/authorize", url))
1004 .form(&[
1005 ("request_uri", request_uri),
1006 ("username", &handle),
1007 ("password", password),
1008 ("remember_device", "false"),
1009 ])
1010 .send()
1011 .await
1012 .unwrap();
1013 assert!(
1014 auth_res.status().is_redirection(),
1015 "Should redirect to 2FA page, got status: {}",
1016 auth_res.status()
1017 );
1018 let location = auth_res
1019 .headers()
1020 .get("location")
1021 .unwrap()
1022 .to_str()
1023 .unwrap();
1024 assert!(
1025 location.contains("/oauth/authorize/2fa"),
1026 "Should redirect to 2FA page, got: {}",
1027 location
1028 );
1029 assert!(
1030 location.contains("request_uri="),
1031 "2FA redirect should include request_uri"
1032 );
1033}
1034#[tokio::test]
1035async fn test_2fa_invalid_code_rejected() {
1036 let url = base_url().await;
1037 let http_client = client();
1038 let ts = Utc::now().timestamp_millis();
1039 let handle = format!("2fa-invalid-{}", ts);
1040 let email = format!("2fa-invalid-{}@example.com", ts);
1041 let password = "2fa-test-password";
1042 let create_res = http_client
1043 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1044 .json(&json!({
1045 "handle": handle,
1046 "email": email,
1047 "password": password
1048 }))
1049 .send()
1050 .await
1051 .unwrap();
1052 assert_eq!(create_res.status(), StatusCode::OK);
1053 let account: Value = create_res.json().await.unwrap();
1054 let user_did = account["did"].as_str().unwrap();
1055 let db_url = common::get_db_connection_string().await;
1056 let pool = sqlx::postgres::PgPoolOptions::new()
1057 .max_connections(1)
1058 .connect(&db_url)
1059 .await
1060 .expect("Failed to connect to database");
1061 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
1062 .bind(user_did)
1063 .execute(&pool)
1064 .await
1065 .expect("Failed to enable 2FA");
1066 let redirect_uri = "https://example.com/2fa-invalid-callback";
1067 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1068 let client_id = mock_client.uri();
1069 let (_, code_challenge) = generate_pkce();
1070 let par_body: Value = http_client
1071 .post(format!("{}/oauth/par", url))
1072 .form(&[
1073 ("response_type", "code"),
1074 ("client_id", &client_id),
1075 ("redirect_uri", redirect_uri),
1076 ("code_challenge", &code_challenge),
1077 ("code_challenge_method", "S256"),
1078 ])
1079 .send()
1080 .await
1081 .unwrap()
1082 .json()
1083 .await
1084 .unwrap();
1085 let request_uri = par_body["request_uri"].as_str().unwrap();
1086 let auth_client = no_redirect_client();
1087 let auth_res = auth_client
1088 .post(format!("{}/oauth/authorize", url))
1089 .form(&[
1090 ("request_uri", request_uri),
1091 ("username", &handle),
1092 ("password", password),
1093 ("remember_device", "false"),
1094 ])
1095 .send()
1096 .await
1097 .unwrap();
1098 assert!(auth_res.status().is_redirection());
1099 let location = auth_res
1100 .headers()
1101 .get("location")
1102 .unwrap()
1103 .to_str()
1104 .unwrap();
1105 assert!(location.contains("/oauth/authorize/2fa"));
1106 let twofa_res = http_client
1107 .post(format!("{}/oauth/authorize/2fa", url))
1108 .form(&[("request_uri", request_uri), ("code", "000000")])
1109 .send()
1110 .await
1111 .unwrap();
1112 assert_eq!(twofa_res.status(), StatusCode::OK);
1113 let body = twofa_res.text().await.unwrap();
1114 assert!(
1115 body.contains("Invalid verification code") || body.contains("invalid"),
1116 "Should show error for invalid code"
1117 );
1118}
1119#[tokio::test]
1120async fn test_2fa_valid_code_completes_auth() {
1121 let url = base_url().await;
1122 let http_client = client();
1123 let ts = Utc::now().timestamp_millis();
1124 let handle = format!("2fa-valid-{}", ts);
1125 let email = format!("2fa-valid-{}@example.com", ts);
1126 let password = "2fa-test-password";
1127 let create_res = http_client
1128 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1129 .json(&json!({
1130 "handle": handle,
1131 "email": email,
1132 "password": password
1133 }))
1134 .send()
1135 .await
1136 .unwrap();
1137 assert_eq!(create_res.status(), StatusCode::OK);
1138 let account: Value = create_res.json().await.unwrap();
1139 let user_did = account["did"].as_str().unwrap();
1140 let db_url = common::get_db_connection_string().await;
1141 let pool = sqlx::postgres::PgPoolOptions::new()
1142 .max_connections(1)
1143 .connect(&db_url)
1144 .await
1145 .expect("Failed to connect to database");
1146 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
1147 .bind(user_did)
1148 .execute(&pool)
1149 .await
1150 .expect("Failed to enable 2FA");
1151 let redirect_uri = "https://example.com/2fa-valid-callback";
1152 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1153 let client_id = mock_client.uri();
1154 let (code_verifier, code_challenge) = generate_pkce();
1155 let par_body: Value = http_client
1156 .post(format!("{}/oauth/par", url))
1157 .form(&[
1158 ("response_type", "code"),
1159 ("client_id", &client_id),
1160 ("redirect_uri", redirect_uri),
1161 ("code_challenge", &code_challenge),
1162 ("code_challenge_method", "S256"),
1163 ])
1164 .send()
1165 .await
1166 .unwrap()
1167 .json()
1168 .await
1169 .unwrap();
1170 let request_uri = par_body["request_uri"].as_str().unwrap();
1171 let auth_client = no_redirect_client();
1172 let auth_res = auth_client
1173 .post(format!("{}/oauth/authorize", url))
1174 .form(&[
1175 ("request_uri", request_uri),
1176 ("username", &handle),
1177 ("password", password),
1178 ("remember_device", "false"),
1179 ])
1180 .send()
1181 .await
1182 .unwrap();
1183 assert!(auth_res.status().is_redirection());
1184 let twofa_code: String =
1185 sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1")
1186 .bind(request_uri)
1187 .fetch_one(&pool)
1188 .await
1189 .expect("Failed to get 2FA code from database");
1190 let twofa_res = auth_client
1191 .post(format!("{}/oauth/authorize/2fa", url))
1192 .form(&[("request_uri", request_uri), ("code", &twofa_code)])
1193 .send()
1194 .await
1195 .unwrap();
1196 assert!(
1197 twofa_res.status().is_redirection(),
1198 "Valid 2FA code should redirect to success, got status: {}",
1199 twofa_res.status()
1200 );
1201 let location = twofa_res
1202 .headers()
1203 .get("location")
1204 .unwrap()
1205 .to_str()
1206 .unwrap();
1207 assert!(
1208 location.starts_with(redirect_uri),
1209 "Should redirect to client callback, got: {}",
1210 location
1211 );
1212 assert!(
1213 location.contains("code="),
1214 "Redirect should include authorization code"
1215 );
1216 let auth_code = location
1217 .split("code=")
1218 .nth(1)
1219 .unwrap()
1220 .split('&')
1221 .next()
1222 .unwrap();
1223 let token_res = http_client
1224 .post(format!("{}/oauth/token", url))
1225 .form(&[
1226 ("grant_type", "authorization_code"),
1227 ("code", auth_code),
1228 ("redirect_uri", redirect_uri),
1229 ("code_verifier", &code_verifier),
1230 ("client_id", &client_id),
1231 ])
1232 .send()
1233 .await
1234 .unwrap();
1235 assert_eq!(
1236 token_res.status(),
1237 StatusCode::OK,
1238 "Token exchange should succeed"
1239 );
1240 let token_body: Value = token_res.json().await.unwrap();
1241 assert!(token_body["access_token"].is_string());
1242 assert_eq!(token_body["sub"], user_did);
1243}
1244#[tokio::test]
1245async fn test_2fa_lockout_after_max_attempts() {
1246 let url = base_url().await;
1247 let http_client = client();
1248 let ts = Utc::now().timestamp_millis();
1249 let handle = format!("2fa-lockout-{}", ts);
1250 let email = format!("2fa-lockout-{}@example.com", ts);
1251 let password = "2fa-test-password";
1252 let create_res = http_client
1253 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1254 .json(&json!({
1255 "handle": handle,
1256 "email": email,
1257 "password": password
1258 }))
1259 .send()
1260 .await
1261 .unwrap();
1262 assert_eq!(create_res.status(), StatusCode::OK);
1263 let account: Value = create_res.json().await.unwrap();
1264 let user_did = account["did"].as_str().unwrap();
1265 let db_url = common::get_db_connection_string().await;
1266 let pool = sqlx::postgres::PgPoolOptions::new()
1267 .max_connections(1)
1268 .connect(&db_url)
1269 .await
1270 .expect("Failed to connect to database");
1271 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
1272 .bind(user_did)
1273 .execute(&pool)
1274 .await
1275 .expect("Failed to enable 2FA");
1276 let redirect_uri = "https://example.com/2fa-lockout-callback";
1277 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1278 let client_id = mock_client.uri();
1279 let (_, code_challenge) = generate_pkce();
1280 let par_body: Value = http_client
1281 .post(format!("{}/oauth/par", url))
1282 .form(&[
1283 ("response_type", "code"),
1284 ("client_id", &client_id),
1285 ("redirect_uri", redirect_uri),
1286 ("code_challenge", &code_challenge),
1287 ("code_challenge_method", "S256"),
1288 ])
1289 .send()
1290 .await
1291 .unwrap()
1292 .json()
1293 .await
1294 .unwrap();
1295 let request_uri = par_body["request_uri"].as_str().unwrap();
1296 let auth_client = no_redirect_client();
1297 let auth_res = auth_client
1298 .post(format!("{}/oauth/authorize", url))
1299 .form(&[
1300 ("request_uri", request_uri),
1301 ("username", &handle),
1302 ("password", password),
1303 ("remember_device", "false"),
1304 ])
1305 .send()
1306 .await
1307 .unwrap();
1308 assert!(auth_res.status().is_redirection());
1309 for i in 0..5 {
1310 let res = http_client
1311 .post(format!("{}/oauth/authorize/2fa", url))
1312 .form(&[("request_uri", request_uri), ("code", "999999")])
1313 .send()
1314 .await
1315 .unwrap();
1316 if i < 4 {
1317 assert_eq!(
1318 res.status(),
1319 StatusCode::OK,
1320 "Attempt {} should show error page",
1321 i + 1
1322 );
1323 let body = res.text().await.unwrap();
1324 assert!(
1325 body.contains("Invalid verification code"),
1326 "Should show invalid code error on attempt {}",
1327 i + 1
1328 );
1329 }
1330 }
1331 let lockout_res = http_client
1332 .post(format!("{}/oauth/authorize/2fa", url))
1333 .form(&[("request_uri", request_uri), ("code", "999999")])
1334 .send()
1335 .await
1336 .unwrap();
1337 assert_eq!(lockout_res.status(), StatusCode::OK);
1338 let body = lockout_res.text().await.unwrap();
1339 assert!(
1340 body.contains("Too many failed attempts") || body.contains("No 2FA challenge found"),
1341 "Should be locked out after max attempts. Body: {}",
1342 &body[..body.len().min(500)]
1343 );
1344}
1345#[tokio::test]
1346async fn test_account_selector_with_2fa_requires_verification() {
1347 let url = base_url().await;
1348 let http_client = client();
1349 let ts = Utc::now().timestamp_millis();
1350 let handle = format!("selector-2fa-{}", ts);
1351 let email = format!("selector-2fa-{}@example.com", ts);
1352 let password = "selector-2fa-password";
1353 let create_res = http_client
1354 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1355 .json(&json!({
1356 "handle": handle,
1357 "email": email,
1358 "password": password
1359 }))
1360 .send()
1361 .await
1362 .unwrap();
1363 assert_eq!(create_res.status(), StatusCode::OK);
1364 let account: Value = create_res.json().await.unwrap();
1365 let user_did = account["did"].as_str().unwrap().to_string();
1366 let redirect_uri = "https://example.com/selector-2fa-callback";
1367 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1368 let client_id = mock_client.uri();
1369 let (code_verifier, code_challenge) = generate_pkce();
1370 let par_body: Value = http_client
1371 .post(format!("{}/oauth/par", url))
1372 .form(&[
1373 ("response_type", "code"),
1374 ("client_id", &client_id),
1375 ("redirect_uri", redirect_uri),
1376 ("code_challenge", &code_challenge),
1377 ("code_challenge_method", "S256"),
1378 ])
1379 .send()
1380 .await
1381 .unwrap()
1382 .json()
1383 .await
1384 .unwrap();
1385 let request_uri = par_body["request_uri"].as_str().unwrap();
1386 let auth_client = no_redirect_client();
1387 let auth_res = auth_client
1388 .post(format!("{}/oauth/authorize", url))
1389 .form(&[
1390 ("request_uri", request_uri),
1391 ("username", &handle),
1392 ("password", password),
1393 ("remember_device", "true"),
1394 ])
1395 .send()
1396 .await
1397 .unwrap();
1398 assert!(auth_res.status().is_redirection());
1399 let device_cookie = auth_res
1400 .headers()
1401 .get("set-cookie")
1402 .and_then(|v| v.to_str().ok())
1403 .map(|s| s.split(';').next().unwrap_or("").to_string())
1404 .expect("Should have received device cookie");
1405 let location = auth_res
1406 .headers()
1407 .get("location")
1408 .unwrap()
1409 .to_str()
1410 .unwrap();
1411 assert!(location.contains("code="), "First auth should succeed");
1412 let code = location
1413 .split("code=")
1414 .nth(1)
1415 .unwrap()
1416 .split('&')
1417 .next()
1418 .unwrap();
1419 let _token_body: Value = http_client
1420 .post(format!("{}/oauth/token", url))
1421 .form(&[
1422 ("grant_type", "authorization_code"),
1423 ("code", code),
1424 ("redirect_uri", redirect_uri),
1425 ("code_verifier", &code_verifier),
1426 ("client_id", &client_id),
1427 ])
1428 .send()
1429 .await
1430 .unwrap()
1431 .json()
1432 .await
1433 .unwrap();
1434 let db_url = common::get_db_connection_string().await;
1435 let pool = sqlx::postgres::PgPoolOptions::new()
1436 .max_connections(1)
1437 .connect(&db_url)
1438 .await
1439 .expect("Failed to connect to database");
1440 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
1441 .bind(&user_did)
1442 .execute(&pool)
1443 .await
1444 .expect("Failed to enable 2FA");
1445 let (code_verifier2, code_challenge2) = generate_pkce();
1446 let par_body2: Value = http_client
1447 .post(format!("{}/oauth/par", url))
1448 .form(&[
1449 ("response_type", "code"),
1450 ("client_id", &client_id),
1451 ("redirect_uri", redirect_uri),
1452 ("code_challenge", &code_challenge2),
1453 ("code_challenge_method", "S256"),
1454 ])
1455 .send()
1456 .await
1457 .unwrap()
1458 .json()
1459 .await
1460 .unwrap();
1461 let request_uri2 = par_body2["request_uri"].as_str().unwrap();
1462 let select_res = auth_client
1463 .post(format!("{}/oauth/authorize/select", url))
1464 .header("cookie", &device_cookie)
1465 .form(&[("request_uri", request_uri2), ("did", &user_did)])
1466 .send()
1467 .await
1468 .unwrap();
1469 assert!(
1470 select_res.status().is_redirection(),
1471 "Account selector should redirect, got status: {}",
1472 select_res.status()
1473 );
1474 let select_location = select_res
1475 .headers()
1476 .get("location")
1477 .unwrap()
1478 .to_str()
1479 .unwrap();
1480 assert!(
1481 select_location.contains("/oauth/authorize/2fa"),
1482 "Account selector with 2FA enabled should redirect to 2FA page, got: {}",
1483 select_location
1484 );
1485 let twofa_code: String =
1486 sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1")
1487 .bind(request_uri2)
1488 .fetch_one(&pool)
1489 .await
1490 .expect("Failed to get 2FA code");
1491 let twofa_res = auth_client
1492 .post(format!("{}/oauth/authorize/2fa", url))
1493 .header("cookie", &device_cookie)
1494 .form(&[("request_uri", request_uri2), ("code", &twofa_code)])
1495 .send()
1496 .await
1497 .unwrap();
1498 assert!(twofa_res.status().is_redirection());
1499 let final_location = twofa_res
1500 .headers()
1501 .get("location")
1502 .unwrap()
1503 .to_str()
1504 .unwrap();
1505 assert!(
1506 final_location.starts_with(redirect_uri) && final_location.contains("code="),
1507 "After 2FA, should redirect to client with code, got: {}",
1508 final_location
1509 );
1510 let final_code = final_location
1511 .split("code=")
1512 .nth(1)
1513 .unwrap()
1514 .split('&')
1515 .next()
1516 .unwrap();
1517 let token_res = http_client
1518 .post(format!("{}/oauth/token", url))
1519 .form(&[
1520 ("grant_type", "authorization_code"),
1521 ("code", final_code),
1522 ("redirect_uri", redirect_uri),
1523 ("code_verifier", &code_verifier2),
1524 ("client_id", &client_id),
1525 ])
1526 .send()
1527 .await
1528 .unwrap();
1529 assert_eq!(token_res.status(), StatusCode::OK);
1530 let final_token: Value = token_res.json().await.unwrap();
1531 assert_eq!(
1532 final_token["sub"], user_did,
1533 "Token should be for the correct user"
1534 );
1535}