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