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 .query(&[("request_uri", request_uri)])
327 .send()
328 .await
329 .expect("Authorize GET failed");
330
331 assert_eq!(auth_res.status(), StatusCode::OK);
332
333 let auth_body: Value = auth_res.json().await.expect("Invalid auth JSON");
334 assert_eq!(auth_body["client_id"], client_id);
335 assert_eq!(auth_body["redirect_uri"], redirect_uri);
336 assert_eq!(auth_body["scope"], "atproto");
337 assert_eq!(auth_body["state"], "test-state");
338}
339
340#[tokio::test]
341async fn test_authorize_rejects_invalid_request_uri() {
342 let url = base_url().await;
343 let client = client();
344
345 let res = client
346 .get(format!("{}/oauth/authorize", url))
347 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:nonexistent")])
348 .send()
349 .await
350 .expect("Request failed");
351
352 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
353
354 let body: Value = res.json().await.expect("Invalid JSON");
355 assert_eq!(body["error"], "invalid_request");
356}
357
358#[tokio::test]
359async fn test_authorize_requires_request_uri() {
360 let url = base_url().await;
361 let client = client();
362
363 let res = client
364 .get(format!("{}/oauth/authorize", url))
365 .send()
366 .await
367 .expect("Request failed");
368
369 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
370}
371
372#[tokio::test]
373async fn test_full_oauth_flow_without_dpop() {
374 let url = base_url().await;
375 let http_client = client();
376
377 let (_, _user_did) = create_account_and_login(&http_client).await;
378
379 let ts = Utc::now().timestamp_millis();
380 let handle = format!("oauth-test-{}", ts);
381 let email = format!("oauth-test-{}@example.com", ts);
382 let password = "oauth-test-password";
383
384 let create_res = http_client
385 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
386 .json(&json!({
387 "handle": handle,
388 "email": email,
389 "password": password
390 }))
391 .send()
392 .await
393 .expect("Account creation failed");
394
395 assert_eq!(create_res.status(), StatusCode::OK);
396 let account: Value = create_res.json().await.unwrap();
397 let user_did = account["did"].as_str().unwrap();
398
399 let redirect_uri = "https://example.com/oauth/callback";
400 let mock_client = setup_mock_client_metadata(redirect_uri).await;
401 let client_id = mock_client.uri();
402
403 let (code_verifier, code_challenge) = generate_pkce();
404 let state = format!("state-{}", ts);
405
406 let par_res = http_client
407 .post(format!("{}/oauth/par", url))
408 .form(&[
409 ("response_type", "code"),
410 ("client_id", &client_id),
411 ("redirect_uri", redirect_uri),
412 ("code_challenge", &code_challenge),
413 ("code_challenge_method", "S256"),
414 ("scope", "atproto"),
415 ("state", &state),
416 ])
417 .send()
418 .await
419 .expect("PAR failed");
420
421 let par_status = par_res.status();
422 let par_text = par_res.text().await.unwrap_or_default();
423 if par_status != StatusCode::OK {
424 panic!("PAR failed with status {}: {}", par_status, par_text);
425 }
426 let par_body: Value = serde_json::from_str(&par_text).unwrap();
427 let request_uri = par_body["request_uri"].as_str().unwrap();
428
429 let auth_client = no_redirect_client();
430 let auth_res = auth_client
431 .post(format!("{}/oauth/authorize", url))
432 .form(&[
433 ("request_uri", request_uri),
434 ("username", &handle),
435 ("password", password),
436 ("remember_device", "false"),
437 ])
438 .send()
439 .await
440 .expect("Authorize POST failed");
441
442 let auth_status = auth_res.status();
443 if auth_status != StatusCode::TEMPORARY_REDIRECT
444 && auth_status != StatusCode::SEE_OTHER
445 && auth_status != StatusCode::FOUND
446 {
447 let auth_text = auth_res.text().await.unwrap_or_default();
448 panic!(
449 "Expected redirect, got {}: {}",
450 auth_status, auth_text
451 );
452 }
453
454 let location = auth_res.headers().get("location")
455 .expect("No Location header")
456 .to_str()
457 .unwrap();
458
459 assert!(location.starts_with(redirect_uri), "Redirect to wrong URI: {}", location);
460 assert!(location.contains("code="), "No code in redirect: {}", location);
461 assert!(location.contains(&format!("state={}", state)), "Wrong state in redirect");
462
463 let code = location
464 .split("code=")
465 .nth(1)
466 .unwrap()
467 .split('&')
468 .next()
469 .unwrap();
470
471 let token_res = http_client
472 .post(format!("{}/oauth/token", url))
473 .form(&[
474 ("grant_type", "authorization_code"),
475 ("code", code),
476 ("redirect_uri", redirect_uri),
477 ("code_verifier", &code_verifier),
478 ("client_id", &client_id),
479 ])
480 .send()
481 .await
482 .expect("Token request failed");
483
484 let token_status = token_res.status();
485 let token_text = token_res.text().await.unwrap_or_default();
486 if token_status != StatusCode::OK {
487 panic!("Token request failed with status {}: {}", token_status, token_text);
488 }
489
490 let token_body: Value = serde_json::from_str(&token_text).unwrap();
491
492 assert!(token_body["access_token"].is_string());
493 assert!(token_body["refresh_token"].is_string());
494 assert_eq!(token_body["token_type"], "Bearer");
495 assert!(token_body["expires_in"].is_number());
496 assert_eq!(token_body["sub"], user_did);
497}
498
499#[tokio::test]
500async fn test_token_refresh_flow() {
501 let url = base_url().await;
502 let http_client = client();
503
504 let ts = Utc::now().timestamp_millis();
505 let handle = format!("refresh-test-{}", ts);
506 let email = format!("refresh-test-{}@example.com", ts);
507 let password = "refresh-test-password";
508
509 http_client
510 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
511 .json(&json!({
512 "handle": handle,
513 "email": email,
514 "password": password
515 }))
516 .send()
517 .await
518 .expect("Account creation failed");
519
520 let redirect_uri = "https://example.com/refresh-callback";
521 let mock_client = setup_mock_client_metadata(redirect_uri).await;
522 let client_id = mock_client.uri();
523
524 let (code_verifier, code_challenge) = generate_pkce();
525
526 let par_body: Value = http_client
527 .post(format!("{}/oauth/par", url))
528 .form(&[
529 ("response_type", "code"),
530 ("client_id", &client_id),
531 ("redirect_uri", redirect_uri),
532 ("code_challenge", &code_challenge),
533 ("code_challenge_method", "S256"),
534 ])
535 .send()
536 .await
537 .unwrap()
538 .json()
539 .await
540 .unwrap();
541
542 let request_uri = par_body["request_uri"].as_str().unwrap();
543
544 let auth_client = no_redirect_client();
545 let auth_res = auth_client
546 .post(format!("{}/oauth/authorize", url))
547 .form(&[
548 ("request_uri", request_uri),
549 ("username", &handle),
550 ("password", password),
551 ("remember_device", "false"),
552 ])
553 .send()
554 .await
555 .unwrap();
556
557 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
558 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
559
560 let token_body: Value = http_client
561 .post(format!("{}/oauth/token", url))
562 .form(&[
563 ("grant_type", "authorization_code"),
564 ("code", code),
565 ("redirect_uri", redirect_uri),
566 ("code_verifier", &code_verifier),
567 ("client_id", &client_id),
568 ])
569 .send()
570 .await
571 .unwrap()
572 .json()
573 .await
574 .unwrap();
575
576 let refresh_token = token_body["refresh_token"].as_str().unwrap();
577 let original_access_token = token_body["access_token"].as_str().unwrap();
578
579 let refresh_res = http_client
580 .post(format!("{}/oauth/token", url))
581 .form(&[
582 ("grant_type", "refresh_token"),
583 ("refresh_token", refresh_token),
584 ("client_id", &client_id),
585 ])
586 .send()
587 .await
588 .expect("Refresh request failed");
589
590 assert_eq!(refresh_res.status(), StatusCode::OK);
591
592 let refresh_body: Value = refresh_res.json().await.unwrap();
593
594 assert!(refresh_body["access_token"].is_string());
595 assert!(refresh_body["refresh_token"].is_string());
596
597 let new_access_token = refresh_body["access_token"].as_str().unwrap();
598 let new_refresh_token = refresh_body["refresh_token"].as_str().unwrap();
599
600 assert_ne!(new_access_token, original_access_token, "Access token should rotate");
601 assert_ne!(new_refresh_token, refresh_token, "Refresh token should rotate");
602}
603
604#[tokio::test]
605async fn test_refresh_token_reuse_detection() {
606 let url = base_url().await;
607 let http_client = client();
608
609 let ts = Utc::now().timestamp_millis();
610 let handle = format!("reuse-test-{}", ts);
611 let email = format!("reuse-test-{}@example.com", ts);
612 let password = "reuse-test-password";
613
614 http_client
615 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
616 .json(&json!({
617 "handle": handle,
618 "email": email,
619 "password": password
620 }))
621 .send()
622 .await
623 .unwrap();
624
625 let redirect_uri = "https://example.com/reuse-callback";
626 let mock_client = setup_mock_client_metadata(redirect_uri).await;
627 let client_id = mock_client.uri();
628
629 let (code_verifier, code_challenge) = generate_pkce();
630
631 let par_body: Value = http_client
632 .post(format!("{}/oauth/par", url))
633 .form(&[
634 ("response_type", "code"),
635 ("client_id", &client_id),
636 ("redirect_uri", redirect_uri),
637 ("code_challenge", &code_challenge),
638 ("code_challenge_method", "S256"),
639 ])
640 .send()
641 .await
642 .unwrap()
643 .json()
644 .await
645 .unwrap();
646
647 let request_uri = par_body["request_uri"].as_str().unwrap();
648
649 let auth_client = no_redirect_client();
650 let auth_res = auth_client
651 .post(format!("{}/oauth/authorize", url))
652 .form(&[
653 ("request_uri", request_uri),
654 ("username", &handle),
655 ("password", password),
656 ("remember_device", "false"),
657 ])
658 .send()
659 .await
660 .unwrap();
661
662 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
663 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
664
665 let token_body: Value = http_client
666 .post(format!("{}/oauth/token", url))
667 .form(&[
668 ("grant_type", "authorization_code"),
669 ("code", code),
670 ("redirect_uri", redirect_uri),
671 ("code_verifier", &code_verifier),
672 ("client_id", &client_id),
673 ])
674 .send()
675 .await
676 .unwrap()
677 .json()
678 .await
679 .unwrap();
680
681 let original_refresh_token = token_body["refresh_token"].as_str().unwrap().to_string();
682
683 let first_refresh: Value = http_client
684 .post(format!("{}/oauth/token", url))
685 .form(&[
686 ("grant_type", "refresh_token"),
687 ("refresh_token", &original_refresh_token),
688 ("client_id", &client_id),
689 ])
690 .send()
691 .await
692 .unwrap()
693 .json()
694 .await
695 .unwrap();
696
697 assert!(first_refresh["access_token"].is_string(), "First refresh should succeed");
698
699 let reuse_res = http_client
700 .post(format!("{}/oauth/token", url))
701 .form(&[
702 ("grant_type", "refresh_token"),
703 ("refresh_token", &original_refresh_token),
704 ("client_id", &client_id),
705 ])
706 .send()
707 .await
708 .unwrap();
709
710 assert_eq!(reuse_res.status(), StatusCode::BAD_REQUEST, "Reuse should be rejected");
711
712 let reuse_body: Value = reuse_res.json().await.unwrap();
713 assert_eq!(reuse_body["error"], "invalid_grant");
714 assert!(
715 reuse_body["error_description"].as_str().unwrap().to_lowercase().contains("reuse"),
716 "Error should mention reuse"
717 );
718}
719
720#[tokio::test]
721async fn test_pkce_verification() {
722 let url = base_url().await;
723 let http_client = client();
724
725 let ts = Utc::now().timestamp_millis();
726 let handle = format!("pkce-test-{}", ts);
727 let email = format!("pkce-test-{}@example.com", ts);
728 let password = "pkce-test-password";
729
730 http_client
731 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
732 .json(&json!({
733 "handle": handle,
734 "email": email,
735 "password": password
736 }))
737 .send()
738 .await
739 .unwrap();
740
741 let redirect_uri = "https://example.com/pkce-callback";
742 let mock_client = setup_mock_client_metadata(redirect_uri).await;
743 let client_id = mock_client.uri();
744
745 let (_, code_challenge) = generate_pkce();
746 let wrong_verifier = "wrong-code-verifier-that-does-not-match";
747
748 let par_body: Value = http_client
749 .post(format!("{}/oauth/par", url))
750 .form(&[
751 ("response_type", "code"),
752 ("client_id", &client_id),
753 ("redirect_uri", redirect_uri),
754 ("code_challenge", &code_challenge),
755 ("code_challenge_method", "S256"),
756 ])
757 .send()
758 .await
759 .unwrap()
760 .json()
761 .await
762 .unwrap();
763
764 let request_uri = par_body["request_uri"].as_str().unwrap();
765
766 let auth_client = no_redirect_client();
767 let auth_res = auth_client
768 .post(format!("{}/oauth/authorize", url))
769 .form(&[
770 ("request_uri", request_uri),
771 ("username", &handle),
772 ("password", password),
773 ("remember_device", "false"),
774 ])
775 .send()
776 .await
777 .unwrap();
778
779 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
780 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
781
782 let token_res = http_client
783 .post(format!("{}/oauth/token", url))
784 .form(&[
785 ("grant_type", "authorization_code"),
786 ("code", code),
787 ("redirect_uri", redirect_uri),
788 ("code_verifier", wrong_verifier),
789 ("client_id", &client_id),
790 ])
791 .send()
792 .await
793 .unwrap();
794
795 assert_eq!(token_res.status(), StatusCode::BAD_REQUEST);
796
797 let token_body: Value = token_res.json().await.unwrap();
798 assert_eq!(token_body["error"], "invalid_grant");
799 assert!(token_body["error_description"].as_str().unwrap().contains("PKCE"));
800}
801
802#[tokio::test]
803async fn test_authorization_code_cannot_be_reused() {
804 let url = base_url().await;
805 let http_client = client();
806
807 let ts = Utc::now().timestamp_millis();
808 let handle = format!("code-reuse-{}", ts);
809 let email = format!("code-reuse-{}@example.com", ts);
810 let password = "code-reuse-password";
811
812 http_client
813 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
814 .json(&json!({
815 "handle": handle,
816 "email": email,
817 "password": password
818 }))
819 .send()
820 .await
821 .unwrap();
822
823 let redirect_uri = "https://example.com/code-reuse-callback";
824 let mock_client = setup_mock_client_metadata(redirect_uri).await;
825 let client_id = mock_client.uri();
826
827 let (code_verifier, code_challenge) = generate_pkce();
828
829 let par_body: Value = http_client
830 .post(format!("{}/oauth/par", url))
831 .form(&[
832 ("response_type", "code"),
833 ("client_id", &client_id),
834 ("redirect_uri", redirect_uri),
835 ("code_challenge", &code_challenge),
836 ("code_challenge_method", "S256"),
837 ])
838 .send()
839 .await
840 .unwrap()
841 .json()
842 .await
843 .unwrap();
844
845 let request_uri = par_body["request_uri"].as_str().unwrap();
846
847 let auth_client = no_redirect_client();
848 let auth_res = auth_client
849 .post(format!("{}/oauth/authorize", url))
850 .form(&[
851 ("request_uri", request_uri),
852 ("username", &handle),
853 ("password", password),
854 ("remember_device", "false"),
855 ])
856 .send()
857 .await
858 .unwrap();
859
860 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
861 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
862
863 let first_token_res = http_client
864 .post(format!("{}/oauth/token", url))
865 .form(&[
866 ("grant_type", "authorization_code"),
867 ("code", code),
868 ("redirect_uri", redirect_uri),
869 ("code_verifier", &code_verifier),
870 ("client_id", &client_id),
871 ])
872 .send()
873 .await
874 .unwrap();
875
876 assert_eq!(first_token_res.status(), StatusCode::OK, "First use should succeed");
877
878 let second_token_res = http_client
879 .post(format!("{}/oauth/token", url))
880 .form(&[
881 ("grant_type", "authorization_code"),
882 ("code", code),
883 ("redirect_uri", redirect_uri),
884 ("code_verifier", &code_verifier),
885 ("client_id", &client_id),
886 ])
887 .send()
888 .await
889 .unwrap();
890
891 assert_eq!(second_token_res.status(), StatusCode::BAD_REQUEST, "Second use should fail");
892
893 let error_body: Value = second_token_res.json().await.unwrap();
894 assert_eq!(error_body["error"], "invalid_grant");
895}
896
897#[tokio::test]
898async fn test_wrong_credentials_denied() {
899 let url = base_url().await;
900 let http_client = client();
901
902 let ts = Utc::now().timestamp_millis();
903 let handle = format!("wrong-creds-{}", ts);
904 let email = format!("wrong-creds-{}@example.com", ts);
905 let password = "correct-password";
906
907 http_client
908 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
909 .json(&json!({
910 "handle": handle,
911 "email": email,
912 "password": password
913 }))
914 .send()
915 .await
916 .unwrap();
917
918 let redirect_uri = "https://example.com/wrong-creds-callback";
919 let mock_client = setup_mock_client_metadata(redirect_uri).await;
920 let client_id = mock_client.uri();
921
922 let (_, code_challenge) = generate_pkce();
923
924 let par_body: Value = http_client
925 .post(format!("{}/oauth/par", url))
926 .form(&[
927 ("response_type", "code"),
928 ("client_id", &client_id),
929 ("redirect_uri", redirect_uri),
930 ("code_challenge", &code_challenge),
931 ("code_challenge_method", "S256"),
932 ])
933 .send()
934 .await
935 .unwrap()
936 .json()
937 .await
938 .unwrap();
939
940 let request_uri = par_body["request_uri"].as_str().unwrap();
941
942 let auth_res = http_client
943 .post(format!("{}/oauth/authorize", url))
944 .form(&[
945 ("request_uri", request_uri),
946 ("username", &handle),
947 ("password", "wrong-password"),
948 ("remember_device", "false"),
949 ])
950 .send()
951 .await
952 .unwrap();
953
954 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN);
955
956 let error_body: Value = auth_res.json().await.unwrap();
957 assert_eq!(error_body["error"], "access_denied");
958}
959
960#[tokio::test]
961async fn test_token_revocation() {
962 let url = base_url().await;
963 let http_client = client();
964
965 let ts = Utc::now().timestamp_millis();
966 let handle = format!("revoke-test-{}", ts);
967 let email = format!("revoke-test-{}@example.com", ts);
968 let password = "revoke-test-password";
969
970 http_client
971 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
972 .json(&json!({
973 "handle": handle,
974 "email": email,
975 "password": password
976 }))
977 .send()
978 .await
979 .unwrap();
980
981 let redirect_uri = "https://example.com/revoke-callback";
982 let mock_client = setup_mock_client_metadata(redirect_uri).await;
983 let client_id = mock_client.uri();
984
985 let (code_verifier, code_challenge) = generate_pkce();
986
987 let par_body: Value = http_client
988 .post(format!("{}/oauth/par", url))
989 .form(&[
990 ("response_type", "code"),
991 ("client_id", &client_id),
992 ("redirect_uri", redirect_uri),
993 ("code_challenge", &code_challenge),
994 ("code_challenge_method", "S256"),
995 ])
996 .send()
997 .await
998 .unwrap()
999 .json()
1000 .await
1001 .unwrap();
1002
1003 let request_uri = par_body["request_uri"].as_str().unwrap();
1004
1005 let auth_client = no_redirect_client();
1006 let auth_res = auth_client
1007 .post(format!("{}/oauth/authorize", url))
1008 .form(&[
1009 ("request_uri", request_uri),
1010 ("username", &handle),
1011 ("password", password),
1012 ("remember_device", "false"),
1013 ])
1014 .send()
1015 .await
1016 .unwrap();
1017
1018 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
1019 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
1020
1021 let token_body: Value = http_client
1022 .post(format!("{}/oauth/token", url))
1023 .form(&[
1024 ("grant_type", "authorization_code"),
1025 ("code", code),
1026 ("redirect_uri", redirect_uri),
1027 ("code_verifier", &code_verifier),
1028 ("client_id", &client_id),
1029 ])
1030 .send()
1031 .await
1032 .unwrap()
1033 .json()
1034 .await
1035 .unwrap();
1036
1037 let refresh_token = token_body["refresh_token"].as_str().unwrap();
1038
1039 let revoke_res = http_client
1040 .post(format!("{}/oauth/revoke", url))
1041 .form(&[("token", refresh_token)])
1042 .send()
1043 .await
1044 .unwrap();
1045
1046 assert_eq!(revoke_res.status(), StatusCode::OK);
1047
1048 let refresh_after_revoke = http_client
1049 .post(format!("{}/oauth/token", url))
1050 .form(&[
1051 ("grant_type", "refresh_token"),
1052 ("refresh_token", refresh_token),
1053 ("client_id", &client_id),
1054 ])
1055 .send()
1056 .await
1057 .unwrap();
1058
1059 assert_eq!(refresh_after_revoke.status(), StatusCode::BAD_REQUEST);
1060}
1061
1062#[tokio::test]
1063async fn test_unsupported_grant_type() {
1064 let url = base_url().await;
1065 let http_client = client();
1066
1067 let res = http_client
1068 .post(format!("{}/oauth/token", url))
1069 .form(&[
1070 ("grant_type", "client_credentials"),
1071 ("client_id", "https://example.com"),
1072 ])
1073 .send()
1074 .await
1075 .unwrap();
1076
1077 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
1078
1079 let body: Value = res.json().await.unwrap();
1080 assert_eq!(body["error"], "unsupported_grant_type");
1081}
1082
1083#[tokio::test]
1084async fn test_invalid_refresh_token() {
1085 let url = base_url().await;
1086 let http_client = client();
1087
1088 let res = http_client
1089 .post(format!("{}/oauth/token", url))
1090 .form(&[
1091 ("grant_type", "refresh_token"),
1092 ("refresh_token", "invalid-refresh-token"),
1093 ("client_id", "https://example.com"),
1094 ])
1095 .send()
1096 .await
1097 .unwrap();
1098
1099 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
1100
1101 let body: Value = res.json().await.unwrap();
1102 assert_eq!(body["error"], "invalid_grant");
1103}
1104
1105#[tokio::test]
1106async fn test_deactivated_account_cannot_authorize() {
1107 let url = base_url().await;
1108 let http_client = client();
1109
1110 let ts = Utc::now().timestamp_millis();
1111 let handle = format!("deact-oauth-{}", ts);
1112 let email = format!("deact-oauth-{}@example.com", ts);
1113 let password = "deact-oauth-password";
1114
1115 let create_res = http_client
1116 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1117 .json(&json!({
1118 "handle": handle,
1119 "email": email,
1120 "password": password
1121 }))
1122 .send()
1123 .await
1124 .unwrap();
1125
1126 assert_eq!(create_res.status(), StatusCode::OK);
1127 let account: Value = create_res.json().await.unwrap();
1128 let access_jwt = account["accessJwt"].as_str().unwrap();
1129
1130 let deact_res = http_client
1131 .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url))
1132 .header("Authorization", format!("Bearer {}", access_jwt))
1133 .json(&json!({}))
1134 .send()
1135 .await
1136 .unwrap();
1137 assert_eq!(deact_res.status(), StatusCode::OK);
1138
1139 let redirect_uri = "https://example.com/deact-callback";
1140 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1141 let client_id = mock_client.uri();
1142
1143 let (_, code_challenge) = generate_pkce();
1144
1145 let par_body: Value = http_client
1146 .post(format!("{}/oauth/par", url))
1147 .form(&[
1148 ("response_type", "code"),
1149 ("client_id", &client_id),
1150 ("redirect_uri", redirect_uri),
1151 ("code_challenge", &code_challenge),
1152 ("code_challenge_method", "S256"),
1153 ])
1154 .send()
1155 .await
1156 .unwrap()
1157 .json()
1158 .await
1159 .unwrap();
1160
1161 let request_uri = par_body["request_uri"].as_str().unwrap();
1162
1163 let auth_res = http_client
1164 .post(format!("{}/oauth/authorize", url))
1165 .form(&[
1166 ("request_uri", request_uri),
1167 ("username", &handle),
1168 ("password", password),
1169 ("remember_device", "false"),
1170 ])
1171 .send()
1172 .await
1173 .unwrap();
1174
1175 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN, "Deactivated account should not be able to authorize");
1176 let body: Value = auth_res.json().await.unwrap();
1177 assert_eq!(body["error"], "access_denied");
1178}
1179
1180#[tokio::test]
1181async fn test_expired_authorization_request() {
1182 let url = base_url().await;
1183 let http_client = client();
1184
1185 let res = http_client
1186 .get(format!("{}/oauth/authorize", url))
1187 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:expired-or-nonexistent")])
1188 .send()
1189 .await
1190 .unwrap();
1191
1192 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
1193 let body: Value = res.json().await.unwrap();
1194 assert_eq!(body["error"], "invalid_request");
1195}
1196
1197#[tokio::test]
1198async fn test_token_introspection() {
1199 let url = base_url().await;
1200 let http_client = client();
1201
1202 let ts = Utc::now().timestamp_millis();
1203 let handle = format!("introspect-{}", ts);
1204 let email = format!("introspect-{}@example.com", ts);
1205 let password = "introspect-password";
1206
1207 http_client
1208 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1209 .json(&json!({
1210 "handle": handle,
1211 "email": email,
1212 "password": password
1213 }))
1214 .send()
1215 .await
1216 .unwrap();
1217
1218 let redirect_uri = "https://example.com/introspect-callback";
1219 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1220 let client_id = mock_client.uri();
1221
1222 let (code_verifier, code_challenge) = generate_pkce();
1223
1224 let par_body: Value = http_client
1225 .post(format!("{}/oauth/par", url))
1226 .form(&[
1227 ("response_type", "code"),
1228 ("client_id", &client_id),
1229 ("redirect_uri", redirect_uri),
1230 ("code_challenge", &code_challenge),
1231 ("code_challenge_method", "S256"),
1232 ])
1233 .send()
1234 .await
1235 .unwrap()
1236 .json()
1237 .await
1238 .unwrap();
1239
1240 let request_uri = par_body["request_uri"].as_str().unwrap();
1241
1242 let auth_client = no_redirect_client();
1243 let auth_res = auth_client
1244 .post(format!("{}/oauth/authorize", url))
1245 .form(&[
1246 ("request_uri", request_uri),
1247 ("username", &handle),
1248 ("password", password),
1249 ("remember_device", "false"),
1250 ])
1251 .send()
1252 .await
1253 .unwrap();
1254
1255 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
1256 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
1257
1258 let token_body: Value = http_client
1259 .post(format!("{}/oauth/token", url))
1260 .form(&[
1261 ("grant_type", "authorization_code"),
1262 ("code", code),
1263 ("redirect_uri", redirect_uri),
1264 ("code_verifier", &code_verifier),
1265 ("client_id", &client_id),
1266 ])
1267 .send()
1268 .await
1269 .unwrap()
1270 .json()
1271 .await
1272 .unwrap();
1273
1274 let access_token = token_body["access_token"].as_str().unwrap();
1275
1276 let introspect_res = http_client
1277 .post(format!("{}/oauth/introspect", url))
1278 .form(&[("token", access_token)])
1279 .send()
1280 .await
1281 .unwrap();
1282
1283 assert_eq!(introspect_res.status(), StatusCode::OK);
1284 let introspect_body: Value = introspect_res.json().await.unwrap();
1285 assert_eq!(introspect_body["active"], true);
1286 assert!(introspect_body["client_id"].is_string());
1287 assert!(introspect_body["exp"].is_number());
1288}
1289
1290#[tokio::test]
1291async fn test_introspect_invalid_token() {
1292 let url = base_url().await;
1293 let http_client = client();
1294
1295 let res = http_client
1296 .post(format!("{}/oauth/introspect", url))
1297 .form(&[("token", "invalid.token.here")])
1298 .send()
1299 .await
1300 .unwrap();
1301
1302 assert_eq!(res.status(), StatusCode::OK);
1303 let body: Value = res.json().await.unwrap();
1304 assert_eq!(body["active"], false);
1305}
1306
1307#[tokio::test]
1308async fn test_introspect_revoked_token() {
1309 let url = base_url().await;
1310 let http_client = client();
1311
1312 let ts = Utc::now().timestamp_millis();
1313 let handle = format!("introspect-revoked-{}", ts);
1314 let email = format!("introspect-revoked-{}@example.com", ts);
1315 let password = "introspect-revoked-password";
1316
1317 http_client
1318 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1319 .json(&json!({
1320 "handle": handle,
1321 "email": email,
1322 "password": password
1323 }))
1324 .send()
1325 .await
1326 .unwrap();
1327
1328 let redirect_uri = "https://example.com/introspect-revoked-callback";
1329 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1330 let client_id = mock_client.uri();
1331
1332 let (code_verifier, code_challenge) = generate_pkce();
1333
1334 let par_body: Value = http_client
1335 .post(format!("{}/oauth/par", url))
1336 .form(&[
1337 ("response_type", "code"),
1338 ("client_id", &client_id),
1339 ("redirect_uri", redirect_uri),
1340 ("code_challenge", &code_challenge),
1341 ("code_challenge_method", "S256"),
1342 ])
1343 .send()
1344 .await
1345 .unwrap()
1346 .json()
1347 .await
1348 .unwrap();
1349
1350 let request_uri = par_body["request_uri"].as_str().unwrap();
1351
1352 let auth_client = no_redirect_client();
1353 let auth_res = auth_client
1354 .post(format!("{}/oauth/authorize", url))
1355 .form(&[
1356 ("request_uri", request_uri),
1357 ("username", &handle),
1358 ("password", password),
1359 ("remember_device", "false"),
1360 ])
1361 .send()
1362 .await
1363 .unwrap();
1364
1365 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
1366 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
1367
1368 let token_body: Value = http_client
1369 .post(format!("{}/oauth/token", url))
1370 .form(&[
1371 ("grant_type", "authorization_code"),
1372 ("code", code),
1373 ("redirect_uri", redirect_uri),
1374 ("code_verifier", &code_verifier),
1375 ("client_id", &client_id),
1376 ])
1377 .send()
1378 .await
1379 .unwrap()
1380 .json()
1381 .await
1382 .unwrap();
1383
1384 let access_token = token_body["access_token"].as_str().unwrap();
1385 let refresh_token = token_body["refresh_token"].as_str().unwrap();
1386
1387 http_client
1388 .post(format!("{}/oauth/revoke", url))
1389 .form(&[("token", refresh_token)])
1390 .send()
1391 .await
1392 .unwrap();
1393
1394 let introspect_res = http_client
1395 .post(format!("{}/oauth/introspect", url))
1396 .form(&[("token", access_token)])
1397 .send()
1398 .await
1399 .unwrap();
1400
1401 assert_eq!(introspect_res.status(), StatusCode::OK);
1402 let body: Value = introspect_res.json().await.unwrap();
1403 assert_eq!(body["active"], false, "Revoked token should be inactive");
1404}
1405
1406#[tokio::test]
1407async fn test_state_with_special_chars() {
1408 let url = base_url().await;
1409 let http_client = client();
1410
1411 let ts = Utc::now().timestamp_millis();
1412 let handle = format!("state-special-{}", ts);
1413 let email = format!("state-special-{}@example.com", ts);
1414 let password = "state-special-password";
1415
1416 http_client
1417 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1418 .json(&json!({
1419 "handle": handle,
1420 "email": email,
1421 "password": password
1422 }))
1423 .send()
1424 .await
1425 .unwrap();
1426
1427 let redirect_uri = "https://example.com/state-special-callback";
1428 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1429 let client_id = mock_client.uri();
1430
1431 let (code_verifier, code_challenge) = generate_pkce();
1432 let special_state = "state=with&special=chars&plus+more";
1433
1434 let par_body: Value = http_client
1435 .post(format!("{}/oauth/par", url))
1436 .form(&[
1437 ("response_type", "code"),
1438 ("client_id", &client_id),
1439 ("redirect_uri", redirect_uri),
1440 ("code_challenge", &code_challenge),
1441 ("code_challenge_method", "S256"),
1442 ("state", special_state),
1443 ])
1444 .send()
1445 .await
1446 .unwrap()
1447 .json()
1448 .await
1449 .unwrap();
1450
1451 let request_uri = par_body["request_uri"].as_str().unwrap();
1452
1453 let auth_client = no_redirect_client();
1454 let auth_res = auth_client
1455 .post(format!("{}/oauth/authorize", url))
1456 .form(&[
1457 ("request_uri", request_uri),
1458 ("username", &handle),
1459 ("password", password),
1460 ("remember_device", "false"),
1461 ])
1462 .send()
1463 .await
1464 .unwrap();
1465
1466 assert!(
1467 auth_res.status().is_redirection(),
1468 "Should redirect even with special chars in state"
1469 );
1470 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
1471 assert!(location.contains("state="), "State should be in redirect URL");
1472
1473 let encoded_state = urlencoding::encode(special_state);
1474 assert!(
1475 location.contains(&format!("state={}", encoded_state)),
1476 "State should be URL-encoded. Got: {}",
1477 location
1478 );
1479}