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