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_par_requires_pkce() {
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 res = client
218 .post(format!("{}/oauth/par", url))
219 .form(&[
220 ("response_type", "code"),
221 ("client_id", &client_id),
222 ("redirect_uri", redirect_uri),
223 ("scope", "atproto"),
224 ])
225 .send()
226 .await
227 .expect("Failed to send PAR request");
228
229 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
230
231 let body: Value = res.json().await.expect("Invalid JSON");
232 assert_eq!(body["error"], "invalid_request");
233}
234
235#[tokio::test]
236async fn test_par_requires_s256() {
237 let url = base_url().await;
238 let client = client();
239
240 let redirect_uri = "https://example.com/callback";
241 let mock_client = setup_mock_client_metadata(redirect_uri).await;
242 let client_id = mock_client.uri();
243
244 let res = client
245 .post(format!("{}/oauth/par", url))
246 .form(&[
247 ("response_type", "code"),
248 ("client_id", &client_id),
249 ("redirect_uri", redirect_uri),
250 ("code_challenge", "test-challenge"),
251 ("code_challenge_method", "plain"),
252 ])
253 .send()
254 .await
255 .expect("Failed to send PAR request");
256
257 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
258
259 let body: Value = res.json().await.expect("Invalid JSON");
260 assert_eq!(body["error"], "invalid_request");
261 assert!(body["error_description"].as_str().unwrap().contains("S256"));
262}
263
264#[tokio::test]
265async fn test_par_validates_redirect_uri() {
266 let url = base_url().await;
267 let client = client();
268
269 let registered_redirect = "https://example.com/callback";
270 let wrong_redirect = "https://evil.com/steal";
271 let mock_client = setup_mock_client_metadata(registered_redirect).await;
272 let client_id = mock_client.uri();
273
274 let (_, code_challenge) = generate_pkce();
275
276 let res = client
277 .post(format!("{}/oauth/par", url))
278 .form(&[
279 ("response_type", "code"),
280 ("client_id", &client_id),
281 ("redirect_uri", wrong_redirect),
282 ("code_challenge", &code_challenge),
283 ("code_challenge_method", "S256"),
284 ])
285 .send()
286 .await
287 .expect("Failed to send PAR request");
288
289 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
290
291 let body: Value = res.json().await.expect("Invalid JSON");
292 assert_eq!(body["error"], "invalid_request");
293}
294
295#[tokio::test]
296async fn test_authorize_get_with_valid_request_uri() {
297 let url = base_url().await;
298 let client = client();
299
300 let redirect_uri = "https://example.com/callback";
301 let mock_client = setup_mock_client_metadata(redirect_uri).await;
302 let client_id = mock_client.uri();
303
304 let (_, code_challenge) = generate_pkce();
305
306 let par_res = client
307 .post(format!("{}/oauth/par", url))
308 .form(&[
309 ("response_type", "code"),
310 ("client_id", &client_id),
311 ("redirect_uri", redirect_uri),
312 ("code_challenge", &code_challenge),
313 ("code_challenge_method", "S256"),
314 ("scope", "atproto"),
315 ("state", "test-state"),
316 ])
317 .send()
318 .await
319 .expect("PAR failed");
320
321 let par_body: Value = par_res.json().await.expect("Invalid PAR JSON");
322 let request_uri = par_body["request_uri"].as_str().unwrap();
323
324 let auth_res = client
325 .get(format!("{}/oauth/authorize", url))
326 .header("Accept", "application/json")
327 .query(&[("request_uri", request_uri)])
328 .send()
329 .await
330 .expect("Authorize GET failed");
331
332 assert_eq!(auth_res.status(), StatusCode::OK);
333
334 let auth_body: Value = auth_res.json().await.expect("Invalid auth JSON");
335 assert_eq!(auth_body["client_id"], client_id);
336 assert_eq!(auth_body["redirect_uri"], redirect_uri);
337 assert_eq!(auth_body["scope"], "atproto");
338 assert_eq!(auth_body["state"], "test-state");
339}
340
341#[tokio::test]
342async fn test_authorize_rejects_invalid_request_uri() {
343 let url = base_url().await;
344 let client = client();
345
346 let res = client
347 .get(format!("{}/oauth/authorize", url))
348 .header("Accept", "application/json")
349 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:nonexistent")])
350 .send()
351 .await
352 .expect("Request failed");
353
354 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
355
356 let body: Value = res.json().await.expect("Invalid JSON");
357 assert_eq!(body["error"], "invalid_request");
358}
359
360#[tokio::test]
361async fn test_authorize_requires_request_uri() {
362 let url = base_url().await;
363 let client = client();
364
365 let res = client
366 .get(format!("{}/oauth/authorize", url))
367 .send()
368 .await
369 .expect("Request failed");
370
371 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
372}
373
374#[tokio::test]
375async fn test_full_oauth_flow_without_dpop() {
376 let url = base_url().await;
377 let http_client = client();
378
379 let (_, _user_did) = create_account_and_login(&http_client).await;
380
381 let ts = Utc::now().timestamp_millis();
382 let handle = format!("oauth-test-{}", ts);
383 let email = format!("oauth-test-{}@example.com", ts);
384 let password = "oauth-test-password";
385
386 let create_res = http_client
387 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
388 .json(&json!({
389 "handle": handle,
390 "email": email,
391 "password": password
392 }))
393 .send()
394 .await
395 .expect("Account creation failed");
396
397 assert_eq!(create_res.status(), StatusCode::OK);
398 let account: Value = create_res.json().await.unwrap();
399 let user_did = account["did"].as_str().unwrap();
400
401 let redirect_uri = "https://example.com/oauth/callback";
402 let mock_client = setup_mock_client_metadata(redirect_uri).await;
403 let client_id = mock_client.uri();
404
405 let (code_verifier, code_challenge) = generate_pkce();
406 let state = format!("state-{}", ts);
407
408 let par_res = http_client
409 .post(format!("{}/oauth/par", url))
410 .form(&[
411 ("response_type", "code"),
412 ("client_id", &client_id),
413 ("redirect_uri", redirect_uri),
414 ("code_challenge", &code_challenge),
415 ("code_challenge_method", "S256"),
416 ("scope", "atproto"),
417 ("state", &state),
418 ])
419 .send()
420 .await
421 .expect("PAR failed");
422
423 let par_status = par_res.status();
424 let par_text = par_res.text().await.unwrap_or_default();
425 if par_status != StatusCode::OK {
426 panic!("PAR failed with status {}: {}", par_status, par_text);
427 }
428 let par_body: Value = serde_json::from_str(&par_text).unwrap();
429 let request_uri = par_body["request_uri"].as_str().unwrap();
430
431 let auth_client = no_redirect_client();
432 let auth_res = auth_client
433 .post(format!("{}/oauth/authorize", url))
434 .form(&[
435 ("request_uri", request_uri),
436 ("username", &handle),
437 ("password", password),
438 ("remember_device", "false"),
439 ])
440 .send()
441 .await
442 .expect("Authorize POST failed");
443
444 let auth_status = auth_res.status();
445 if auth_status != StatusCode::TEMPORARY_REDIRECT
446 && auth_status != StatusCode::SEE_OTHER
447 && auth_status != StatusCode::FOUND
448 {
449 let auth_text = auth_res.text().await.unwrap_or_default();
450 panic!(
451 "Expected redirect, got {}: {}",
452 auth_status, auth_text
453 );
454 }
455
456 let location = auth_res.headers().get("location")
457 .expect("No Location header")
458 .to_str()
459 .unwrap();
460
461 assert!(location.starts_with(redirect_uri), "Redirect to wrong URI: {}", location);
462 assert!(location.contains("code="), "No code in redirect: {}", location);
463 assert!(location.contains(&format!("state={}", state)), "Wrong state in redirect");
464
465 let code = location
466 .split("code=")
467 .nth(1)
468 .unwrap()
469 .split('&')
470 .next()
471 .unwrap();
472
473 let token_res = http_client
474 .post(format!("{}/oauth/token", url))
475 .form(&[
476 ("grant_type", "authorization_code"),
477 ("code", code),
478 ("redirect_uri", redirect_uri),
479 ("code_verifier", &code_verifier),
480 ("client_id", &client_id),
481 ])
482 .send()
483 .await
484 .expect("Token request failed");
485
486 let token_status = token_res.status();
487 let token_text = token_res.text().await.unwrap_or_default();
488 if token_status != StatusCode::OK {
489 panic!("Token request failed with status {}: {}", token_status, token_text);
490 }
491
492 let token_body: Value = serde_json::from_str(&token_text).unwrap();
493
494 assert!(token_body["access_token"].is_string());
495 assert!(token_body["refresh_token"].is_string());
496 assert_eq!(token_body["token_type"], "Bearer");
497 assert!(token_body["expires_in"].is_number());
498 assert_eq!(token_body["sub"], user_did);
499}
500
501#[tokio::test]
502async fn test_token_refresh_flow() {
503 let url = base_url().await;
504 let http_client = client();
505
506 let ts = Utc::now().timestamp_millis();
507 let handle = format!("refresh-test-{}", ts);
508 let email = format!("refresh-test-{}@example.com", ts);
509 let password = "refresh-test-password";
510
511 http_client
512 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
513 .json(&json!({
514 "handle": handle,
515 "email": email,
516 "password": password
517 }))
518 .send()
519 .await
520 .expect("Account creation failed");
521
522 let redirect_uri = "https://example.com/refresh-callback";
523 let mock_client = setup_mock_client_metadata(redirect_uri).await;
524 let client_id = mock_client.uri();
525
526 let (code_verifier, code_challenge) = generate_pkce();
527
528 let par_body: Value = http_client
529 .post(format!("{}/oauth/par", url))
530 .form(&[
531 ("response_type", "code"),
532 ("client_id", &client_id),
533 ("redirect_uri", redirect_uri),
534 ("code_challenge", &code_challenge),
535 ("code_challenge_method", "S256"),
536 ])
537 .send()
538 .await
539 .unwrap()
540 .json()
541 .await
542 .unwrap();
543
544 let request_uri = par_body["request_uri"].as_str().unwrap();
545
546 let auth_client = no_redirect_client();
547 let auth_res = auth_client
548 .post(format!("{}/oauth/authorize", url))
549 .form(&[
550 ("request_uri", request_uri),
551 ("username", &handle),
552 ("password", password),
553 ("remember_device", "false"),
554 ])
555 .send()
556 .await
557 .unwrap();
558
559 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
560 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
561
562 let token_body: Value = http_client
563 .post(format!("{}/oauth/token", url))
564 .form(&[
565 ("grant_type", "authorization_code"),
566 ("code", code),
567 ("redirect_uri", redirect_uri),
568 ("code_verifier", &code_verifier),
569 ("client_id", &client_id),
570 ])
571 .send()
572 .await
573 .unwrap()
574 .json()
575 .await
576 .unwrap();
577
578 let refresh_token = token_body["refresh_token"].as_str().unwrap();
579 let original_access_token = token_body["access_token"].as_str().unwrap();
580
581 let refresh_res = http_client
582 .post(format!("{}/oauth/token", url))
583 .form(&[
584 ("grant_type", "refresh_token"),
585 ("refresh_token", refresh_token),
586 ("client_id", &client_id),
587 ])
588 .send()
589 .await
590 .expect("Refresh request failed");
591
592 assert_eq!(refresh_res.status(), StatusCode::OK);
593
594 let refresh_body: Value = refresh_res.json().await.unwrap();
595
596 assert!(refresh_body["access_token"].is_string());
597 assert!(refresh_body["refresh_token"].is_string());
598
599 let new_access_token = refresh_body["access_token"].as_str().unwrap();
600 let new_refresh_token = refresh_body["refresh_token"].as_str().unwrap();
601
602 assert_ne!(new_access_token, original_access_token, "Access token should rotate");
603 assert_ne!(new_refresh_token, refresh_token, "Refresh token should rotate");
604}
605
606#[tokio::test]
607async fn test_refresh_token_reuse_detection() {
608 let url = base_url().await;
609 let http_client = client();
610
611 let ts = Utc::now().timestamp_millis();
612 let handle = format!("reuse-test-{}", ts);
613 let email = format!("reuse-test-{}@example.com", ts);
614 let password = "reuse-test-password";
615
616 http_client
617 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
618 .json(&json!({
619 "handle": handle,
620 "email": email,
621 "password": password
622 }))
623 .send()
624 .await
625 .unwrap();
626
627 let redirect_uri = "https://example.com/reuse-callback";
628 let mock_client = setup_mock_client_metadata(redirect_uri).await;
629 let client_id = mock_client.uri();
630
631 let (code_verifier, code_challenge) = generate_pkce();
632
633 let par_body: Value = http_client
634 .post(format!("{}/oauth/par", url))
635 .form(&[
636 ("response_type", "code"),
637 ("client_id", &client_id),
638 ("redirect_uri", redirect_uri),
639 ("code_challenge", &code_challenge),
640 ("code_challenge_method", "S256"),
641 ])
642 .send()
643 .await
644 .unwrap()
645 .json()
646 .await
647 .unwrap();
648
649 let request_uri = par_body["request_uri"].as_str().unwrap();
650
651 let auth_client = no_redirect_client();
652 let auth_res = auth_client
653 .post(format!("{}/oauth/authorize", url))
654 .form(&[
655 ("request_uri", request_uri),
656 ("username", &handle),
657 ("password", password),
658 ("remember_device", "false"),
659 ])
660 .send()
661 .await
662 .unwrap();
663
664 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
665 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
666
667 let token_body: Value = http_client
668 .post(format!("{}/oauth/token", url))
669 .form(&[
670 ("grant_type", "authorization_code"),
671 ("code", code),
672 ("redirect_uri", redirect_uri),
673 ("code_verifier", &code_verifier),
674 ("client_id", &client_id),
675 ])
676 .send()
677 .await
678 .unwrap()
679 .json()
680 .await
681 .unwrap();
682
683 let original_refresh_token = token_body["refresh_token"].as_str().unwrap().to_string();
684
685 let first_refresh: Value = http_client
686 .post(format!("{}/oauth/token", url))
687 .form(&[
688 ("grant_type", "refresh_token"),
689 ("refresh_token", &original_refresh_token),
690 ("client_id", &client_id),
691 ])
692 .send()
693 .await
694 .unwrap()
695 .json()
696 .await
697 .unwrap();
698
699 assert!(first_refresh["access_token"].is_string(), "First refresh should succeed");
700
701 let reuse_res = http_client
702 .post(format!("{}/oauth/token", url))
703 .form(&[
704 ("grant_type", "refresh_token"),
705 ("refresh_token", &original_refresh_token),
706 ("client_id", &client_id),
707 ])
708 .send()
709 .await
710 .unwrap();
711
712 assert_eq!(reuse_res.status(), StatusCode::BAD_REQUEST, "Reuse should be rejected");
713
714 let reuse_body: Value = reuse_res.json().await.unwrap();
715 assert_eq!(reuse_body["error"], "invalid_grant");
716 assert!(
717 reuse_body["error_description"].as_str().unwrap().to_lowercase().contains("reuse"),
718 "Error should mention reuse"
719 );
720}
721
722#[tokio::test]
723async fn test_pkce_verification() {
724 let url = base_url().await;
725 let http_client = client();
726
727 let ts = Utc::now().timestamp_millis();
728 let handle = format!("pkce-test-{}", ts);
729 let email = format!("pkce-test-{}@example.com", ts);
730 let password = "pkce-test-password";
731
732 http_client
733 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
734 .json(&json!({
735 "handle": handle,
736 "email": email,
737 "password": password
738 }))
739 .send()
740 .await
741 .unwrap();
742
743 let redirect_uri = "https://example.com/pkce-callback";
744 let mock_client = setup_mock_client_metadata(redirect_uri).await;
745 let client_id = mock_client.uri();
746
747 let (_, code_challenge) = generate_pkce();
748 let wrong_verifier = "wrong-code-verifier-that-does-not-match";
749
750 let par_body: Value = http_client
751 .post(format!("{}/oauth/par", url))
752 .form(&[
753 ("response_type", "code"),
754 ("client_id", &client_id),
755 ("redirect_uri", redirect_uri),
756 ("code_challenge", &code_challenge),
757 ("code_challenge_method", "S256"),
758 ])
759 .send()
760 .await
761 .unwrap()
762 .json()
763 .await
764 .unwrap();
765
766 let request_uri = par_body["request_uri"].as_str().unwrap();
767
768 let auth_client = no_redirect_client();
769 let auth_res = auth_client
770 .post(format!("{}/oauth/authorize", url))
771 .form(&[
772 ("request_uri", request_uri),
773 ("username", &handle),
774 ("password", password),
775 ("remember_device", "false"),
776 ])
777 .send()
778 .await
779 .unwrap();
780
781 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
782 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
783
784 let token_res = http_client
785 .post(format!("{}/oauth/token", url))
786 .form(&[
787 ("grant_type", "authorization_code"),
788 ("code", code),
789 ("redirect_uri", redirect_uri),
790 ("code_verifier", wrong_verifier),
791 ("client_id", &client_id),
792 ])
793 .send()
794 .await
795 .unwrap();
796
797 assert_eq!(token_res.status(), StatusCode::BAD_REQUEST);
798
799 let token_body: Value = token_res.json().await.unwrap();
800 assert_eq!(token_body["error"], "invalid_grant");
801 assert!(token_body["error_description"].as_str().unwrap().contains("PKCE"));
802}
803
804#[tokio::test]
805async fn test_authorization_code_cannot_be_reused() {
806 let url = base_url().await;
807 let http_client = client();
808
809 let ts = Utc::now().timestamp_millis();
810 let handle = format!("code-reuse-{}", ts);
811 let email = format!("code-reuse-{}@example.com", ts);
812 let password = "code-reuse-password";
813
814 http_client
815 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
816 .json(&json!({
817 "handle": handle,
818 "email": email,
819 "password": password
820 }))
821 .send()
822 .await
823 .unwrap();
824
825 let redirect_uri = "https://example.com/code-reuse-callback";
826 let mock_client = setup_mock_client_metadata(redirect_uri).await;
827 let client_id = mock_client.uri();
828
829 let (code_verifier, code_challenge) = generate_pkce();
830
831 let par_body: Value = http_client
832 .post(format!("{}/oauth/par", url))
833 .form(&[
834 ("response_type", "code"),
835 ("client_id", &client_id),
836 ("redirect_uri", redirect_uri),
837 ("code_challenge", &code_challenge),
838 ("code_challenge_method", "S256"),
839 ])
840 .send()
841 .await
842 .unwrap()
843 .json()
844 .await
845 .unwrap();
846
847 let request_uri = par_body["request_uri"].as_str().unwrap();
848
849 let auth_client = no_redirect_client();
850 let auth_res = auth_client
851 .post(format!("{}/oauth/authorize", url))
852 .form(&[
853 ("request_uri", request_uri),
854 ("username", &handle),
855 ("password", password),
856 ("remember_device", "false"),
857 ])
858 .send()
859 .await
860 .unwrap();
861
862 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
863 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
864
865 let first_token_res = http_client
866 .post(format!("{}/oauth/token", url))
867 .form(&[
868 ("grant_type", "authorization_code"),
869 ("code", code),
870 ("redirect_uri", redirect_uri),
871 ("code_verifier", &code_verifier),
872 ("client_id", &client_id),
873 ])
874 .send()
875 .await
876 .unwrap();
877
878 assert_eq!(first_token_res.status(), StatusCode::OK, "First use should succeed");
879
880 let second_token_res = http_client
881 .post(format!("{}/oauth/token", url))
882 .form(&[
883 ("grant_type", "authorization_code"),
884 ("code", code),
885 ("redirect_uri", redirect_uri),
886 ("code_verifier", &code_verifier),
887 ("client_id", &client_id),
888 ])
889 .send()
890 .await
891 .unwrap();
892
893 assert_eq!(second_token_res.status(), StatusCode::BAD_REQUEST, "Second use should fail");
894
895 let error_body: Value = second_token_res.json().await.unwrap();
896 assert_eq!(error_body["error"], "invalid_grant");
897}
898
899#[tokio::test]
900async fn test_wrong_credentials_denied() {
901 let url = base_url().await;
902 let http_client = client();
903
904 let ts = Utc::now().timestamp_millis();
905 let handle = format!("wrong-creds-{}", ts);
906 let email = format!("wrong-creds-{}@example.com", ts);
907 let password = "correct-password";
908
909 http_client
910 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
911 .json(&json!({
912 "handle": handle,
913 "email": email,
914 "password": password
915 }))
916 .send()
917 .await
918 .unwrap();
919
920 let redirect_uri = "https://example.com/wrong-creds-callback";
921 let mock_client = setup_mock_client_metadata(redirect_uri).await;
922 let client_id = mock_client.uri();
923
924 let (_, code_challenge) = generate_pkce();
925
926 let par_body: Value = http_client
927 .post(format!("{}/oauth/par", url))
928 .form(&[
929 ("response_type", "code"),
930 ("client_id", &client_id),
931 ("redirect_uri", redirect_uri),
932 ("code_challenge", &code_challenge),
933 ("code_challenge_method", "S256"),
934 ])
935 .send()
936 .await
937 .unwrap()
938 .json()
939 .await
940 .unwrap();
941
942 let request_uri = par_body["request_uri"].as_str().unwrap();
943
944 let auth_res = http_client
945 .post(format!("{}/oauth/authorize", url))
946 .header("Accept", "application/json")
947 .form(&[
948 ("request_uri", request_uri),
949 ("username", &handle),
950 ("password", "wrong-password"),
951 ("remember_device", "false"),
952 ])
953 .send()
954 .await
955 .unwrap();
956
957 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN);
958
959 let error_body: Value = auth_res.json().await.unwrap();
960 assert_eq!(error_body["error"], "access_denied");
961}
962
963#[tokio::test]
964async fn test_token_revocation() {
965 let url = base_url().await;
966 let http_client = client();
967
968 let ts = Utc::now().timestamp_millis();
969 let handle = format!("revoke-test-{}", ts);
970 let email = format!("revoke-test-{}@example.com", ts);
971 let password = "revoke-test-password";
972
973 http_client
974 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
975 .json(&json!({
976 "handle": handle,
977 "email": email,
978 "password": password
979 }))
980 .send()
981 .await
982 .unwrap();
983
984 let redirect_uri = "https://example.com/revoke-callback";
985 let mock_client = setup_mock_client_metadata(redirect_uri).await;
986 let client_id = mock_client.uri();
987
988 let (code_verifier, code_challenge) = generate_pkce();
989
990 let par_body: Value = http_client
991 .post(format!("{}/oauth/par", url))
992 .form(&[
993 ("response_type", "code"),
994 ("client_id", &client_id),
995 ("redirect_uri", redirect_uri),
996 ("code_challenge", &code_challenge),
997 ("code_challenge_method", "S256"),
998 ])
999 .send()
1000 .await
1001 .unwrap()
1002 .json()
1003 .await
1004 .unwrap();
1005
1006 let request_uri = par_body["request_uri"].as_str().unwrap();
1007
1008 let auth_client = no_redirect_client();
1009 let auth_res = auth_client
1010 .post(format!("{}/oauth/authorize", url))
1011 .form(&[
1012 ("request_uri", request_uri),
1013 ("username", &handle),
1014 ("password", password),
1015 ("remember_device", "false"),
1016 ])
1017 .send()
1018 .await
1019 .unwrap();
1020
1021 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
1022 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
1023
1024 let token_body: Value = http_client
1025 .post(format!("{}/oauth/token", url))
1026 .form(&[
1027 ("grant_type", "authorization_code"),
1028 ("code", code),
1029 ("redirect_uri", redirect_uri),
1030 ("code_verifier", &code_verifier),
1031 ("client_id", &client_id),
1032 ])
1033 .send()
1034 .await
1035 .unwrap()
1036 .json()
1037 .await
1038 .unwrap();
1039
1040 let refresh_token = token_body["refresh_token"].as_str().unwrap();
1041
1042 let revoke_res = http_client
1043 .post(format!("{}/oauth/revoke", url))
1044 .form(&[("token", refresh_token)])
1045 .send()
1046 .await
1047 .unwrap();
1048
1049 assert_eq!(revoke_res.status(), StatusCode::OK);
1050
1051 let refresh_after_revoke = http_client
1052 .post(format!("{}/oauth/token", url))
1053 .form(&[
1054 ("grant_type", "refresh_token"),
1055 ("refresh_token", refresh_token),
1056 ("client_id", &client_id),
1057 ])
1058 .send()
1059 .await
1060 .unwrap();
1061
1062 assert_eq!(refresh_after_revoke.status(), StatusCode::BAD_REQUEST);
1063}
1064
1065#[tokio::test]
1066async fn test_unsupported_grant_type() {
1067 let url = base_url().await;
1068 let http_client = client();
1069
1070 let res = http_client
1071 .post(format!("{}/oauth/token", url))
1072 .form(&[
1073 ("grant_type", "client_credentials"),
1074 ("client_id", "https://example.com"),
1075 ])
1076 .send()
1077 .await
1078 .unwrap();
1079
1080 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
1081
1082 let body: Value = res.json().await.unwrap();
1083 assert_eq!(body["error"], "unsupported_grant_type");
1084}
1085
1086#[tokio::test]
1087async fn test_invalid_refresh_token() {
1088 let url = base_url().await;
1089 let http_client = client();
1090
1091 let res = http_client
1092 .post(format!("{}/oauth/token", url))
1093 .form(&[
1094 ("grant_type", "refresh_token"),
1095 ("refresh_token", "invalid-refresh-token"),
1096 ("client_id", "https://example.com"),
1097 ])
1098 .send()
1099 .await
1100 .unwrap();
1101
1102 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
1103
1104 let body: Value = res.json().await.unwrap();
1105 assert_eq!(body["error"], "invalid_grant");
1106}
1107
1108#[tokio::test]
1109async fn test_deactivated_account_cannot_authorize() {
1110 let url = base_url().await;
1111 let http_client = client();
1112
1113 let ts = Utc::now().timestamp_millis();
1114 let handle = format!("deact-oauth-{}", ts);
1115 let email = format!("deact-oauth-{}@example.com", ts);
1116 let password = "deact-oauth-password";
1117
1118 let create_res = http_client
1119 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1120 .json(&json!({
1121 "handle": handle,
1122 "email": email,
1123 "password": password
1124 }))
1125 .send()
1126 .await
1127 .unwrap();
1128
1129 assert_eq!(create_res.status(), StatusCode::OK);
1130 let account: Value = create_res.json().await.unwrap();
1131 let access_jwt = account["accessJwt"].as_str().unwrap();
1132
1133 let deact_res = http_client
1134 .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url))
1135 .header("Authorization", format!("Bearer {}", access_jwt))
1136 .json(&json!({}))
1137 .send()
1138 .await
1139 .unwrap();
1140 assert_eq!(deact_res.status(), StatusCode::OK);
1141
1142 let redirect_uri = "https://example.com/deact-callback";
1143 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1144 let client_id = mock_client.uri();
1145
1146 let (_, code_challenge) = generate_pkce();
1147
1148 let par_body: Value = http_client
1149 .post(format!("{}/oauth/par", url))
1150 .form(&[
1151 ("response_type", "code"),
1152 ("client_id", &client_id),
1153 ("redirect_uri", redirect_uri),
1154 ("code_challenge", &code_challenge),
1155 ("code_challenge_method", "S256"),
1156 ])
1157 .send()
1158 .await
1159 .unwrap()
1160 .json()
1161 .await
1162 .unwrap();
1163
1164 let request_uri = par_body["request_uri"].as_str().unwrap();
1165
1166 let auth_res = http_client
1167 .post(format!("{}/oauth/authorize", url))
1168 .header("Accept", "application/json")
1169 .form(&[
1170 ("request_uri", request_uri),
1171 ("username", &handle),
1172 ("password", password),
1173 ("remember_device", "false"),
1174 ])
1175 .send()
1176 .await
1177 .unwrap();
1178
1179 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN, "Deactivated account should not be able to authorize");
1180 let body: Value = auth_res.json().await.unwrap();
1181 assert_eq!(body["error"], "access_denied");
1182}
1183
1184#[tokio::test]
1185async fn test_expired_authorization_request() {
1186 let url = base_url().await;
1187 let http_client = client();
1188
1189 let res = http_client
1190 .get(format!("{}/oauth/authorize", url))
1191 .header("Accept", "application/json")
1192 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:expired-or-nonexistent")])
1193 .send()
1194 .await
1195 .unwrap();
1196
1197 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
1198 let body: Value = res.json().await.unwrap();
1199 assert_eq!(body["error"], "invalid_request");
1200}
1201
1202#[tokio::test]
1203async fn test_token_introspection() {
1204 let url = base_url().await;
1205 let http_client = client();
1206
1207 let ts = Utc::now().timestamp_millis();
1208 let handle = format!("introspect-{}", ts);
1209 let email = format!("introspect-{}@example.com", ts);
1210 let password = "introspect-password";
1211
1212 http_client
1213 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1214 .json(&json!({
1215 "handle": handle,
1216 "email": email,
1217 "password": password
1218 }))
1219 .send()
1220 .await
1221 .unwrap();
1222
1223 let redirect_uri = "https://example.com/introspect-callback";
1224 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1225 let client_id = mock_client.uri();
1226
1227 let (code_verifier, code_challenge) = generate_pkce();
1228
1229 let par_body: Value = http_client
1230 .post(format!("{}/oauth/par", url))
1231 .form(&[
1232 ("response_type", "code"),
1233 ("client_id", &client_id),
1234 ("redirect_uri", redirect_uri),
1235 ("code_challenge", &code_challenge),
1236 ("code_challenge_method", "S256"),
1237 ])
1238 .send()
1239 .await
1240 .unwrap()
1241 .json()
1242 .await
1243 .unwrap();
1244
1245 let request_uri = par_body["request_uri"].as_str().unwrap();
1246
1247 let auth_client = no_redirect_client();
1248 let auth_res = auth_client
1249 .post(format!("{}/oauth/authorize", url))
1250 .form(&[
1251 ("request_uri", request_uri),
1252 ("username", &handle),
1253 ("password", password),
1254 ("remember_device", "false"),
1255 ])
1256 .send()
1257 .await
1258 .unwrap();
1259
1260 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
1261 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
1262
1263 let token_body: Value = http_client
1264 .post(format!("{}/oauth/token", url))
1265 .form(&[
1266 ("grant_type", "authorization_code"),
1267 ("code", code),
1268 ("redirect_uri", redirect_uri),
1269 ("code_verifier", &code_verifier),
1270 ("client_id", &client_id),
1271 ])
1272 .send()
1273 .await
1274 .unwrap()
1275 .json()
1276 .await
1277 .unwrap();
1278
1279 let access_token = token_body["access_token"].as_str().unwrap();
1280
1281 let introspect_res = http_client
1282 .post(format!("{}/oauth/introspect", url))
1283 .form(&[("token", access_token)])
1284 .send()
1285 .await
1286 .unwrap();
1287
1288 assert_eq!(introspect_res.status(), StatusCode::OK);
1289 let introspect_body: Value = introspect_res.json().await.unwrap();
1290 assert_eq!(introspect_body["active"], true);
1291 assert!(introspect_body["client_id"].is_string());
1292 assert!(introspect_body["exp"].is_number());
1293}
1294
1295#[tokio::test]
1296async fn test_introspect_invalid_token() {
1297 let url = base_url().await;
1298 let http_client = client();
1299
1300 let res = http_client
1301 .post(format!("{}/oauth/introspect", url))
1302 .form(&[("token", "invalid.token.here")])
1303 .send()
1304 .await
1305 .unwrap();
1306
1307 assert_eq!(res.status(), StatusCode::OK);
1308 let body: Value = res.json().await.unwrap();
1309 assert_eq!(body["active"], false);
1310}
1311
1312#[tokio::test]
1313async fn test_introspect_revoked_token() {
1314 let url = base_url().await;
1315 let http_client = client();
1316
1317 let ts = Utc::now().timestamp_millis();
1318 let handle = format!("introspect-revoked-{}", ts);
1319 let email = format!("introspect-revoked-{}@example.com", ts);
1320 let password = "introspect-revoked-password";
1321
1322 http_client
1323 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1324 .json(&json!({
1325 "handle": handle,
1326 "email": email,
1327 "password": password
1328 }))
1329 .send()
1330 .await
1331 .unwrap();
1332
1333 let redirect_uri = "https://example.com/introspect-revoked-callback";
1334 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1335 let client_id = mock_client.uri();
1336
1337 let (code_verifier, code_challenge) = generate_pkce();
1338
1339 let par_body: Value = http_client
1340 .post(format!("{}/oauth/par", url))
1341 .form(&[
1342 ("response_type", "code"),
1343 ("client_id", &client_id),
1344 ("redirect_uri", redirect_uri),
1345 ("code_challenge", &code_challenge),
1346 ("code_challenge_method", "S256"),
1347 ])
1348 .send()
1349 .await
1350 .unwrap()
1351 .json()
1352 .await
1353 .unwrap();
1354
1355 let request_uri = par_body["request_uri"].as_str().unwrap();
1356
1357 let auth_client = no_redirect_client();
1358 let auth_res = auth_client
1359 .post(format!("{}/oauth/authorize", url))
1360 .form(&[
1361 ("request_uri", request_uri),
1362 ("username", &handle),
1363 ("password", password),
1364 ("remember_device", "false"),
1365 ])
1366 .send()
1367 .await
1368 .unwrap();
1369
1370 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
1371 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
1372
1373 let token_body: Value = http_client
1374 .post(format!("{}/oauth/token", url))
1375 .form(&[
1376 ("grant_type", "authorization_code"),
1377 ("code", code),
1378 ("redirect_uri", redirect_uri),
1379 ("code_verifier", &code_verifier),
1380 ("client_id", &client_id),
1381 ])
1382 .send()
1383 .await
1384 .unwrap()
1385 .json()
1386 .await
1387 .unwrap();
1388
1389 let access_token = token_body["access_token"].as_str().unwrap();
1390 let refresh_token = token_body["refresh_token"].as_str().unwrap();
1391
1392 http_client
1393 .post(format!("{}/oauth/revoke", url))
1394 .form(&[("token", refresh_token)])
1395 .send()
1396 .await
1397 .unwrap();
1398
1399 let introspect_res = http_client
1400 .post(format!("{}/oauth/introspect", url))
1401 .form(&[("token", access_token)])
1402 .send()
1403 .await
1404 .unwrap();
1405
1406 assert_eq!(introspect_res.status(), StatusCode::OK);
1407 let body: Value = introspect_res.json().await.unwrap();
1408 assert_eq!(body["active"], false, "Revoked token should be inactive");
1409}
1410
1411#[tokio::test]
1412async fn test_state_with_special_chars() {
1413 let url = base_url().await;
1414 let http_client = client();
1415
1416 let ts = Utc::now().timestamp_millis();
1417 let handle = format!("state-special-{}", ts);
1418 let email = format!("state-special-{}@example.com", ts);
1419 let password = "state-special-password";
1420
1421 http_client
1422 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1423 .json(&json!({
1424 "handle": handle,
1425 "email": email,
1426 "password": password
1427 }))
1428 .send()
1429 .await
1430 .unwrap();
1431
1432 let redirect_uri = "https://example.com/state-special-callback";
1433 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1434 let client_id = mock_client.uri();
1435
1436 let (_code_verifier, code_challenge) = generate_pkce();
1437 let special_state = "state=with&special=chars&plus+more";
1438
1439 let par_body: Value = http_client
1440 .post(format!("{}/oauth/par", url))
1441 .form(&[
1442 ("response_type", "code"),
1443 ("client_id", &client_id),
1444 ("redirect_uri", redirect_uri),
1445 ("code_challenge", &code_challenge),
1446 ("code_challenge_method", "S256"),
1447 ("state", special_state),
1448 ])
1449 .send()
1450 .await
1451 .unwrap()
1452 .json()
1453 .await
1454 .unwrap();
1455
1456 let request_uri = par_body["request_uri"].as_str().unwrap();
1457
1458 let auth_client = no_redirect_client();
1459 let auth_res = auth_client
1460 .post(format!("{}/oauth/authorize", url))
1461 .form(&[
1462 ("request_uri", request_uri),
1463 ("username", &handle),
1464 ("password", password),
1465 ("remember_device", "false"),
1466 ])
1467 .send()
1468 .await
1469 .unwrap();
1470
1471 assert!(
1472 auth_res.status().is_redirection(),
1473 "Should redirect even with special chars in state"
1474 );
1475 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
1476 assert!(location.contains("state="), "State should be in redirect URL");
1477
1478 let encoded_state = urlencoding::encode(special_state);
1479 assert!(
1480 location.contains(&format!("state={}", encoded_state)),
1481 "State should be URL-encoded. Got: {}",
1482 location
1483 );
1484}
1485
1486#[tokio::test]
1487async fn test_2fa_required_when_enabled() {
1488 let url = base_url().await;
1489 let http_client = client();
1490
1491 let ts = Utc::now().timestamp_millis();
1492 let handle = format!("2fa-required-{}", ts);
1493 let email = format!("2fa-required-{}@example.com", ts);
1494 let password = "2fa-test-password";
1495
1496 let create_res = http_client
1497 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1498 .json(&json!({
1499 "handle": handle,
1500 "email": email,
1501 "password": password
1502 }))
1503 .send()
1504 .await
1505 .unwrap();
1506 assert_eq!(create_res.status(), StatusCode::OK);
1507 let account: Value = create_res.json().await.unwrap();
1508 let user_did = account["did"].as_str().unwrap();
1509
1510 let db_url = common::get_db_connection_string().await;
1511 let pool = sqlx::postgres::PgPoolOptions::new()
1512 .max_connections(1)
1513 .connect(&db_url)
1514 .await
1515 .expect("Failed to connect to database");
1516
1517 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
1518 .bind(user_did)
1519 .execute(&pool)
1520 .await
1521 .expect("Failed to enable 2FA");
1522
1523 let redirect_uri = "https://example.com/2fa-callback";
1524 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1525 let client_id = mock_client.uri();
1526
1527 let (_, code_challenge) = generate_pkce();
1528
1529 let par_body: Value = http_client
1530 .post(format!("{}/oauth/par", url))
1531 .form(&[
1532 ("response_type", "code"),
1533 ("client_id", &client_id),
1534 ("redirect_uri", redirect_uri),
1535 ("code_challenge", &code_challenge),
1536 ("code_challenge_method", "S256"),
1537 ])
1538 .send()
1539 .await
1540 .unwrap()
1541 .json()
1542 .await
1543 .unwrap();
1544
1545 let request_uri = par_body["request_uri"].as_str().unwrap();
1546
1547 let auth_client = no_redirect_client();
1548 let auth_res = auth_client
1549 .post(format!("{}/oauth/authorize", url))
1550 .form(&[
1551 ("request_uri", request_uri),
1552 ("username", &handle),
1553 ("password", password),
1554 ("remember_device", "false"),
1555 ])
1556 .send()
1557 .await
1558 .unwrap();
1559
1560 assert!(
1561 auth_res.status().is_redirection(),
1562 "Should redirect to 2FA page, got status: {}",
1563 auth_res.status()
1564 );
1565
1566 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
1567 assert!(
1568 location.contains("/oauth/authorize/2fa"),
1569 "Should redirect to 2FA page, got: {}",
1570 location
1571 );
1572 assert!(
1573 location.contains("request_uri="),
1574 "2FA redirect should include request_uri"
1575 );
1576}
1577
1578#[tokio::test]
1579async fn test_2fa_invalid_code_rejected() {
1580 let url = base_url().await;
1581 let http_client = client();
1582
1583 let ts = Utc::now().timestamp_millis();
1584 let handle = format!("2fa-invalid-{}", ts);
1585 let email = format!("2fa-invalid-{}@example.com", ts);
1586 let password = "2fa-test-password";
1587
1588 let create_res = http_client
1589 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1590 .json(&json!({
1591 "handle": handle,
1592 "email": email,
1593 "password": password
1594 }))
1595 .send()
1596 .await
1597 .unwrap();
1598 assert_eq!(create_res.status(), StatusCode::OK);
1599 let account: Value = create_res.json().await.unwrap();
1600 let user_did = account["did"].as_str().unwrap();
1601
1602 let db_url = common::get_db_connection_string().await;
1603 let pool = sqlx::postgres::PgPoolOptions::new()
1604 .max_connections(1)
1605 .connect(&db_url)
1606 .await
1607 .expect("Failed to connect to database");
1608
1609 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
1610 .bind(user_did)
1611 .execute(&pool)
1612 .await
1613 .expect("Failed to enable 2FA");
1614
1615 let redirect_uri = "https://example.com/2fa-invalid-callback";
1616 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1617 let client_id = mock_client.uri();
1618
1619 let (_, code_challenge) = generate_pkce();
1620
1621 let par_body: Value = http_client
1622 .post(format!("{}/oauth/par", url))
1623 .form(&[
1624 ("response_type", "code"),
1625 ("client_id", &client_id),
1626 ("redirect_uri", redirect_uri),
1627 ("code_challenge", &code_challenge),
1628 ("code_challenge_method", "S256"),
1629 ])
1630 .send()
1631 .await
1632 .unwrap()
1633 .json()
1634 .await
1635 .unwrap();
1636
1637 let request_uri = par_body["request_uri"].as_str().unwrap();
1638
1639 let auth_client = no_redirect_client();
1640 let auth_res = auth_client
1641 .post(format!("{}/oauth/authorize", url))
1642 .form(&[
1643 ("request_uri", request_uri),
1644 ("username", &handle),
1645 ("password", password),
1646 ("remember_device", "false"),
1647 ])
1648 .send()
1649 .await
1650 .unwrap();
1651
1652 assert!(auth_res.status().is_redirection());
1653 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
1654 assert!(location.contains("/oauth/authorize/2fa"));
1655
1656 let twofa_res = http_client
1657 .post(format!("{}/oauth/authorize/2fa", url))
1658 .form(&[
1659 ("request_uri", request_uri),
1660 ("code", "000000"),
1661 ])
1662 .send()
1663 .await
1664 .unwrap();
1665
1666 assert_eq!(twofa_res.status(), StatusCode::OK);
1667 let body = twofa_res.text().await.unwrap();
1668 assert!(
1669 body.contains("Invalid verification code") || body.contains("invalid"),
1670 "Should show error for invalid code"
1671 );
1672}
1673
1674#[tokio::test]
1675async fn test_2fa_valid_code_completes_auth() {
1676 let url = base_url().await;
1677 let http_client = client();
1678
1679 let ts = Utc::now().timestamp_millis();
1680 let handle = format!("2fa-valid-{}", ts);
1681 let email = format!("2fa-valid-{}@example.com", ts);
1682 let password = "2fa-test-password";
1683
1684 let create_res = http_client
1685 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1686 .json(&json!({
1687 "handle": handle,
1688 "email": email,
1689 "password": password
1690 }))
1691 .send()
1692 .await
1693 .unwrap();
1694 assert_eq!(create_res.status(), StatusCode::OK);
1695 let account: Value = create_res.json().await.unwrap();
1696 let user_did = account["did"].as_str().unwrap();
1697
1698 let db_url = common::get_db_connection_string().await;
1699 let pool = sqlx::postgres::PgPoolOptions::new()
1700 .max_connections(1)
1701 .connect(&db_url)
1702 .await
1703 .expect("Failed to connect to database");
1704
1705 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
1706 .bind(user_did)
1707 .execute(&pool)
1708 .await
1709 .expect("Failed to enable 2FA");
1710
1711 let redirect_uri = "https://example.com/2fa-valid-callback";
1712 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1713 let client_id = mock_client.uri();
1714
1715 let (code_verifier, code_challenge) = generate_pkce();
1716
1717 let par_body: Value = http_client
1718 .post(format!("{}/oauth/par", url))
1719 .form(&[
1720 ("response_type", "code"),
1721 ("client_id", &client_id),
1722 ("redirect_uri", redirect_uri),
1723 ("code_challenge", &code_challenge),
1724 ("code_challenge_method", "S256"),
1725 ])
1726 .send()
1727 .await
1728 .unwrap()
1729 .json()
1730 .await
1731 .unwrap();
1732
1733 let request_uri = par_body["request_uri"].as_str().unwrap();
1734
1735 let auth_client = no_redirect_client();
1736 let auth_res = auth_client
1737 .post(format!("{}/oauth/authorize", url))
1738 .form(&[
1739 ("request_uri", request_uri),
1740 ("username", &handle),
1741 ("password", password),
1742 ("remember_device", "false"),
1743 ])
1744 .send()
1745 .await
1746 .unwrap();
1747
1748 assert!(auth_res.status().is_redirection());
1749
1750 let twofa_code: String = sqlx::query_scalar(
1751 "SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1"
1752 )
1753 .bind(request_uri)
1754 .fetch_one(&pool)
1755 .await
1756 .expect("Failed to get 2FA code from database");
1757
1758 let twofa_res = auth_client
1759 .post(format!("{}/oauth/authorize/2fa", url))
1760 .form(&[
1761 ("request_uri", request_uri),
1762 ("code", &twofa_code),
1763 ])
1764 .send()
1765 .await
1766 .unwrap();
1767
1768 assert!(
1769 twofa_res.status().is_redirection(),
1770 "Valid 2FA code should redirect to success, got status: {}",
1771 twofa_res.status()
1772 );
1773
1774 let location = twofa_res.headers().get("location").unwrap().to_str().unwrap();
1775 assert!(
1776 location.starts_with(redirect_uri),
1777 "Should redirect to client callback, got: {}",
1778 location
1779 );
1780 assert!(
1781 location.contains("code="),
1782 "Redirect should include authorization code"
1783 );
1784
1785 let auth_code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
1786
1787 let token_res = http_client
1788 .post(format!("{}/oauth/token", url))
1789 .form(&[
1790 ("grant_type", "authorization_code"),
1791 ("code", auth_code),
1792 ("redirect_uri", redirect_uri),
1793 ("code_verifier", &code_verifier),
1794 ("client_id", &client_id),
1795 ])
1796 .send()
1797 .await
1798 .unwrap();
1799
1800 assert_eq!(token_res.status(), StatusCode::OK, "Token exchange should succeed");
1801 let token_body: Value = token_res.json().await.unwrap();
1802 assert!(token_body["access_token"].is_string());
1803 assert_eq!(token_body["sub"], user_did);
1804}
1805
1806#[tokio::test]
1807async fn test_2fa_lockout_after_max_attempts() {
1808 let url = base_url().await;
1809 let http_client = client();
1810
1811 let ts = Utc::now().timestamp_millis();
1812 let handle = format!("2fa-lockout-{}", ts);
1813 let email = format!("2fa-lockout-{}@example.com", ts);
1814 let password = "2fa-test-password";
1815
1816 let create_res = http_client
1817 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1818 .json(&json!({
1819 "handle": handle,
1820 "email": email,
1821 "password": password
1822 }))
1823 .send()
1824 .await
1825 .unwrap();
1826 assert_eq!(create_res.status(), StatusCode::OK);
1827 let account: Value = create_res.json().await.unwrap();
1828 let user_did = account["did"].as_str().unwrap();
1829
1830 let db_url = common::get_db_connection_string().await;
1831 let pool = sqlx::postgres::PgPoolOptions::new()
1832 .max_connections(1)
1833 .connect(&db_url)
1834 .await
1835 .expect("Failed to connect to database");
1836
1837 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
1838 .bind(user_did)
1839 .execute(&pool)
1840 .await
1841 .expect("Failed to enable 2FA");
1842
1843 let redirect_uri = "https://example.com/2fa-lockout-callback";
1844 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1845 let client_id = mock_client.uri();
1846
1847 let (_, code_challenge) = generate_pkce();
1848
1849 let par_body: Value = http_client
1850 .post(format!("{}/oauth/par", url))
1851 .form(&[
1852 ("response_type", "code"),
1853 ("client_id", &client_id),
1854 ("redirect_uri", redirect_uri),
1855 ("code_challenge", &code_challenge),
1856 ("code_challenge_method", "S256"),
1857 ])
1858 .send()
1859 .await
1860 .unwrap()
1861 .json()
1862 .await
1863 .unwrap();
1864
1865 let request_uri = par_body["request_uri"].as_str().unwrap();
1866
1867 let auth_client = no_redirect_client();
1868 let auth_res = auth_client
1869 .post(format!("{}/oauth/authorize", url))
1870 .form(&[
1871 ("request_uri", request_uri),
1872 ("username", &handle),
1873 ("password", password),
1874 ("remember_device", "false"),
1875 ])
1876 .send()
1877 .await
1878 .unwrap();
1879
1880 assert!(auth_res.status().is_redirection());
1881
1882 for i in 0..5 {
1883 let res = http_client
1884 .post(format!("{}/oauth/authorize/2fa", url))
1885 .form(&[
1886 ("request_uri", request_uri),
1887 ("code", "999999"),
1888 ])
1889 .send()
1890 .await
1891 .unwrap();
1892
1893 if i < 4 {
1894 assert_eq!(res.status(), StatusCode::OK, "Attempt {} should show error page", i + 1);
1895 let body = res.text().await.unwrap();
1896 assert!(
1897 body.contains("Invalid verification code"),
1898 "Should show invalid code error on attempt {}", i + 1
1899 );
1900 }
1901 }
1902
1903 let lockout_res = http_client
1904 .post(format!("{}/oauth/authorize/2fa", url))
1905 .form(&[
1906 ("request_uri", request_uri),
1907 ("code", "999999"),
1908 ])
1909 .send()
1910 .await
1911 .unwrap();
1912
1913 assert_eq!(lockout_res.status(), StatusCode::OK);
1914 let body = lockout_res.text().await.unwrap();
1915 assert!(
1916 body.contains("Too many failed attempts") || body.contains("No 2FA challenge found"),
1917 "Should be locked out after max attempts. Body: {}",
1918 &body[..body.len().min(500)]
1919 );
1920}
1921
1922#[tokio::test]
1923async fn test_account_selector_with_2fa_requires_verification() {
1924 let url = base_url().await;
1925 let http_client = client();
1926
1927 let ts = Utc::now().timestamp_millis();
1928 let handle = format!("selector-2fa-{}", ts);
1929 let email = format!("selector-2fa-{}@example.com", ts);
1930 let password = "selector-2fa-password";
1931
1932 let create_res = http_client
1933 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1934 .json(&json!({
1935 "handle": handle,
1936 "email": email,
1937 "password": password
1938 }))
1939 .send()
1940 .await
1941 .unwrap();
1942 assert_eq!(create_res.status(), StatusCode::OK);
1943 let account: Value = create_res.json().await.unwrap();
1944 let user_did = account["did"].as_str().unwrap().to_string();
1945
1946 let redirect_uri = "https://example.com/selector-2fa-callback";
1947 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1948 let client_id = mock_client.uri();
1949
1950 let (code_verifier, code_challenge) = generate_pkce();
1951
1952 let par_body: Value = http_client
1953 .post(format!("{}/oauth/par", url))
1954 .form(&[
1955 ("response_type", "code"),
1956 ("client_id", &client_id),
1957 ("redirect_uri", redirect_uri),
1958 ("code_challenge", &code_challenge),
1959 ("code_challenge_method", "S256"),
1960 ])
1961 .send()
1962 .await
1963 .unwrap()
1964 .json()
1965 .await
1966 .unwrap();
1967
1968 let request_uri = par_body["request_uri"].as_str().unwrap();
1969
1970 let auth_client = no_redirect_client();
1971 let auth_res = auth_client
1972 .post(format!("{}/oauth/authorize", url))
1973 .form(&[
1974 ("request_uri", request_uri),
1975 ("username", &handle),
1976 ("password", password),
1977 ("remember_device", "true"),
1978 ])
1979 .send()
1980 .await
1981 .unwrap();
1982
1983 assert!(auth_res.status().is_redirection());
1984
1985 let device_cookie = auth_res.headers()
1986 .get("set-cookie")
1987 .and_then(|v| v.to_str().ok())
1988 .map(|s| s.split(';').next().unwrap_or("").to_string())
1989 .expect("Should have received device cookie");
1990
1991 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
1992 assert!(location.contains("code="), "First auth should succeed");
1993
1994 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
1995 let _token_body: Value = http_client
1996 .post(format!("{}/oauth/token", url))
1997 .form(&[
1998 ("grant_type", "authorization_code"),
1999 ("code", code),
2000 ("redirect_uri", redirect_uri),
2001 ("code_verifier", &code_verifier),
2002 ("client_id", &client_id),
2003 ])
2004 .send()
2005 .await
2006 .unwrap()
2007 .json()
2008 .await
2009 .unwrap();
2010
2011 let db_url = common::get_db_connection_string().await;
2012 let pool = sqlx::postgres::PgPoolOptions::new()
2013 .max_connections(1)
2014 .connect(&db_url)
2015 .await
2016 .expect("Failed to connect to database");
2017
2018 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
2019 .bind(&user_did)
2020 .execute(&pool)
2021 .await
2022 .expect("Failed to enable 2FA");
2023
2024 let (code_verifier2, code_challenge2) = generate_pkce();
2025
2026 let par_body2: Value = http_client
2027 .post(format!("{}/oauth/par", url))
2028 .form(&[
2029 ("response_type", "code"),
2030 ("client_id", &client_id),
2031 ("redirect_uri", redirect_uri),
2032 ("code_challenge", &code_challenge2),
2033 ("code_challenge_method", "S256"),
2034 ])
2035 .send()
2036 .await
2037 .unwrap()
2038 .json()
2039 .await
2040 .unwrap();
2041
2042 let request_uri2 = par_body2["request_uri"].as_str().unwrap();
2043
2044 let select_res = auth_client
2045 .post(format!("{}/oauth/authorize/select", url))
2046 .header("cookie", &device_cookie)
2047 .form(&[
2048 ("request_uri", request_uri2),
2049 ("did", &user_did),
2050 ])
2051 .send()
2052 .await
2053 .unwrap();
2054
2055 assert!(
2056 select_res.status().is_redirection(),
2057 "Account selector should redirect, got status: {}",
2058 select_res.status()
2059 );
2060
2061 let select_location = select_res.headers().get("location").unwrap().to_str().unwrap();
2062 assert!(
2063 select_location.contains("/oauth/authorize/2fa"),
2064 "Account selector with 2FA enabled should redirect to 2FA page, got: {}",
2065 select_location
2066 );
2067
2068 let twofa_code: String = sqlx::query_scalar(
2069 "SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1"
2070 )
2071 .bind(request_uri2)
2072 .fetch_one(&pool)
2073 .await
2074 .expect("Failed to get 2FA code");
2075
2076 let twofa_res = auth_client
2077 .post(format!("{}/oauth/authorize/2fa", url))
2078 .header("cookie", &device_cookie)
2079 .form(&[
2080 ("request_uri", request_uri2),
2081 ("code", &twofa_code),
2082 ])
2083 .send()
2084 .await
2085 .unwrap();
2086
2087 assert!(twofa_res.status().is_redirection());
2088 let final_location = twofa_res.headers().get("location").unwrap().to_str().unwrap();
2089 assert!(
2090 final_location.starts_with(redirect_uri) && final_location.contains("code="),
2091 "After 2FA, should redirect to client with code, got: {}",
2092 final_location
2093 );
2094
2095 let final_code = final_location.split("code=").nth(1).unwrap().split('&').next().unwrap();
2096 let token_res = http_client
2097 .post(format!("{}/oauth/token", url))
2098 .form(&[
2099 ("grant_type", "authorization_code"),
2100 ("code", final_code),
2101 ("redirect_uri", redirect_uri),
2102 ("code_verifier", &code_verifier2),
2103 ("client_id", &client_id),
2104 ])
2105 .send()
2106 .await
2107 .unwrap();
2108
2109 assert_eq!(token_res.status(), StatusCode::OK);
2110 let final_token: Value = token_res.json().await.unwrap();
2111 assert_eq!(final_token["sub"], user_did, "Token should be for the correct user");
2112}