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, get_db_connection_string}; 6use helpers::verify_new_account; 7use reqwest::{StatusCode, redirect}; 8use serde_json::{Value, json}; 9use sha2::{Digest, Sha256}; 10use wiremock::matchers::{method, path}; 11use wiremock::{Mock, MockServer, ResponseTemplate}; 12 13fn no_redirect_client() -> reqwest::Client { 14 reqwest::Client::builder().redirect(redirect::Policy::none()).build().unwrap() 15} 16 17fn generate_pkce() -> (String, String) { 18 let verifier_bytes: [u8; 32] = rand::random(); 19 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes); 20 let mut hasher = Sha256::new(); 21 hasher.update(code_verifier.as_bytes()); 22 let code_challenge = URL_SAFE_NO_PAD.encode(&hasher.finalize()); 23 (code_verifier, code_challenge) 24} 25 26async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer { 27 let mock_server = MockServer::start().await; 28 let client_id = mock_server.uri(); 29 let metadata = json!({ 30 "client_id": client_id, 31 "client_name": "Test OAuth Client", 32 "redirect_uris": [redirect_uri], 33 "grant_types": ["authorization_code", "refresh_token"], 34 "response_types": ["code"], 35 "token_endpoint_auth_method": "none", 36 "dpop_bound_access_tokens": false 37 }); 38 Mock::given(method("GET")) 39 .and(path("/")) 40 .respond_with(ResponseTemplate::new(200).set_body_json(metadata)) 41 .mount(&mock_server) 42 .await; 43 mock_server 44} 45 46#[tokio::test] 47async fn test_oauth_metadata_endpoints() { 48 let url = base_url().await; 49 let client = client(); 50 let pr_res = client.get(format!("{}/.well-known/oauth-protected-resource", url)).send().await.unwrap(); 51 assert_eq!(pr_res.status(), StatusCode::OK); 52 let pr_body: Value = pr_res.json().await.unwrap(); 53 assert!(pr_body["resource"].is_string()); 54 assert!(pr_body["authorization_servers"].is_array()); 55 assert!(pr_body["bearer_methods_supported"].as_array().unwrap().contains(&json!("header"))); 56 let as_res = client.get(format!("{}/.well-known/oauth-authorization-server", url)).send().await.unwrap(); 57 assert_eq!(as_res.status(), StatusCode::OK); 58 let as_body: Value = as_res.json().await.unwrap(); 59 assert!(as_body["issuer"].is_string()); 60 assert!(as_body["authorization_endpoint"].is_string()); 61 assert!(as_body["token_endpoint"].is_string()); 62 assert!(as_body["jwks_uri"].is_string()); 63 assert!(as_body["response_types_supported"].as_array().unwrap().contains(&json!("code"))); 64 assert!(as_body["grant_types_supported"].as_array().unwrap().contains(&json!("authorization_code"))); 65 assert!(as_body["code_challenge_methods_supported"].as_array().unwrap().contains(&json!("S256"))); 66 assert_eq!(as_body["require_pushed_authorization_requests"], json!(true)); 67 assert!(as_body["dpop_signing_alg_values_supported"].as_array().unwrap().contains(&json!("ES256"))); 68 let jwks_res = client.get(format!("{}/oauth/jwks", url)).send().await.unwrap(); 69 assert_eq!(jwks_res.status(), StatusCode::OK); 70 let jwks_body: Value = jwks_res.json().await.unwrap(); 71 assert!(jwks_body["keys"].is_array()); 72} 73 74#[tokio::test] 75async fn test_par_and_authorize() { 76 let url = base_url().await; 77 let client = client(); 78 let redirect_uri = "https://example.com/callback"; 79 let mock_client = setup_mock_client_metadata(redirect_uri).await; 80 let client_id = mock_client.uri(); 81 let (_, code_challenge) = generate_pkce(); 82 let par_res = client 83 .post(format!("{}/oauth/par", url)) 84 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri), 85 ("code_challenge", &code_challenge), ("code_challenge_method", "S256"), ("scope", "atproto"), ("state", "test-state")]) 86 .send().await.unwrap(); 87 assert_eq!(par_res.status(), StatusCode::CREATED, "PAR should succeed"); 88 let par_body: Value = par_res.json().await.unwrap(); 89 assert!(par_body["request_uri"].is_string()); 90 assert!(par_body["expires_in"].is_number()); 91 let request_uri = par_body["request_uri"].as_str().unwrap(); 92 assert!(request_uri.starts_with("urn:ietf:params:oauth:request_uri:")); 93 let auth_res = client 94 .get(format!("{}/oauth/authorize", url)) 95 .header("Accept", "application/json") 96 .query(&[("request_uri", request_uri)]) 97 .send().await.unwrap(); 98 assert_eq!(auth_res.status(), StatusCode::OK); 99 let auth_body: Value = auth_res.json().await.unwrap(); 100 assert_eq!(auth_body["client_id"], client_id); 101 assert_eq!(auth_body["redirect_uri"], redirect_uri); 102 assert_eq!(auth_body["scope"], "atproto"); 103 let invalid_res = client 104 .get(format!("{}/oauth/authorize", url)) 105 .header("Accept", "application/json") 106 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:nonexistent")]) 107 .send().await.unwrap(); 108 assert_eq!(invalid_res.status(), StatusCode::BAD_REQUEST); 109 let missing_res = client.get(format!("{}/oauth/authorize", url)).send().await.unwrap(); 110 assert_eq!(missing_res.status(), StatusCode::BAD_REQUEST); 111} 112 113#[tokio::test] 114async fn test_full_oauth_flow() { 115 let url = base_url().await; 116 let http_client = client(); 117 let ts = Utc::now().timestamp_millis(); 118 let handle = format!("oauth-test-{}", ts); 119 let email = format!("oauth-test-{}@example.com", ts); 120 let password = "oauth-test-password"; 121 let create_res = http_client 122 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 123 .json(&json!({ "handle": handle, "email": email, "password": password })) 124 .send().await.unwrap(); 125 assert_eq!(create_res.status(), StatusCode::OK); 126 let account: Value = create_res.json().await.unwrap(); 127 let user_did = account["did"].as_str().unwrap(); 128 verify_new_account(&http_client, user_did).await; 129 let redirect_uri = "https://example.com/oauth/callback"; 130 let mock_client = setup_mock_client_metadata(redirect_uri).await; 131 let client_id = mock_client.uri(); 132 let (code_verifier, code_challenge) = generate_pkce(); 133 let state = format!("state-{}", ts); 134 let par_res = http_client 135 .post(format!("{}/oauth/par", url)) 136 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri), 137 ("code_challenge", &code_challenge), ("code_challenge_method", "S256"), ("scope", "atproto"), ("state", &state)]) 138 .send().await.unwrap(); 139 let par_body: Value = par_res.json().await.unwrap(); 140 let request_uri = par_body["request_uri"].as_str().unwrap(); 141 let auth_client = no_redirect_client(); 142 let auth_res = auth_client 143 .post(format!("{}/oauth/authorize", url)) 144 .form(&[("request_uri", request_uri), ("username", &handle), ("password", password), ("remember_device", "false")]) 145 .send().await.unwrap(); 146 assert!(auth_res.status().is_redirection(), "Expected redirect, got {}", auth_res.status()); 147 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 148 assert!(location.starts_with(redirect_uri), "Redirect to wrong URI"); 149 assert!(location.contains("code="), "No code in redirect"); 150 assert!(location.contains(&format!("state={}", state)), "Wrong state"); 151 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 152 let token_res = http_client 153 .post(format!("{}/oauth/token", url)) 154 .form(&[("grant_type", "authorization_code"), ("code", code), ("redirect_uri", redirect_uri), 155 ("code_verifier", &code_verifier), ("client_id", &client_id)]) 156 .send().await.unwrap(); 157 assert_eq!(token_res.status(), StatusCode::OK, "Token exchange failed"); 158 let token_body: Value = token_res.json().await.unwrap(); 159 assert!(token_body["access_token"].is_string()); 160 assert!(token_body["refresh_token"].is_string()); 161 assert_eq!(token_body["token_type"], "Bearer"); 162 assert!(token_body["expires_in"].is_number()); 163 assert_eq!(token_body["sub"], user_did); 164 let access_token = token_body["access_token"].as_str().unwrap(); 165 let refresh_token = token_body["refresh_token"].as_str().unwrap(); 166 let refresh_res = http_client 167 .post(format!("{}/oauth/token", url)) 168 .form(&[("grant_type", "refresh_token"), ("refresh_token", refresh_token), ("client_id", &client_id)]) 169 .send().await.unwrap(); 170 assert_eq!(refresh_res.status(), StatusCode::OK); 171 let refresh_body: Value = refresh_res.json().await.unwrap(); 172 assert_ne!(refresh_body["access_token"].as_str().unwrap(), access_token); 173 assert_ne!(refresh_body["refresh_token"].as_str().unwrap(), refresh_token); 174 let introspect_res = http_client 175 .post(format!("{}/oauth/introspect", url)) 176 .form(&[("token", refresh_body["access_token"].as_str().unwrap())]) 177 .send().await.unwrap(); 178 assert_eq!(introspect_res.status(), StatusCode::OK); 179 let introspect_body: Value = introspect_res.json().await.unwrap(); 180 assert_eq!(introspect_body["active"], true); 181 let revoke_res = http_client 182 .post(format!("{}/oauth/revoke", url)) 183 .form(&[("token", refresh_body["refresh_token"].as_str().unwrap())]) 184 .send().await.unwrap(); 185 assert_eq!(revoke_res.status(), StatusCode::OK); 186 let introspect_after = http_client 187 .post(format!("{}/oauth/introspect", url)) 188 .form(&[("token", refresh_body["access_token"].as_str().unwrap())]) 189 .send().await.unwrap(); 190 let after_body: Value = introspect_after.json().await.unwrap(); 191 assert_eq!(after_body["active"], false, "Revoked token should be inactive"); 192} 193 194#[tokio::test] 195async fn test_oauth_error_cases() { 196 let url = base_url().await; 197 let http_client = client(); 198 let ts = Utc::now().timestamp_millis(); 199 let handle = format!("wrong-creds-{}", ts); 200 let email = format!("wrong-creds-{}@example.com", ts); 201 http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 202 .json(&json!({ "handle": handle, "email": email, "password": "correct-password" })) 203 .send().await.unwrap(); 204 let redirect_uri = "https://example.com/callback"; 205 let mock_client = setup_mock_client_metadata(redirect_uri).await; 206 let client_id = mock_client.uri(); 207 let (_, code_challenge) = generate_pkce(); 208 let par_body: Value = http_client 209 .post(format!("{}/oauth/par", url)) 210 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri), 211 ("code_challenge", &code_challenge), ("code_challenge_method", "S256")]) 212 .send().await.unwrap().json().await.unwrap(); 213 let request_uri = par_body["request_uri"].as_str().unwrap(); 214 let auth_res = http_client 215 .post(format!("{}/oauth/authorize", url)) 216 .header("Accept", "application/json") 217 .form(&[("request_uri", request_uri), ("username", &handle), ("password", "wrong-password"), ("remember_device", "false")]) 218 .send().await.unwrap(); 219 assert_eq!(auth_res.status(), StatusCode::FORBIDDEN); 220 let error_body: Value = auth_res.json().await.unwrap(); 221 assert_eq!(error_body["error"], "access_denied"); 222 let unsupported = http_client 223 .post(format!("{}/oauth/token", url)) 224 .form(&[("grant_type", "client_credentials"), ("client_id", "https://example.com")]) 225 .send().await.unwrap(); 226 assert_eq!(unsupported.status(), StatusCode::BAD_REQUEST); 227 let body: Value = unsupported.json().await.unwrap(); 228 assert_eq!(body["error"], "unsupported_grant_type"); 229 let invalid_refresh = http_client 230 .post(format!("{}/oauth/token", url)) 231 .form(&[("grant_type", "refresh_token"), ("refresh_token", "invalid-token"), ("client_id", "https://example.com")]) 232 .send().await.unwrap(); 233 assert_eq!(invalid_refresh.status(), StatusCode::BAD_REQUEST); 234 let body: Value = invalid_refresh.json().await.unwrap(); 235 assert_eq!(body["error"], "invalid_grant"); 236 let invalid_introspect = http_client 237 .post(format!("{}/oauth/introspect", url)) 238 .form(&[("token", "invalid.token.here")]) 239 .send().await.unwrap(); 240 assert_eq!(invalid_introspect.status(), StatusCode::OK); 241 let body: Value = invalid_introspect.json().await.unwrap(); 242 assert_eq!(body["active"], false); 243 let expired_res = http_client 244 .get(format!("{}/oauth/authorize", url)) 245 .header("Accept", "application/json") 246 .query(&[("request_uri", "urn:ietf:params:oauth:request_uri:expired")]) 247 .send().await.unwrap(); 248 assert_eq!(expired_res.status(), StatusCode::BAD_REQUEST); 249} 250 251#[tokio::test] 252async fn test_oauth_2fa_flow() { 253 let url = base_url().await; 254 let http_client = client(); 255 let ts = Utc::now().timestamp_millis(); 256 let handle = format!("2fa-test-{}", ts); 257 let email = format!("2fa-test-{}@example.com", ts); 258 let password = "2fa-test-password"; 259 let create_res = http_client 260 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 261 .json(&json!({ "handle": handle, "email": email, "password": password })) 262 .send().await.unwrap(); 263 assert_eq!(create_res.status(), StatusCode::OK); 264 let account: Value = create_res.json().await.unwrap(); 265 let user_did = account["did"].as_str().unwrap(); 266 verify_new_account(&http_client, user_did).await; 267 let db_url = get_db_connection_string().await; 268 let pool = sqlx::postgres::PgPoolOptions::new().max_connections(1).connect(&db_url).await.unwrap(); 269 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 270 .bind(user_did).execute(&pool).await.unwrap(); 271 let redirect_uri = "https://example.com/2fa-callback"; 272 let mock_client = setup_mock_client_metadata(redirect_uri).await; 273 let client_id = mock_client.uri(); 274 let (code_verifier, code_challenge) = generate_pkce(); 275 let par_body: Value = http_client 276 .post(format!("{}/oauth/par", url)) 277 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri), 278 ("code_challenge", &code_challenge), ("code_challenge_method", "S256")]) 279 .send().await.unwrap().json().await.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(&[("request_uri", request_uri), ("username", &handle), ("password", password), ("remember_device", "false")]) 285 .send().await.unwrap(); 286 assert!(auth_res.status().is_redirection(), "Should redirect to 2FA page"); 287 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 288 assert!(location.contains("/oauth/authorize/2fa"), "Should redirect to 2FA page, got: {}", location); 289 let twofa_invalid = http_client 290 .post(format!("{}/oauth/authorize/2fa", url)) 291 .form(&[("request_uri", request_uri), ("code", "000000")]) 292 .send().await.unwrap(); 293 assert_eq!(twofa_invalid.status(), StatusCode::OK); 294 let body = twofa_invalid.text().await.unwrap(); 295 assert!(body.contains("Invalid verification code") || body.contains("invalid")); 296 let twofa_code: String = sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1") 297 .bind(request_uri).fetch_one(&pool).await.unwrap(); 298 let twofa_res = auth_client 299 .post(format!("{}/oauth/authorize/2fa", url)) 300 .form(&[("request_uri", request_uri), ("code", &twofa_code)]) 301 .send().await.unwrap(); 302 assert!(twofa_res.status().is_redirection(), "Valid 2FA code should redirect"); 303 let final_location = twofa_res.headers().get("location").unwrap().to_str().unwrap(); 304 assert!(final_location.starts_with(redirect_uri) && final_location.contains("code=")); 305 let auth_code = final_location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 306 let token_res = http_client 307 .post(format!("{}/oauth/token", url)) 308 .form(&[("grant_type", "authorization_code"), ("code", auth_code), ("redirect_uri", redirect_uri), 309 ("code_verifier", &code_verifier), ("client_id", &client_id)]) 310 .send().await.unwrap(); 311 assert_eq!(token_res.status(), StatusCode::OK); 312 let token_body: Value = token_res.json().await.unwrap(); 313 assert_eq!(token_body["sub"], user_did); 314} 315 316#[tokio::test] 317async fn test_oauth_2fa_lockout() { 318 let url = base_url().await; 319 let http_client = client(); 320 let ts = Utc::now().timestamp_millis(); 321 let handle = format!("2fa-lockout-{}", ts); 322 let email = format!("2fa-lockout-{}@example.com", ts); 323 let password = "2fa-test-password"; 324 let create_res = http_client 325 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 326 .json(&json!({ "handle": handle, "email": email, "password": password })) 327 .send().await.unwrap(); 328 let account: Value = create_res.json().await.unwrap(); 329 let user_did = account["did"].as_str().unwrap(); 330 verify_new_account(&http_client, user_did).await; 331 let db_url = get_db_connection_string().await; 332 let pool = sqlx::postgres::PgPoolOptions::new().max_connections(1).connect(&db_url).await.unwrap(); 333 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 334 .bind(user_did).execute(&pool).await.unwrap(); 335 let redirect_uri = "https://example.com/2fa-lockout-callback"; 336 let mock_client = setup_mock_client_metadata(redirect_uri).await; 337 let client_id = mock_client.uri(); 338 let (_, code_challenge) = generate_pkce(); 339 let par_body: Value = http_client 340 .post(format!("{}/oauth/par", url)) 341 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri), 342 ("code_challenge", &code_challenge), ("code_challenge_method", "S256")]) 343 .send().await.unwrap().json().await.unwrap(); 344 let request_uri = par_body["request_uri"].as_str().unwrap(); 345 let auth_client = no_redirect_client(); 346 let auth_res = auth_client 347 .post(format!("{}/oauth/authorize", url)) 348 .form(&[("request_uri", request_uri), ("username", &handle), ("password", password), ("remember_device", "false")]) 349 .send().await.unwrap(); 350 assert!(auth_res.status().is_redirection()); 351 for i in 0..5 { 352 let res = http_client 353 .post(format!("{}/oauth/authorize/2fa", url)) 354 .form(&[("request_uri", request_uri), ("code", "999999")]) 355 .send().await.unwrap(); 356 if i < 4 { 357 assert_eq!(res.status(), StatusCode::OK); 358 } 359 } 360 let lockout_res = http_client 361 .post(format!("{}/oauth/authorize/2fa", url)) 362 .form(&[("request_uri", request_uri), ("code", "999999")]) 363 .send().await.unwrap(); 364 let body = lockout_res.text().await.unwrap(); 365 assert!(body.contains("Too many failed attempts") || body.contains("No 2FA challenge found")); 366} 367 368#[tokio::test] 369async fn test_account_selector_with_2fa() { 370 let url = base_url().await; 371 let http_client = client(); 372 let ts = Utc::now().timestamp_millis(); 373 let handle = format!("selector-2fa-{}", ts); 374 let email = format!("selector-2fa-{}@example.com", ts); 375 let password = "selector-2fa-password"; 376 let create_res = http_client 377 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 378 .json(&json!({ "handle": handle, "email": email, "password": password })) 379 .send().await.unwrap(); 380 let account: Value = create_res.json().await.unwrap(); 381 let user_did = account["did"].as_str().unwrap().to_string(); 382 verify_new_account(&http_client, &user_did).await; 383 let redirect_uri = "https://example.com/selector-2fa-callback"; 384 let mock_client = setup_mock_client_metadata(redirect_uri).await; 385 let client_id = mock_client.uri(); 386 let (code_verifier, code_challenge) = generate_pkce(); 387 let par_body: Value = http_client 388 .post(format!("{}/oauth/par", url)) 389 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri), 390 ("code_challenge", &code_challenge), ("code_challenge_method", "S256")]) 391 .send().await.unwrap().json().await.unwrap(); 392 let request_uri = par_body["request_uri"].as_str().unwrap(); 393 let auth_client = no_redirect_client(); 394 let auth_res = auth_client 395 .post(format!("{}/oauth/authorize", url)) 396 .form(&[("request_uri", request_uri), ("username", &handle), ("password", password), ("remember_device", "true")]) 397 .send().await.unwrap(); 398 assert!(auth_res.status().is_redirection()); 399 let device_cookie = auth_res.headers().get("set-cookie") 400 .and_then(|v| v.to_str().ok()) 401 .map(|s| s.split(';').next().unwrap_or("").to_string()) 402 .expect("Should have device cookie"); 403 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 404 assert!(location.contains("code=")); 405 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 406 let _ = http_client 407 .post(format!("{}/oauth/token", url)) 408 .form(&[("grant_type", "authorization_code"), ("code", code), ("redirect_uri", redirect_uri), 409 ("code_verifier", &code_verifier), ("client_id", &client_id)]) 410 .send().await.unwrap().json::<Value>().await.unwrap(); 411 let db_url = get_db_connection_string().await; 412 let pool = sqlx::postgres::PgPoolOptions::new().max_connections(1).connect(&db_url).await.unwrap(); 413 sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1") 414 .bind(&user_did).execute(&pool).await.unwrap(); 415 let (code_verifier2, code_challenge2) = generate_pkce(); 416 let par_body2: Value = http_client 417 .post(format!("{}/oauth/par", url)) 418 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri), 419 ("code_challenge", &code_challenge2), ("code_challenge_method", "S256")]) 420 .send().await.unwrap().json().await.unwrap(); 421 let request_uri2 = par_body2["request_uri"].as_str().unwrap(); 422 let select_res = auth_client 423 .post(format!("{}/oauth/authorize/select", url)) 424 .header("cookie", &device_cookie) 425 .form(&[("request_uri", request_uri2), ("did", &user_did)]) 426 .send().await.unwrap(); 427 assert!(select_res.status().is_redirection()); 428 let select_location = select_res.headers().get("location").unwrap().to_str().unwrap(); 429 assert!(select_location.contains("/oauth/authorize/2fa"), "Should redirect to 2FA page"); 430 let twofa_code: String = sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1") 431 .bind(request_uri2).fetch_one(&pool).await.unwrap(); 432 let twofa_res = auth_client 433 .post(format!("{}/oauth/authorize/2fa", url)) 434 .header("cookie", &device_cookie) 435 .form(&[("request_uri", request_uri2), ("code", &twofa_code)]) 436 .send().await.unwrap(); 437 assert!(twofa_res.status().is_redirection()); 438 let final_location = twofa_res.headers().get("location").unwrap().to_str().unwrap(); 439 assert!(final_location.starts_with(redirect_uri) && final_location.contains("code=")); 440 let final_code = final_location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 441 let token_res = http_client 442 .post(format!("{}/oauth/token", url)) 443 .form(&[("grant_type", "authorization_code"), ("code", final_code), ("redirect_uri", redirect_uri), 444 ("code_verifier", &code_verifier2), ("client_id", &client_id)]) 445 .send().await.unwrap(); 446 assert_eq!(token_res.status(), StatusCode::OK); 447 let final_token: Value = token_res.json().await.unwrap(); 448 assert_eq!(final_token["sub"], user_did); 449} 450 451#[tokio::test] 452async fn test_oauth_state_encoding() { 453 let url = base_url().await; 454 let http_client = client(); 455 let ts = Utc::now().timestamp_millis(); 456 let handle = format!("state-special-{}", ts); 457 let email = format!("state-special-{}@example.com", ts); 458 let password = "state-special-password"; 459 let create_res = http_client 460 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 461 .json(&json!({ "handle": handle, "email": email, "password": password })) 462 .send().await.unwrap(); 463 let account: Value = create_res.json().await.unwrap(); 464 verify_new_account(&http_client, account["did"].as_str().unwrap()).await; 465 let redirect_uri = "https://example.com/state-special-callback"; 466 let mock_client = setup_mock_client_metadata(redirect_uri).await; 467 let client_id = mock_client.uri(); 468 let (_, code_challenge) = generate_pkce(); 469 let special_state = "state=with&special=chars&plus+more"; 470 let par_body: Value = http_client 471 .post(format!("{}/oauth/par", url)) 472 .form(&[("response_type", "code"), ("client_id", &client_id), ("redirect_uri", redirect_uri), 473 ("code_challenge", &code_challenge), ("code_challenge_method", "S256"), ("state", special_state)]) 474 .send().await.unwrap().json().await.unwrap(); 475 let request_uri = par_body["request_uri"].as_str().unwrap(); 476 let auth_client = no_redirect_client(); 477 let auth_res = auth_client 478 .post(format!("{}/oauth/authorize", url)) 479 .form(&[("request_uri", request_uri), ("username", &handle), ("password", password), ("remember_device", "false")]) 480 .send().await.unwrap(); 481 assert!(auth_res.status().is_redirection()); 482 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 483 assert!(location.contains("state=")); 484 let encoded_state = urlencoding::encode(special_state); 485 assert!(location.contains(&format!("state={}", encoded_state)), "State should be URL-encoded. Got: {}", location); 486}