this repo has no description
1#![allow(unused_imports)]
2#![allow(unused_variables)]
3
4mod common;
5mod helpers;
6
7use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
8use bspds::oauth::dpop::{DPoPVerifier, DPoPJwk, compute_jwk_thumbprint};
9use chrono::Utc;
10use common::{base_url, client};
11use helpers::verify_new_account;
12use reqwest::{redirect, StatusCode};
13use serde_json::{json, Value};
14use sha2::{Digest, Sha256};
15use wiremock::{Mock, MockServer, ResponseTemplate};
16use wiremock::matchers::{method, path};
17
18fn no_redirect_client() -> reqwest::Client {
19 reqwest::Client::builder()
20 .redirect(redirect::Policy::none())
21 .build()
22 .unwrap()
23}
24
25fn generate_pkce() -> (String, String) {
26 let verifier_bytes: [u8; 32] = rand::random();
27 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
28
29 let mut hasher = Sha256::new();
30 hasher.update(code_verifier.as_bytes());
31 let hash = hasher.finalize();
32 let code_challenge = URL_SAFE_NO_PAD.encode(&hash);
33
34 (code_verifier, code_challenge)
35}
36
37async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer {
38 let mock_server = MockServer::start().await;
39
40 let client_id = mock_server.uri();
41 let metadata = json!({
42 "client_id": client_id,
43 "client_name": "Security Test Client",
44 "redirect_uris": [redirect_uri],
45 "grant_types": ["authorization_code", "refresh_token"],
46 "response_types": ["code"],
47 "token_endpoint_auth_method": "none",
48 "dpop_bound_access_tokens": false
49 });
50
51 Mock::given(method("GET"))
52 .and(path("/"))
53 .respond_with(ResponseTemplate::new(200).set_body_json(metadata))
54 .mount(&mock_server)
55 .await;
56
57 mock_server
58}
59
60async fn get_oauth_tokens(
61 http_client: &reqwest::Client,
62 url: &str,
63) -> (String, String, String) {
64 let ts = Utc::now().timestamp_millis();
65 let handle = format!("sec-test-{}", ts);
66 let email = format!("sec-test-{}@example.com", ts);
67 let password = "security-test-password";
68
69 http_client
70 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
71 .json(&json!({
72 "handle": handle,
73 "email": email,
74 "password": password
75 }))
76 .send()
77 .await
78 .unwrap();
79
80 let redirect_uri = "https://example.com/sec-callback";
81 let mock_client = setup_mock_client_metadata(redirect_uri).await;
82 let client_id = mock_client.uri();
83
84 let (code_verifier, code_challenge) = generate_pkce();
85
86 let par_body: Value = http_client
87 .post(format!("{}/oauth/par", url))
88 .form(&[
89 ("response_type", "code"),
90 ("client_id", &client_id),
91 ("redirect_uri", redirect_uri),
92 ("code_challenge", &code_challenge),
93 ("code_challenge_method", "S256"),
94 ])
95 .send()
96 .await
97 .unwrap()
98 .json()
99 .await
100 .unwrap();
101
102 let request_uri = par_body["request_uri"].as_str().unwrap();
103
104 let auth_client = no_redirect_client();
105 let auth_res = auth_client
106 .post(format!("{}/oauth/authorize", url))
107 .form(&[
108 ("request_uri", request_uri),
109 ("username", &handle),
110 ("password", password),
111 ("remember_device", "false"),
112 ])
113 .send()
114 .await
115 .unwrap();
116
117 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
118 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
119
120 let token_body: Value = http_client
121 .post(format!("{}/oauth/token", url))
122 .form(&[
123 ("grant_type", "authorization_code"),
124 ("code", code),
125 ("redirect_uri", redirect_uri),
126 ("code_verifier", &code_verifier),
127 ("client_id", &client_id),
128 ])
129 .send()
130 .await
131 .unwrap()
132 .json()
133 .await
134 .unwrap();
135
136 let access_token = token_body["access_token"].as_str().unwrap().to_string();
137 let refresh_token = token_body["refresh_token"].as_str().unwrap().to_string();
138
139 (access_token, refresh_token, client_id)
140}
141
142#[tokio::test]
143async fn test_security_forged_token_signature_rejected() {
144 let url = base_url().await;
145 let http_client = client();
146
147 let (access_token, _, _) = get_oauth_tokens(&http_client, url).await;
148
149 let parts: Vec<&str> = access_token.split('.').collect();
150 assert_eq!(parts.len(), 3, "Token should have 3 parts");
151
152 let forged_signature = URL_SAFE_NO_PAD.encode(&[0u8; 32]);
153 let forged_token = format!("{}.{}.{}", parts[0], parts[1], forged_signature);
154
155 let res = http_client
156 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
157 .header("Authorization", format!("Bearer {}", forged_token))
158 .send()
159 .await
160 .unwrap();
161
162 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Forged signature should be rejected");
163}
164
165#[tokio::test]
166async fn test_security_modified_payload_rejected() {
167 let url = base_url().await;
168 let http_client = client();
169
170 let (access_token, _, _) = get_oauth_tokens(&http_client, url).await;
171
172 let parts: Vec<&str> = access_token.split('.').collect();
173
174 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap();
175 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
176 payload["sub"] = json!("did:plc:attacker");
177 let modified_payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
178 let modified_token = format!("{}.{}.{}", parts[0], modified_payload, parts[2]);
179
180 let res = http_client
181 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
182 .header("Authorization", format!("Bearer {}", modified_token))
183 .send()
184 .await
185 .unwrap();
186
187 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Modified payload should be rejected");
188}
189
190#[tokio::test]
191async fn test_security_algorithm_none_attack_rejected() {
192 let url = base_url().await;
193 let http_client = client();
194
195 let header = json!({
196 "alg": "none",
197 "typ": "at+jwt"
198 });
199 let payload = json!({
200 "iss": "https://test.pds",
201 "sub": "did:plc:attacker",
202 "aud": "https://test.pds",
203 "iat": Utc::now().timestamp(),
204 "exp": Utc::now().timestamp() + 3600,
205 "jti": "fake-token-id",
206 "scope": "atproto"
207 });
208
209 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
210 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
211 let malicious_token = format!("{}.{}.", header_b64, payload_b64);
212
213 let res = http_client
214 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
215 .header("Authorization", format!("Bearer {}", malicious_token))
216 .send()
217 .await
218 .unwrap();
219
220 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Algorithm 'none' attack should be rejected");
221}
222
223#[tokio::test]
224async fn test_security_algorithm_substitution_attack_rejected() {
225 let url = base_url().await;
226 let http_client = client();
227
228 let header = json!({
229 "alg": "RS256",
230 "typ": "at+jwt"
231 });
232 let payload = json!({
233 "iss": "https://test.pds",
234 "sub": "did:plc:attacker",
235 "aud": "https://test.pds",
236 "iat": Utc::now().timestamp(),
237 "exp": Utc::now().timestamp() + 3600,
238 "jti": "fake-token-id"
239 });
240
241 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
242 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
243 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 64]);
244 let malicious_token = format!("{}.{}.{}", header_b64, payload_b64, fake_sig);
245
246 let res = http_client
247 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
248 .header("Authorization", format!("Bearer {}", malicious_token))
249 .send()
250 .await
251 .unwrap();
252
253 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Algorithm substitution attack should be rejected");
254}
255
256#[tokio::test]
257async fn test_security_expired_token_rejected() {
258 let url = base_url().await;
259 let http_client = client();
260
261 let header = json!({
262 "alg": "HS256",
263 "typ": "at+jwt"
264 });
265 let payload = json!({
266 "iss": "https://test.pds",
267 "sub": "did:plc:test",
268 "aud": "https://test.pds",
269 "iat": Utc::now().timestamp() - 7200,
270 "exp": Utc::now().timestamp() - 3600,
271 "jti": "expired-token-id"
272 });
273
274 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
275 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
276 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 32]);
277 let expired_token = format!("{}.{}.{}", header_b64, payload_b64, fake_sig);
278
279 let res = http_client
280 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
281 .header("Authorization", format!("Bearer {}", expired_token))
282 .send()
283 .await
284 .unwrap();
285
286 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Expired token should be rejected");
287}
288
289#[tokio::test]
290async fn test_security_pkce_plain_method_rejected() {
291 let url = base_url().await;
292 let http_client = client();
293
294 let redirect_uri = "https://example.com/pkce-plain-callback";
295 let mock_client = setup_mock_client_metadata(redirect_uri).await;
296 let client_id = mock_client.uri();
297
298 let res = http_client
299 .post(format!("{}/oauth/par", url))
300 .form(&[
301 ("response_type", "code"),
302 ("client_id", &client_id),
303 ("redirect_uri", redirect_uri),
304 ("code_challenge", "plain-text-challenge"),
305 ("code_challenge_method", "plain"),
306 ])
307 .send()
308 .await
309 .unwrap();
310
311 assert_eq!(res.status(), StatusCode::BAD_REQUEST, "PKCE plain method should be rejected");
312 let body: Value = res.json().await.unwrap();
313 assert_eq!(body["error"], "invalid_request");
314 assert!(
315 body["error_description"].as_str().unwrap().to_lowercase().contains("s256"),
316 "Error should mention S256 requirement"
317 );
318}
319
320#[tokio::test]
321async fn test_security_pkce_missing_challenge_rejected() {
322 let url = base_url().await;
323 let http_client = client();
324
325 let redirect_uri = "https://example.com/no-pkce-callback";
326 let mock_client = setup_mock_client_metadata(redirect_uri).await;
327 let client_id = mock_client.uri();
328
329 let res = http_client
330 .post(format!("{}/oauth/par", url))
331 .form(&[
332 ("response_type", "code"),
333 ("client_id", &client_id),
334 ("redirect_uri", redirect_uri),
335 ])
336 .send()
337 .await
338 .unwrap();
339
340 assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Missing PKCE challenge should be rejected");
341}
342
343#[tokio::test]
344async fn test_security_pkce_wrong_verifier_rejected() {
345 let url = base_url().await;
346 let http_client = client();
347
348 let ts = Utc::now().timestamp_millis();
349 let handle = format!("pkce-attack-{}", ts);
350 let email = format!("pkce-attack-{}@example.com", ts);
351 let password = "pkce-attack-password";
352
353 http_client
354 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
355 .json(&json!({
356 "handle": handle,
357 "email": email,
358 "password": password
359 }))
360 .send()
361 .await
362 .unwrap();
363
364 let redirect_uri = "https://example.com/pkce-attack-callback";
365 let mock_client = setup_mock_client_metadata(redirect_uri).await;
366 let client_id = mock_client.uri();
367
368 let (_, code_challenge) = generate_pkce();
369 let (attacker_verifier, _) = generate_pkce();
370
371 let par_body: Value = http_client
372 .post(format!("{}/oauth/par", url))
373 .form(&[
374 ("response_type", "code"),
375 ("client_id", &client_id),
376 ("redirect_uri", redirect_uri),
377 ("code_challenge", &code_challenge),
378 ("code_challenge_method", "S256"),
379 ])
380 .send()
381 .await
382 .unwrap()
383 .json()
384 .await
385 .unwrap();
386
387 let request_uri = par_body["request_uri"].as_str().unwrap();
388
389 let auth_client = no_redirect_client();
390 let auth_res = auth_client
391 .post(format!("{}/oauth/authorize", url))
392 .form(&[
393 ("request_uri", request_uri),
394 ("username", &handle),
395 ("password", password),
396 ("remember_device", "false"),
397 ])
398 .send()
399 .await
400 .unwrap();
401
402 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
403 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
404
405 let token_res = http_client
406 .post(format!("{}/oauth/token", url))
407 .form(&[
408 ("grant_type", "authorization_code"),
409 ("code", code),
410 ("redirect_uri", redirect_uri),
411 ("code_verifier", &attacker_verifier),
412 ("client_id", &client_id),
413 ])
414 .send()
415 .await
416 .unwrap();
417
418 assert_eq!(token_res.status(), StatusCode::BAD_REQUEST, "Wrong PKCE verifier should be rejected");
419 let body: Value = token_res.json().await.unwrap();
420 assert_eq!(body["error"], "invalid_grant");
421}
422
423#[tokio::test]
424async fn test_security_authorization_code_replay_attack() {
425 let url = base_url().await;
426 let http_client = client();
427
428 let ts = Utc::now().timestamp_millis();
429 let handle = format!("code-replay-{}", ts);
430 let email = format!("code-replay-{}@example.com", ts);
431 let password = "code-replay-password";
432
433 http_client
434 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
435 .json(&json!({
436 "handle": handle,
437 "email": email,
438 "password": password
439 }))
440 .send()
441 .await
442 .unwrap();
443
444 let redirect_uri = "https://example.com/code-replay-callback";
445 let mock_client = setup_mock_client_metadata(redirect_uri).await;
446 let client_id = mock_client.uri();
447
448 let (code_verifier, code_challenge) = generate_pkce();
449
450 let par_body: Value = http_client
451 .post(format!("{}/oauth/par", url))
452 .form(&[
453 ("response_type", "code"),
454 ("client_id", &client_id),
455 ("redirect_uri", redirect_uri),
456 ("code_challenge", &code_challenge),
457 ("code_challenge_method", "S256"),
458 ])
459 .send()
460 .await
461 .unwrap()
462 .json()
463 .await
464 .unwrap();
465
466 let request_uri = par_body["request_uri"].as_str().unwrap();
467
468 let auth_client = no_redirect_client();
469 let auth_res = auth_client
470 .post(format!("{}/oauth/authorize", url))
471 .form(&[
472 ("request_uri", request_uri),
473 ("username", &handle),
474 ("password", password),
475 ("remember_device", "false"),
476 ])
477 .send()
478 .await
479 .unwrap();
480
481 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
482 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
483 let stolen_code = code.to_string();
484
485 let first_res = http_client
486 .post(format!("{}/oauth/token", url))
487 .form(&[
488 ("grant_type", "authorization_code"),
489 ("code", code),
490 ("redirect_uri", redirect_uri),
491 ("code_verifier", &code_verifier),
492 ("client_id", &client_id),
493 ])
494 .send()
495 .await
496 .unwrap();
497
498 assert_eq!(first_res.status(), StatusCode::OK, "First use should succeed");
499
500 let replay_res = http_client
501 .post(format!("{}/oauth/token", url))
502 .form(&[
503 ("grant_type", "authorization_code"),
504 ("code", &stolen_code),
505 ("redirect_uri", redirect_uri),
506 ("code_verifier", &code_verifier),
507 ("client_id", &client_id),
508 ])
509 .send()
510 .await
511 .unwrap();
512
513 assert_eq!(replay_res.status(), StatusCode::BAD_REQUEST, "Replay attack should fail");
514 let body: Value = replay_res.json().await.unwrap();
515 assert_eq!(body["error"], "invalid_grant");
516}
517
518#[tokio::test]
519async fn test_security_refresh_token_replay_attack() {
520 let url = base_url().await;
521 let http_client = client();
522
523 let ts = Utc::now().timestamp_millis();
524 let handle = format!("rt-replay-{}", ts);
525 let email = format!("rt-replay-{}@example.com", ts);
526 let password = "rt-replay-password";
527
528 http_client
529 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
530 .json(&json!({
531 "handle": handle,
532 "email": email,
533 "password": password
534 }))
535 .send()
536 .await
537 .unwrap();
538
539 let redirect_uri = "https://example.com/rt-replay-callback";
540 let mock_client = setup_mock_client_metadata(redirect_uri).await;
541 let client_id = mock_client.uri();
542
543 let (code_verifier, code_challenge) = generate_pkce();
544
545 let par_body: Value = http_client
546 .post(format!("{}/oauth/par", url))
547 .form(&[
548 ("response_type", "code"),
549 ("client_id", &client_id),
550 ("redirect_uri", redirect_uri),
551 ("code_challenge", &code_challenge),
552 ("code_challenge_method", "S256"),
553 ])
554 .send()
555 .await
556 .unwrap()
557 .json()
558 .await
559 .unwrap();
560
561 let request_uri = par_body["request_uri"].as_str().unwrap();
562
563 let auth_client = no_redirect_client();
564 let auth_res = auth_client
565 .post(format!("{}/oauth/authorize", url))
566 .form(&[
567 ("request_uri", request_uri),
568 ("username", &handle),
569 ("password", password),
570 ("remember_device", "false"),
571 ])
572 .send()
573 .await
574 .unwrap();
575
576 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
577 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
578
579 let token_body: Value = http_client
580 .post(format!("{}/oauth/token", url))
581 .form(&[
582 ("grant_type", "authorization_code"),
583 ("code", code),
584 ("redirect_uri", redirect_uri),
585 ("code_verifier", &code_verifier),
586 ("client_id", &client_id),
587 ])
588 .send()
589 .await
590 .unwrap()
591 .json()
592 .await
593 .unwrap();
594
595 let stolen_refresh_token = token_body["refresh_token"].as_str().unwrap().to_string();
596
597 let first_refresh: Value = http_client
598 .post(format!("{}/oauth/token", url))
599 .form(&[
600 ("grant_type", "refresh_token"),
601 ("refresh_token", &stolen_refresh_token),
602 ("client_id", &client_id),
603 ])
604 .send()
605 .await
606 .unwrap()
607 .json()
608 .await
609 .unwrap();
610
611 assert!(first_refresh["access_token"].is_string(), "First refresh should succeed");
612 let new_refresh_token = first_refresh["refresh_token"].as_str().unwrap();
613
614 let replay_res = http_client
615 .post(format!("{}/oauth/token", url))
616 .form(&[
617 ("grant_type", "refresh_token"),
618 ("refresh_token", &stolen_refresh_token),
619 ("client_id", &client_id),
620 ])
621 .send()
622 .await
623 .unwrap();
624
625 assert_eq!(replay_res.status(), StatusCode::BAD_REQUEST, "Refresh token replay should fail");
626 let body: Value = replay_res.json().await.unwrap();
627 assert_eq!(body["error"], "invalid_grant");
628 assert!(
629 body["error_description"].as_str().unwrap().to_lowercase().contains("reuse"),
630 "Error should mention token reuse"
631 );
632
633 let family_revoked_res = http_client
634 .post(format!("{}/oauth/token", url))
635 .form(&[
636 ("grant_type", "refresh_token"),
637 ("refresh_token", new_refresh_token),
638 ("client_id", &client_id),
639 ])
640 .send()
641 .await
642 .unwrap();
643
644 assert_eq!(
645 family_revoked_res.status(),
646 StatusCode::BAD_REQUEST,
647 "Token family should be revoked after replay detection"
648 );
649}
650
651#[tokio::test]
652async fn test_security_redirect_uri_manipulation() {
653 let url = base_url().await;
654 let http_client = client();
655
656 let registered_redirect = "https://legitimate-app.com/callback";
657 let attacker_redirect = "https://attacker.com/steal";
658 let mock_client = setup_mock_client_metadata(registered_redirect).await;
659 let client_id = mock_client.uri();
660
661 let (_, code_challenge) = generate_pkce();
662
663 let res = http_client
664 .post(format!("{}/oauth/par", url))
665 .form(&[
666 ("response_type", "code"),
667 ("client_id", &client_id),
668 ("redirect_uri", attacker_redirect),
669 ("code_challenge", &code_challenge),
670 ("code_challenge_method", "S256"),
671 ])
672 .send()
673 .await
674 .unwrap();
675
676 assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Unregistered redirect_uri should be rejected");
677}
678
679#[tokio::test]
680async fn test_security_deactivated_account_blocked() {
681 let url = base_url().await;
682 let http_client = client();
683
684 let ts = Utc::now().timestamp_millis();
685 let handle = format!("deact-sec-{}", ts);
686 let email = format!("deact-sec-{}@example.com", ts);
687 let password = "deact-sec-password";
688
689 let create_res = http_client
690 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
691 .json(&json!({
692 "handle": handle,
693 "email": email,
694 "password": password
695 }))
696 .send()
697 .await
698 .unwrap();
699
700 assert_eq!(create_res.status(), StatusCode::OK);
701 let account: Value = create_res.json().await.unwrap();
702 let did = account["did"].as_str().unwrap();
703
704 let access_jwt = verify_new_account(&http_client, did).await;
705
706 let deact_res = http_client
707 .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url))
708 .header("Authorization", format!("Bearer {}", access_jwt))
709 .json(&json!({}))
710 .send()
711 .await
712 .unwrap();
713 assert_eq!(deact_res.status(), StatusCode::OK);
714
715 let redirect_uri = "https://example.com/deact-sec-callback";
716 let mock_client = setup_mock_client_metadata(redirect_uri).await;
717 let client_id = mock_client.uri();
718
719 let (_, code_challenge) = generate_pkce();
720
721 let par_body: Value = http_client
722 .post(format!("{}/oauth/par", url))
723 .form(&[
724 ("response_type", "code"),
725 ("client_id", &client_id),
726 ("redirect_uri", redirect_uri),
727 ("code_challenge", &code_challenge),
728 ("code_challenge_method", "S256"),
729 ])
730 .send()
731 .await
732 .unwrap()
733 .json()
734 .await
735 .unwrap();
736
737 let request_uri = par_body["request_uri"].as_str().unwrap();
738
739 let auth_res = http_client
740 .post(format!("{}/oauth/authorize", url))
741 .header("Accept", "application/json")
742 .form(&[
743 ("request_uri", request_uri),
744 ("username", &handle),
745 ("password", password),
746 ("remember_device", "false"),
747 ])
748 .send()
749 .await
750 .unwrap();
751
752 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN, "Deactivated account should be blocked from OAuth");
753 let body: Value = auth_res.json().await.unwrap();
754 assert_eq!(body["error"], "access_denied");
755}
756
757#[tokio::test]
758async fn test_security_url_injection_in_state_parameter() {
759 let url = base_url().await;
760 let http_client = client();
761
762 let ts = Utc::now().timestamp_millis();
763 let handle = format!("inject-state-{}", ts);
764 let email = format!("inject-state-{}@example.com", ts);
765 let password = "inject-state-password";
766
767 http_client
768 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
769 .json(&json!({
770 "handle": handle,
771 "email": email,
772 "password": password
773 }))
774 .send()
775 .await
776 .unwrap();
777
778 let redirect_uri = "https://example.com/inject-callback";
779 let mock_client = setup_mock_client_metadata(redirect_uri).await;
780 let client_id = mock_client.uri();
781
782 let (code_verifier, code_challenge) = generate_pkce();
783
784 let malicious_state = "state&redirect_uri=https://attacker.com&extra=";
785
786 let par_body: Value = http_client
787 .post(format!("{}/oauth/par", url))
788 .form(&[
789 ("response_type", "code"),
790 ("client_id", &client_id),
791 ("redirect_uri", redirect_uri),
792 ("code_challenge", &code_challenge),
793 ("code_challenge_method", "S256"),
794 ("state", malicious_state),
795 ])
796 .send()
797 .await
798 .unwrap()
799 .json()
800 .await
801 .unwrap();
802
803 let request_uri = par_body["request_uri"].as_str().unwrap();
804
805 let auth_client = no_redirect_client();
806 let auth_res = auth_client
807 .post(format!("{}/oauth/authorize", url))
808 .form(&[
809 ("request_uri", request_uri),
810 ("username", &handle),
811 ("password", password),
812 ("remember_device", "false"),
813 ])
814 .send()
815 .await
816 .unwrap();
817
818 assert!(auth_res.status().is_redirection(), "Should redirect successfully");
819 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
820
821 assert!(
822 location.starts_with(redirect_uri),
823 "Redirect should go to registered URI, not attacker URI. Got: {}",
824 location
825 );
826
827 let redirect_uri_count = location.matches("redirect_uri=").count();
828 assert!(
829 redirect_uri_count <= 1,
830 "State injection should not add extra redirect_uri parameters"
831 );
832
833 assert!(
834 location.contains(&urlencoding::encode(malicious_state).to_string()) ||
835 location.contains("state=state%26redirect_uri"),
836 "State parameter should be properly URL-encoded. Got: {}",
837 location
838 );
839}
840
841#[tokio::test]
842async fn test_security_cross_client_token_theft() {
843 let url = base_url().await;
844 let http_client = client();
845
846 let ts = Utc::now().timestamp_millis();
847 let handle = format!("cross-client-{}", ts);
848 let email = format!("cross-client-{}@example.com", ts);
849 let password = "cross-client-password";
850
851 http_client
852 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
853 .json(&json!({
854 "handle": handle,
855 "email": email,
856 "password": password
857 }))
858 .send()
859 .await
860 .unwrap();
861
862 let redirect_uri_a = "https://app-a.com/callback";
863 let mock_client_a = setup_mock_client_metadata(redirect_uri_a).await;
864 let client_id_a = mock_client_a.uri();
865
866 let redirect_uri_b = "https://app-b.com/callback";
867 let mock_client_b = setup_mock_client_metadata(redirect_uri_b).await;
868 let client_id_b = mock_client_b.uri();
869
870 let (code_verifier, code_challenge) = generate_pkce();
871
872 let par_body: Value = http_client
873 .post(format!("{}/oauth/par", url))
874 .form(&[
875 ("response_type", "code"),
876 ("client_id", &client_id_a),
877 ("redirect_uri", redirect_uri_a),
878 ("code_challenge", &code_challenge),
879 ("code_challenge_method", "S256"),
880 ])
881 .send()
882 .await
883 .unwrap()
884 .json()
885 .await
886 .unwrap();
887
888 let request_uri = par_body["request_uri"].as_str().unwrap();
889
890 let auth_client = no_redirect_client();
891 let auth_res = auth_client
892 .post(format!("{}/oauth/authorize", url))
893 .form(&[
894 ("request_uri", request_uri),
895 ("username", &handle),
896 ("password", password),
897 ("remember_device", "false"),
898 ])
899 .send()
900 .await
901 .unwrap();
902
903 let location = auth_res.headers().get("location").unwrap().to_str().unwrap();
904 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap();
905
906 let token_res = http_client
907 .post(format!("{}/oauth/token", url))
908 .form(&[
909 ("grant_type", "authorization_code"),
910 ("code", code),
911 ("redirect_uri", redirect_uri_a),
912 ("code_verifier", &code_verifier),
913 ("client_id", &client_id_b),
914 ])
915 .send()
916 .await
917 .unwrap();
918
919 assert_eq!(
920 token_res.status(),
921 StatusCode::BAD_REQUEST,
922 "Cross-client code exchange must be explicitly rejected (defense-in-depth)"
923 );
924 let body: Value = token_res.json().await.unwrap();
925 assert_eq!(body["error"], "invalid_grant");
926 assert!(
927 body["error_description"].as_str().unwrap().contains("client_id"),
928 "Error should mention client_id mismatch"
929 );
930}
931
932#[test]
933fn test_security_dpop_nonce_tamper_detection() {
934 let secret = b"test-dpop-secret-32-bytes-long!!";
935 let verifier = DPoPVerifier::new(secret);
936
937 let nonce = verifier.generate_nonce();
938 let nonce_bytes = URL_SAFE_NO_PAD.decode(&nonce).unwrap();
939
940 let mut tampered = nonce_bytes.clone();
941 if !tampered.is_empty() {
942 tampered[0] ^= 0xFF;
943 }
944 let tampered_nonce = URL_SAFE_NO_PAD.encode(&tampered);
945
946 let result = verifier.validate_nonce(&tampered_nonce);
947 assert!(result.is_err(), "Tampered nonce should be rejected");
948}
949
950#[test]
951fn test_security_dpop_nonce_cross_server_rejected() {
952 let secret1 = b"server-1-secret-32-bytes-long!!!";
953 let secret2 = b"server-2-secret-32-bytes-long!!!";
954
955 let verifier1 = DPoPVerifier::new(secret1);
956 let verifier2 = DPoPVerifier::new(secret2);
957
958 let nonce_from_server1 = verifier1.generate_nonce();
959
960 let result = verifier2.validate_nonce(&nonce_from_server1);
961 assert!(result.is_err(), "Nonce from different server should be rejected");
962}
963
964#[test]
965fn test_security_dpop_proof_signature_tampering() {
966 use p256::ecdsa::{SigningKey, Signature, signature::Signer};
967 use p256::elliptic_curve::sec1::ToEncodedPoint;
968
969 let secret = b"test-dpop-secret-32-bytes-long!!";
970 let verifier = DPoPVerifier::new(secret);
971
972 let signing_key = SigningKey::random(&mut rand::thread_rng());
973 let verifying_key = signing_key.verifying_key();
974 let point = verifying_key.to_encoded_point(false);
975
976 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
977 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
978
979 let header = json!({
980 "typ": "dpop+jwt",
981 "alg": "ES256",
982 "jwk": {
983 "kty": "EC",
984 "crv": "P-256",
985 "x": x,
986 "y": y
987 }
988 });
989
990 let payload = json!({
991 "jti": format!("tamper-test-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)),
992 "htm": "POST",
993 "htu": "https://example.com/token",
994 "iat": Utc::now().timestamp()
995 });
996
997 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
998 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
999
1000 let signing_input = format!("{}.{}", header_b64, payload_b64);
1001 let signature: Signature = signing_key.sign(signing_input.as_bytes());
1002 let mut sig_bytes = signature.to_bytes().to_vec();
1003
1004 sig_bytes[0] ^= 0xFF;
1005 let tampered_sig = URL_SAFE_NO_PAD.encode(&sig_bytes);
1006
1007 let tampered_proof = format!("{}.{}.{}", header_b64, payload_b64, tampered_sig);
1008
1009 let result = verifier.verify_proof(&tampered_proof, "POST", "https://example.com/token", None);
1010 assert!(result.is_err(), "Tampered DPoP signature should be rejected");
1011}
1012
1013#[test]
1014fn test_security_dpop_proof_key_substitution() {
1015 use p256::ecdsa::{SigningKey, Signature, signature::Signer};
1016 use p256::elliptic_curve::sec1::ToEncodedPoint;
1017
1018 let secret = b"test-dpop-secret-32-bytes-long!!";
1019 let verifier = DPoPVerifier::new(secret);
1020
1021 let signing_key = SigningKey::random(&mut rand::thread_rng());
1022
1023 let attacker_key = SigningKey::random(&mut rand::thread_rng());
1024 let attacker_verifying = attacker_key.verifying_key();
1025 let attacker_point = attacker_verifying.to_encoded_point(false);
1026
1027 let x = URL_SAFE_NO_PAD.encode(attacker_point.x().unwrap());
1028 let y = URL_SAFE_NO_PAD.encode(attacker_point.y().unwrap());
1029
1030 let header = json!({
1031 "typ": "dpop+jwt",
1032 "alg": "ES256",
1033 "jwk": {
1034 "kty": "EC",
1035 "crv": "P-256",
1036 "x": x,
1037 "y": y
1038 }
1039 });
1040
1041 let payload = json!({
1042 "jti": format!("key-sub-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)),
1043 "htm": "POST",
1044 "htu": "https://example.com/token",
1045 "iat": Utc::now().timestamp()
1046 });
1047
1048 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
1049 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
1050 let signing_input = format!("{}.{}", header_b64, payload_b64);
1051 let signature: Signature = signing_key.sign(signing_input.as_bytes());
1052 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
1053
1054 let mismatched_proof = format!("{}.{}.{}", header_b64, payload_b64, signature_b64);
1055
1056 let result = verifier.verify_proof(&mismatched_proof, "POST", "https://example.com/token", None);
1057 assert!(result.is_err(), "DPoP proof with mismatched key should be rejected");
1058}
1059
1060#[test]
1061fn test_security_jwk_thumbprint_consistency() {
1062 let jwk = DPoPJwk {
1063 kty: "EC".to_string(),
1064 crv: Some("P-256".to_string()),
1065 x: Some("WbbXrPhtCg66wuF0NLhzXxF5PFzNZ7wNJm9M_1pCcXY".to_string()),
1066 y: Some("DubR6_2kU1H5EYhbcNpYZGy1EY6GEKKxv6PYx8VW0rA".to_string()),
1067 };
1068
1069 let mut results = Vec::new();
1070 for _ in 0..100 {
1071 results.push(compute_jwk_thumbprint(&jwk).unwrap());
1072 }
1073
1074 let first = &results[0];
1075 for (i, result) in results.iter().enumerate() {
1076 assert_eq!(first, result, "Thumbprint should be deterministic, but iteration {} differs", i);
1077 }
1078}
1079
1080#[test]
1081fn test_security_dpop_iat_clock_skew_limits() {
1082 use p256::ecdsa::{SigningKey, Signature, signature::Signer};
1083 use p256::elliptic_curve::sec1::ToEncodedPoint;
1084
1085 let secret = b"test-dpop-secret-32-bytes-long!!";
1086 let verifier = DPoPVerifier::new(secret);
1087
1088 let test_offsets = vec![
1089 (-600, true),
1090 (-301, true),
1091 (-299, false),
1092 (0, false),
1093 (299, false),
1094 (301, true),
1095 (600, true),
1096 ];
1097
1098 for (offset_secs, should_fail) in test_offsets {
1099 let signing_key = SigningKey::random(&mut rand::thread_rng());
1100 let verifying_key = signing_key.verifying_key();
1101 let point = verifying_key.to_encoded_point(false);
1102
1103 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
1104 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
1105
1106 let header = json!({
1107 "typ": "dpop+jwt",
1108 "alg": "ES256",
1109 "jwk": {
1110 "kty": "EC",
1111 "crv": "P-256",
1112 "x": x,
1113 "y": y
1114 }
1115 });
1116
1117 let payload = json!({
1118 "jti": format!("clock-{}-{}", offset_secs, Utc::now().timestamp_nanos_opt().unwrap_or(0)),
1119 "htm": "POST",
1120 "htu": "https://example.com/token",
1121 "iat": Utc::now().timestamp() + offset_secs
1122 });
1123
1124 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
1125 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
1126 let signing_input = format!("{}.{}", header_b64, payload_b64);
1127 let signature: Signature = signing_key.sign(signing_input.as_bytes());
1128 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
1129
1130 let proof = format!("{}.{}.{}", header_b64, payload_b64, signature_b64);
1131
1132 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None);
1133
1134 if should_fail {
1135 assert!(result.is_err(), "iat offset {} should be rejected", offset_secs);
1136 } else {
1137 assert!(result.is_ok(), "iat offset {} should be accepted", offset_secs);
1138 }
1139 }
1140}
1141
1142#[test]
1143fn test_security_dpop_method_case_insensitivity() {
1144 use p256::ecdsa::{SigningKey, Signature, signature::Signer};
1145 use p256::elliptic_curve::sec1::ToEncodedPoint;
1146
1147 let secret = b"test-dpop-secret-32-bytes-long!!";
1148 let verifier = DPoPVerifier::new(secret);
1149
1150 let signing_key = SigningKey::random(&mut rand::thread_rng());
1151 let verifying_key = signing_key.verifying_key();
1152 let point = verifying_key.to_encoded_point(false);
1153
1154 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
1155 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
1156
1157 let header = json!({
1158 "typ": "dpop+jwt",
1159 "alg": "ES256",
1160 "jwk": {
1161 "kty": "EC",
1162 "crv": "P-256",
1163 "x": x,
1164 "y": y
1165 }
1166 });
1167
1168 let payload = json!({
1169 "jti": format!("case-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)),
1170 "htm": "post",
1171 "htu": "https://example.com/token",
1172 "iat": Utc::now().timestamp()
1173 });
1174
1175 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
1176 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
1177 let signing_input = format!("{}.{}", header_b64, payload_b64);
1178 let signature: Signature = signing_key.sign(signing_input.as_bytes());
1179 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
1180
1181 let proof = format!("{}.{}.{}", header_b64, payload_b64, signature_b64);
1182
1183 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None);
1184 assert!(result.is_ok(), "HTTP method comparison should be case-insensitive");
1185}
1186
1187#[tokio::test]
1188async fn test_security_invalid_grant_type_rejected() {
1189 let url = base_url().await;
1190 let http_client = client();
1191
1192 let grant_types = vec![
1193 "client_credentials",
1194 "password",
1195 "implicit",
1196 "urn:ietf:params:oauth:grant-type:jwt-bearer",
1197 "urn:ietf:params:oauth:grant-type:device_code",
1198 "",
1199 "AUTHORIZATION_CODE",
1200 "Authorization_Code",
1201 ];
1202
1203 for grant_type in grant_types {
1204 let res = http_client
1205 .post(format!("{}/oauth/token", url))
1206 .form(&[
1207 ("grant_type", grant_type),
1208 ("client_id", "https://example.com"),
1209 ])
1210 .send()
1211 .await
1212 .unwrap();
1213
1214 assert_eq!(
1215 res.status(),
1216 StatusCode::BAD_REQUEST,
1217 "Grant type '{}' should be rejected",
1218 grant_type
1219 );
1220 }
1221}
1222
1223#[tokio::test]
1224async fn test_security_token_with_wrong_typ_rejected() {
1225 let url = base_url().await;
1226 let http_client = client();
1227
1228 let wrong_types = vec![
1229 "JWT",
1230 "jwt",
1231 "at+JWT",
1232 "access_token",
1233 "",
1234 ];
1235
1236 for typ in wrong_types {
1237 let header = json!({
1238 "alg": "HS256",
1239 "typ": typ
1240 });
1241 let payload = json!({
1242 "iss": "https://test.pds",
1243 "sub": "did:plc:test",
1244 "aud": "https://test.pds",
1245 "iat": Utc::now().timestamp(),
1246 "exp": Utc::now().timestamp() + 3600,
1247 "jti": "wrong-typ-token"
1248 });
1249
1250 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
1251 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
1252 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 32]);
1253 let token = format!("{}.{}.{}", header_b64, payload_b64, fake_sig);
1254
1255 let res = http_client
1256 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
1257 .header("Authorization", format!("Bearer {}", token))
1258 .send()
1259 .await
1260 .unwrap();
1261
1262 assert_eq!(
1263 res.status(),
1264 StatusCode::UNAUTHORIZED,
1265 "Token with typ='{}' should be rejected",
1266 typ
1267 );
1268 }
1269}
1270
1271#[tokio::test]
1272async fn test_security_missing_required_claims_rejected() {
1273 let url = base_url().await;
1274 let http_client = client();
1275
1276 let tokens_missing_claims = vec![
1277 (json!({"iss": "x", "sub": "x", "aud": "x", "iat": 0}), "exp"),
1278 (json!({"iss": "x", "sub": "x", "aud": "x", "exp": 9999999999i64}), "iat"),
1279 (json!({"iss": "x", "aud": "x", "iat": 0, "exp": 9999999999i64}), "sub"),
1280 ];
1281
1282 for (payload, missing_claim) in tokens_missing_claims {
1283 let header = json!({
1284 "alg": "HS256",
1285 "typ": "at+jwt"
1286 });
1287
1288 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
1289 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
1290 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 32]);
1291 let token = format!("{}.{}.{}", header_b64, payload_b64, fake_sig);
1292
1293 let res = http_client
1294 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
1295 .header("Authorization", format!("Bearer {}", token))
1296 .send()
1297 .await
1298 .unwrap();
1299
1300 assert_eq!(
1301 res.status(),
1302 StatusCode::UNAUTHORIZED,
1303 "Token missing '{}' claim should be rejected",
1304 missing_claim
1305 );
1306 }
1307}
1308
1309#[tokio::test]
1310async fn test_security_malformed_tokens_rejected() {
1311 let url = base_url().await;
1312 let http_client = client();
1313
1314 let malformed_tokens = vec![
1315 "",
1316 "not-a-token",
1317 "one.two",
1318 "one.two.three.four",
1319 "....",
1320 "eyJhbGciOiJIUzI1NiJ9",
1321 "eyJhbGciOiJIUzI1NiJ9.",
1322 "eyJhbGciOiJIUzI1NiJ9..",
1323 ".eyJzdWIiOiJ0ZXN0In0.",
1324 "!!invalid-base64!!.eyJzdWIiOiJ0ZXN0In0.sig",
1325 "eyJhbGciOiJIUzI1NiJ9.!!invalid!!.sig",
1326 ];
1327
1328 for token in malformed_tokens {
1329 let res = http_client
1330 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
1331 .header("Authorization", format!("Bearer {}", token))
1332 .send()
1333 .await
1334 .unwrap();
1335
1336 assert_eq!(
1337 res.status(),
1338 StatusCode::UNAUTHORIZED,
1339 "Malformed token '{}' should be rejected",
1340 if token.len() > 50 { &token[..50] } else { token }
1341 );
1342 }
1343}
1344
1345#[tokio::test]
1346async fn test_security_authorization_header_formats() {
1347 let url = base_url().await;
1348 let http_client = client();
1349
1350 let (access_token, _, _) = get_oauth_tokens(&http_client, url).await;
1351
1352 let valid_case_variants = vec![
1353 format!("bearer {}", access_token),
1354 format!("BEARER {}", access_token),
1355 format!("Bearer {}", access_token),
1356 ];
1357
1358 for auth_header in valid_case_variants {
1359 let res = http_client
1360 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
1361 .header("Authorization", &auth_header)
1362 .send()
1363 .await
1364 .unwrap();
1365
1366 assert_eq!(
1367 res.status(),
1368 StatusCode::OK,
1369 "Auth header '{}...' should be accepted (RFC 7235 case-insensitivity)",
1370 if auth_header.len() > 30 { &auth_header[..30] } else { &auth_header }
1371 );
1372 }
1373
1374 let invalid_formats = vec![
1375 format!("Basic {}", access_token),
1376 format!("Digest {}", access_token),
1377 access_token.clone(),
1378 format!("Bearer{}", access_token),
1379 ];
1380
1381 for auth_header in invalid_formats {
1382 let res = http_client
1383 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
1384 .header("Authorization", &auth_header)
1385 .send()
1386 .await
1387 .unwrap();
1388
1389 assert_eq!(
1390 res.status(),
1391 StatusCode::UNAUTHORIZED,
1392 "Auth header '{}...' should be rejected",
1393 if auth_header.len() > 30 { &auth_header[..30] } else { &auth_header }
1394 );
1395 }
1396}
1397
1398#[tokio::test]
1399async fn test_security_no_authorization_header() {
1400 let url = base_url().await;
1401 let http_client = client();
1402
1403 let res = http_client
1404 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
1405 .send()
1406 .await
1407 .unwrap();
1408
1409 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Missing auth header should return 401");
1410}
1411
1412#[tokio::test]
1413async fn test_security_empty_authorization_header() {
1414 let url = base_url().await;
1415 let http_client = client();
1416
1417 let res = http_client
1418 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
1419 .header("Authorization", "")
1420 .send()
1421 .await
1422 .unwrap();
1423
1424 assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Empty auth header should return 401");
1425}
1426
1427#[tokio::test]
1428async fn test_security_revoked_token_rejected() {
1429 let url = base_url().await;
1430 let http_client = client();
1431
1432 let (access_token, refresh_token, _) = get_oauth_tokens(&http_client, url).await;
1433
1434 let revoke_res = http_client
1435 .post(format!("{}/oauth/revoke", url))
1436 .form(&[("token", &refresh_token)])
1437 .send()
1438 .await
1439 .unwrap();
1440
1441 assert_eq!(revoke_res.status(), StatusCode::OK);
1442
1443 let introspect_res = http_client
1444 .post(format!("{}/oauth/introspect", url))
1445 .form(&[("token", &access_token)])
1446 .send()
1447 .await
1448 .unwrap();
1449
1450 let introspect_body: Value = introspect_res.json().await.unwrap();
1451 assert_eq!(introspect_body["active"], false, "Revoked token should be inactive");
1452}
1453
1454#[tokio::test]
1455#[ignore = "rate limiting is disabled in test environment"]
1456async fn test_security_oauth_authorize_rate_limiting() {
1457 let url = base_url().await;
1458 let http_client = no_redirect_client();
1459
1460 let ts = Utc::now().timestamp_nanos_opt().unwrap_or(0);
1461 let unique_ip = format!("10.{}.{}.{}", (ts >> 16) & 0xFF, (ts >> 8) & 0xFF, ts & 0xFF);
1462
1463 let redirect_uri = "https://example.com/rate-limit-callback";
1464 let mock_client = setup_mock_client_metadata(redirect_uri).await;
1465 let client_id = mock_client.uri();
1466
1467 let (_, code_challenge) = generate_pkce();
1468
1469 let client_for_par = client();
1470 let par_body: Value = client_for_par
1471 .post(format!("{}/oauth/par", url))
1472 .form(&[
1473 ("response_type", "code"),
1474 ("client_id", &client_id),
1475 ("redirect_uri", redirect_uri),
1476 ("code_challenge", &code_challenge),
1477 ("code_challenge_method", "S256"),
1478 ])
1479 .send()
1480 .await
1481 .unwrap()
1482 .json()
1483 .await
1484 .unwrap();
1485
1486 let request_uri = par_body["request_uri"].as_str().unwrap();
1487
1488 let mut rate_limited_count = 0;
1489 let mut other_count = 0;
1490
1491 for _ in 0..15 {
1492 let res = http_client
1493 .post(format!("{}/oauth/authorize", url))
1494 .header("X-Forwarded-For", &unique_ip)
1495 .form(&[
1496 ("request_uri", request_uri),
1497 ("username", "nonexistent_user"),
1498 ("password", "wrong_password"),
1499 ("remember_device", "false"),
1500 ])
1501 .send()
1502 .await
1503 .unwrap();
1504
1505 match res.status() {
1506 StatusCode::TOO_MANY_REQUESTS => rate_limited_count += 1,
1507 _ => other_count += 1,
1508 }
1509 }
1510
1511 assert!(
1512 rate_limited_count > 0,
1513 "Expected at least one rate-limited response after 15 OAuth authorize attempts. Got {} other and {} rate limited.",
1514 other_count,
1515 rate_limited_count
1516 );
1517}
1518
1519fn create_dpop_proof(
1520 method: &str,
1521 uri: &str,
1522 nonce: Option<&str>,
1523 ath: Option<&str>,
1524 iat_offset_secs: i64,
1525) -> String {
1526 use p256::ecdsa::{SigningKey, Signature, signature::Signer};
1527
1528 let signing_key = SigningKey::random(&mut rand::thread_rng());
1529 let verifying_key = signing_key.verifying_key();
1530 let point = verifying_key.to_encoded_point(false);
1531
1532 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
1533 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
1534
1535 let jwk = json!({
1536 "kty": "EC",
1537 "crv": "P-256",
1538 "x": x,
1539 "y": y
1540 });
1541
1542 let header = json!({
1543 "typ": "dpop+jwt",
1544 "alg": "ES256",
1545 "jwk": jwk
1546 });
1547
1548 let mut payload = json!({
1549 "jti": format!("unique-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)),
1550 "htm": method,
1551 "htu": uri,
1552 "iat": Utc::now().timestamp() + iat_offset_secs
1553 });
1554
1555 if let Some(n) = nonce {
1556 payload["nonce"] = json!(n);
1557 }
1558
1559 if let Some(a) = ath {
1560 payload["ath"] = json!(a);
1561 }
1562
1563 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
1564 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
1565
1566 let signing_input = format!("{}.{}", header_b64, payload_b64);
1567 let signature: Signature = signing_key.sign(signing_input.as_bytes());
1568 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
1569
1570 format!("{}.{}", signing_input, signature_b64)
1571}
1572
1573#[test]
1574fn test_dpop_nonce_generation() {
1575 let secret = b"test-dpop-secret-32-bytes-long!!";
1576 let verifier = DPoPVerifier::new(secret);
1577
1578 let nonce1 = verifier.generate_nonce();
1579 let nonce2 = verifier.generate_nonce();
1580
1581 assert!(!nonce1.is_empty());
1582 assert!(!nonce2.is_empty());
1583}
1584
1585#[test]
1586fn test_dpop_nonce_validation_success() {
1587 let secret = b"test-dpop-secret-32-bytes-long!!";
1588 let verifier = DPoPVerifier::new(secret);
1589
1590 let nonce = verifier.generate_nonce();
1591 let result = verifier.validate_nonce(&nonce);
1592
1593 assert!(result.is_ok(), "Valid nonce should pass: {:?}", result);
1594}
1595
1596#[test]
1597fn test_dpop_nonce_wrong_secret() {
1598 let secret1 = b"test-dpop-secret-32-bytes-long!!";
1599 let secret2 = b"different-secret-32-bytes-long!!";
1600
1601 let verifier1 = DPoPVerifier::new(secret1);
1602 let verifier2 = DPoPVerifier::new(secret2);
1603
1604 let nonce = verifier1.generate_nonce();
1605 let result = verifier2.validate_nonce(&nonce);
1606
1607 assert!(result.is_err(), "Nonce from different secret should fail");
1608}
1609
1610#[test]
1611fn test_dpop_nonce_invalid_format() {
1612 let secret = b"test-dpop-secret-32-bytes-long!!";
1613 let verifier = DPoPVerifier::new(secret);
1614
1615 assert!(verifier.validate_nonce("invalid").is_err());
1616 assert!(verifier.validate_nonce("").is_err());
1617 assert!(verifier.validate_nonce("!!!not-base64!!!").is_err());
1618}
1619
1620#[test]
1621fn test_jwk_thumbprint_ec_p256() {
1622 let jwk = DPoPJwk {
1623 kty: "EC".to_string(),
1624 crv: Some("P-256".to_string()),
1625 x: Some("WbbXrPhtCg66wuF0NLhzXxF5PFzNZ7wNJm9M_1pCcXY".to_string()),
1626 y: Some("DubR6_2kU1H5EYhbcNpYZGy1EY6GEKKxv6PYx8VW0rA".to_string()),
1627 };
1628
1629 let thumbprint = compute_jwk_thumbprint(&jwk);
1630 assert!(thumbprint.is_ok());
1631
1632 let tp = thumbprint.unwrap();
1633 assert!(!tp.is_empty());
1634 assert!(tp.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_'));
1635}
1636
1637#[test]
1638fn test_jwk_thumbprint_ec_secp256k1() {
1639 let jwk = DPoPJwk {
1640 kty: "EC".to_string(),
1641 crv: Some("secp256k1".to_string()),
1642 x: Some("some_x_value".to_string()),
1643 y: Some("some_y_value".to_string()),
1644 };
1645
1646 let thumbprint = compute_jwk_thumbprint(&jwk);
1647 assert!(thumbprint.is_ok());
1648}
1649
1650#[test]
1651fn test_jwk_thumbprint_okp_ed25519() {
1652 let jwk = DPoPJwk {
1653 kty: "OKP".to_string(),
1654 crv: Some("Ed25519".to_string()),
1655 x: Some("some_x_value".to_string()),
1656 y: None,
1657 };
1658
1659 let thumbprint = compute_jwk_thumbprint(&jwk);
1660 assert!(thumbprint.is_ok());
1661}
1662
1663#[test]
1664fn test_jwk_thumbprint_missing_crv() {
1665 let jwk = DPoPJwk {
1666 kty: "EC".to_string(),
1667 crv: None,
1668 x: Some("x".to_string()),
1669 y: Some("y".to_string()),
1670 };
1671
1672 let thumbprint = compute_jwk_thumbprint(&jwk);
1673 assert!(thumbprint.is_err());
1674}
1675
1676#[test]
1677fn test_jwk_thumbprint_missing_x() {
1678 let jwk = DPoPJwk {
1679 kty: "EC".to_string(),
1680 crv: Some("P-256".to_string()),
1681 x: None,
1682 y: Some("y".to_string()),
1683 };
1684
1685 let thumbprint = compute_jwk_thumbprint(&jwk);
1686 assert!(thumbprint.is_err());
1687}
1688
1689#[test]
1690fn test_jwk_thumbprint_missing_y_for_ec() {
1691 let jwk = DPoPJwk {
1692 kty: "EC".to_string(),
1693 crv: Some("P-256".to_string()),
1694 x: Some("x".to_string()),
1695 y: None,
1696 };
1697
1698 let thumbprint = compute_jwk_thumbprint(&jwk);
1699 assert!(thumbprint.is_err());
1700}
1701
1702#[test]
1703fn test_jwk_thumbprint_unsupported_key_type() {
1704 let jwk = DPoPJwk {
1705 kty: "RSA".to_string(),
1706 crv: None,
1707 x: None,
1708 y: None,
1709 };
1710
1711 let thumbprint = compute_jwk_thumbprint(&jwk);
1712 assert!(thumbprint.is_err());
1713}
1714
1715#[test]
1716fn test_jwk_thumbprint_deterministic() {
1717 let jwk = DPoPJwk {
1718 kty: "EC".to_string(),
1719 crv: Some("P-256".to_string()),
1720 x: Some("WbbXrPhtCg66wuF0NLhzXxF5PFzNZ7wNJm9M_1pCcXY".to_string()),
1721 y: Some("DubR6_2kU1H5EYhbcNpYZGy1EY6GEKKxv6PYx8VW0rA".to_string()),
1722 };
1723
1724 let tp1 = compute_jwk_thumbprint(&jwk).unwrap();
1725 let tp2 = compute_jwk_thumbprint(&jwk).unwrap();
1726
1727 assert_eq!(tp1, tp2, "Thumbprint should be deterministic");
1728}
1729
1730#[test]
1731fn test_dpop_proof_invalid_format() {
1732 let secret = b"test-dpop-secret-32-bytes-long!!";
1733 let verifier = DPoPVerifier::new(secret);
1734
1735 let result = verifier.verify_proof("not.enough.parts", "POST", "https://example.com", None);
1736 assert!(result.is_err());
1737
1738 let result = verifier.verify_proof("invalid", "POST", "https://example.com", None);
1739 assert!(result.is_err());
1740}
1741
1742#[test]
1743fn test_dpop_proof_invalid_typ() {
1744 let secret = b"test-dpop-secret-32-bytes-long!!";
1745 let verifier = DPoPVerifier::new(secret);
1746
1747 let header = json!({
1748 "typ": "JWT",
1749 "alg": "ES256",
1750 "jwk": {
1751 "kty": "EC",
1752 "crv": "P-256",
1753 "x": "x",
1754 "y": "y"
1755 }
1756 });
1757
1758 let payload = json!({
1759 "jti": "unique",
1760 "htm": "POST",
1761 "htu": "https://example.com",
1762 "iat": Utc::now().timestamp()
1763 });
1764
1765 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
1766 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
1767 let proof = format!("{}.{}.sig", header_b64, payload_b64);
1768
1769 let result = verifier.verify_proof(&proof, "POST", "https://example.com", None);
1770 assert!(result.is_err());
1771}
1772
1773#[test]
1774fn test_dpop_proof_method_mismatch() {
1775 let secret = b"test-dpop-secret-32-bytes-long!!";
1776 let verifier = DPoPVerifier::new(secret);
1777
1778 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0);
1779
1780 let result = verifier.verify_proof(&proof, "GET", "https://example.com/token", None);
1781 assert!(result.is_err());
1782}
1783
1784#[test]
1785fn test_dpop_proof_uri_mismatch() {
1786 let secret = b"test-dpop-secret-32-bytes-long!!";
1787 let verifier = DPoPVerifier::new(secret);
1788
1789 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0);
1790
1791 let result = verifier.verify_proof(&proof, "POST", "https://other.com/token", None);
1792 assert!(result.is_err());
1793}
1794
1795#[test]
1796fn test_dpop_proof_iat_too_old() {
1797 let secret = b"test-dpop-secret-32-bytes-long!!";
1798 let verifier = DPoPVerifier::new(secret);
1799
1800 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, -600);
1801
1802 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None);
1803 assert!(result.is_err());
1804}
1805
1806#[test]
1807fn test_dpop_proof_iat_future() {
1808 let secret = b"test-dpop-secret-32-bytes-long!!";
1809 let verifier = DPoPVerifier::new(secret);
1810
1811 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 600);
1812
1813 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None);
1814 assert!(result.is_err());
1815}
1816
1817#[test]
1818fn test_dpop_proof_ath_mismatch() {
1819 let secret = b"test-dpop-secret-32-bytes-long!!";
1820 let verifier = DPoPVerifier::new(secret);
1821
1822 let proof = create_dpop_proof(
1823 "GET",
1824 "https://example.com/resource",
1825 None,
1826 Some("wrong_hash"),
1827 0,
1828 );
1829
1830 let result = verifier.verify_proof(
1831 &proof,
1832 "GET",
1833 "https://example.com/resource",
1834 Some("correct_hash"),
1835 );
1836 assert!(result.is_err());
1837}
1838
1839#[test]
1840fn test_dpop_proof_missing_ath_when_required() {
1841 let secret = b"test-dpop-secret-32-bytes-long!!";
1842 let verifier = DPoPVerifier::new(secret);
1843
1844 let proof = create_dpop_proof("GET", "https://example.com/resource", None, None, 0);
1845
1846 let result = verifier.verify_proof(
1847 &proof,
1848 "GET",
1849 "https://example.com/resource",
1850 Some("expected_hash"),
1851 );
1852 assert!(result.is_err());
1853}
1854
1855#[test]
1856fn test_dpop_proof_uri_ignores_query_params() {
1857 let secret = b"test-dpop-secret-32-bytes-long!!";
1858 let verifier = DPoPVerifier::new(secret);
1859
1860 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0);
1861
1862 let result = verifier.verify_proof(
1863 &proof,
1864 "POST",
1865 "https://example.com/token?foo=bar",
1866 None,
1867 );
1868
1869 assert!(result.is_ok(), "Query params should be ignored: {:?}", result);
1870}