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