this repo has no description
1#![allow(unused_imports)] 2mod common; 3mod helpers; 4use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 5use chrono::Utc; 6use common::{base_url, client}; 7use helpers::verify_new_account; 8use reqwest::StatusCode; 9use serde_json::{Value, json}; 10use sha2::{Digest, Sha256}; 11use tranquil_pds::oauth::dpop::{DPoPJwk, DPoPVerifier, compute_jwk_thumbprint}; 12use wiremock::matchers::{method, path}; 13use wiremock::{Mock, MockServer, ResponseTemplate}; 14 15fn generate_pkce() -> (String, String) { 16 let verifier_bytes: [u8; 32] = rand::random(); 17 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes); 18 let mut hasher = Sha256::new(); 19 hasher.update(code_verifier.as_bytes()); 20 let code_challenge = URL_SAFE_NO_PAD.encode(&hasher.finalize()); 21 (code_verifier, code_challenge) 22} 23 24async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer { 25 let mock_server = MockServer::start().await; 26 let metadata = json!({ 27 "client_id": mock_server.uri(), 28 "client_name": "Security Test Client", 29 "redirect_uris": [redirect_uri], 30 "grant_types": ["authorization_code", "refresh_token"], 31 "response_types": ["code"], 32 "token_endpoint_auth_method": "none", 33 "dpop_bound_access_tokens": false 34 }); 35 Mock::given(method("GET")) 36 .and(path("/")) 37 .respond_with(ResponseTemplate::new(200).set_body_json(metadata)) 38 .mount(&mock_server) 39 .await; 40 mock_server 41} 42 43async fn get_oauth_tokens(http_client: &reqwest::Client, url: &str) -> (String, String, String) { 44 let ts = Utc::now().timestamp_millis(); 45 let handle = format!("sec-test-{}", ts); 46 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 47 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Security123!" })) 48 .send().await.unwrap(); 49 let account: Value = create_res.json().await.unwrap(); 50 let did = account["did"].as_str().unwrap(); 51 verify_new_account(http_client, did).await; 52 let redirect_uri = "https://example.com/sec-callback"; 53 let mock_client = setup_mock_client_metadata(redirect_uri).await; 54 let client_id = mock_client.uri(); 55 let (code_verifier, code_challenge) = generate_pkce(); 56 let par_body: Value = http_client 57 .post(format!("{}/oauth/par", url)) 58 .form(&[ 59 ("response_type", "code"), 60 ("client_id", &client_id), 61 ("redirect_uri", redirect_uri), 62 ("code_challenge", &code_challenge), 63 ("code_challenge_method", "S256"), 64 ]) 65 .send() 66 .await 67 .unwrap() 68 .json() 69 .await 70 .unwrap(); 71 let request_uri = par_body["request_uri"].as_str().unwrap(); 72 let auth_res = http_client.post(format!("{}/oauth/authorize", url)) 73 .header("Content-Type", "application/json") 74 .header("Accept", "application/json") 75 .json(&json!({"request_uri": request_uri, "username": &handle, "password": "Security123!", "remember_device": false})) 76 .send().await.unwrap(); 77 let auth_body: Value = auth_res.json().await.unwrap(); 78 let mut location = auth_body["redirect_uri"].as_str().unwrap().to_string(); 79 if location.contains("/oauth/consent") { 80 let consent_res = http_client.post(format!("{}/oauth/authorize/consent", url)) 81 .header("Content-Type", "application/json") 82 .json(&json!({"request_uri": request_uri, "approved_scopes": ["atproto"], "remember": false})) 83 .send().await.unwrap(); 84 let consent_body: Value = consent_res.json().await.unwrap(); 85 location = consent_body["redirect_uri"].as_str().unwrap().to_string(); 86 } 87 let code = location 88 .split("code=") 89 .nth(1) 90 .unwrap() 91 .split('&') 92 .next() 93 .unwrap(); 94 let token_body: Value = http_client 95 .post(format!("{}/oauth/token", url)) 96 .form(&[ 97 ("grant_type", "authorization_code"), 98 ("code", code), 99 ("redirect_uri", redirect_uri), 100 ("code_verifier", &code_verifier), 101 ("client_id", &client_id), 102 ]) 103 .send() 104 .await 105 .unwrap() 106 .json() 107 .await 108 .unwrap(); 109 ( 110 token_body["access_token"].as_str().unwrap().to_string(), 111 token_body["refresh_token"].as_str().unwrap().to_string(), 112 client_id, 113 ) 114} 115 116#[tokio::test] 117async fn test_token_tampering_attacks() { 118 let url = base_url().await; 119 let http_client = client(); 120 let (access_token, _, _) = get_oauth_tokens(&http_client, url).await; 121 let parts: Vec<&str> = access_token.split('.').collect(); 122 assert_eq!(parts.len(), 3); 123 let forged_sig = URL_SAFE_NO_PAD.encode(&[0u8; 32]); 124 let forged_token = format!("{}.{}.{}", parts[0], parts[1], forged_sig); 125 assert_eq!( 126 http_client 127 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 128 .bearer_auth(&forged_token) 129 .send() 130 .await 131 .unwrap() 132 .status(), 133 StatusCode::UNAUTHORIZED, 134 "Forged signature should be rejected" 135 ); 136 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap(); 137 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap(); 138 payload["sub"] = json!("did:plc:attacker"); 139 let modified_payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 140 let modified_token = format!("{}.{}.{}", parts[0], modified_payload, parts[2]); 141 assert_eq!( 142 http_client 143 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 144 .bearer_auth(&modified_token) 145 .send() 146 .await 147 .unwrap() 148 .status(), 149 StatusCode::UNAUTHORIZED, 150 "Modified payload should be rejected" 151 ); 152 let none_header = json!({ "alg": "none", "typ": "at+jwt" }); 153 let none_payload = json!({ "iss": "https://test.pds", "sub": "did:plc:attacker", "aud": "https://test.pds", 154 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, "jti": "fake", "scope": "atproto" }); 155 let none_token = format!( 156 "{}.{}.", 157 URL_SAFE_NO_PAD.encode(serde_json::to_string(&none_header).unwrap()), 158 URL_SAFE_NO_PAD.encode(serde_json::to_string(&none_payload).unwrap()) 159 ); 160 assert_eq!( 161 http_client 162 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 163 .bearer_auth(&none_token) 164 .send() 165 .await 166 .unwrap() 167 .status(), 168 StatusCode::UNAUTHORIZED, 169 "alg=none should be rejected" 170 ); 171 let rs256_header = json!({ "alg": "RS256", "typ": "at+jwt" }); 172 let rs256_token = format!( 173 "{}.{}.{}", 174 URL_SAFE_NO_PAD.encode(serde_json::to_string(&rs256_header).unwrap()), 175 URL_SAFE_NO_PAD.encode(serde_json::to_string(&none_payload).unwrap()), 176 URL_SAFE_NO_PAD.encode(&[1u8; 64]) 177 ); 178 assert_eq!( 179 http_client 180 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 181 .bearer_auth(&rs256_token) 182 .send() 183 .await 184 .unwrap() 185 .status(), 186 StatusCode::UNAUTHORIZED, 187 "Algorithm substitution should be rejected" 188 ); 189 let expired_payload = json!({ "iss": "https://test.pds", "sub": "did:plc:test", "aud": "https://test.pds", 190 "iat": Utc::now().timestamp() - 7200, "exp": Utc::now().timestamp() - 3600, "jti": "expired" }); 191 let expired_token = format!( 192 "{}.{}.{}", 193 URL_SAFE_NO_PAD 194 .encode(serde_json::to_string(&json!({"alg":"HS256","typ":"at+jwt"})).unwrap()), 195 URL_SAFE_NO_PAD.encode(serde_json::to_string(&expired_payload).unwrap()), 196 URL_SAFE_NO_PAD.encode(&[1u8; 32]) 197 ); 198 assert_eq!( 199 http_client 200 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 201 .bearer_auth(&expired_token) 202 .send() 203 .await 204 .unwrap() 205 .status(), 206 StatusCode::UNAUTHORIZED, 207 "Expired token should be rejected" 208 ); 209} 210 211#[tokio::test] 212async fn test_pkce_security() { 213 let url = base_url().await; 214 let http_client = client(); 215 let redirect_uri = "https://example.com/pkce-callback"; 216 let mock_client = setup_mock_client_metadata(redirect_uri).await; 217 let client_id = mock_client.uri(); 218 let res = http_client 219 .post(format!("{}/oauth/par", url)) 220 .form(&[ 221 ("response_type", "code"), 222 ("client_id", &client_id), 223 ("redirect_uri", redirect_uri), 224 ("code_challenge", "plain-text-challenge"), 225 ("code_challenge_method", "plain"), 226 ]) 227 .send() 228 .await 229 .unwrap(); 230 assert_eq!( 231 res.status(), 232 StatusCode::BAD_REQUEST, 233 "PKCE plain method should be rejected" 234 ); 235 let body: Value = res.json().await.unwrap(); 236 assert!( 237 body["error_description"] 238 .as_str() 239 .unwrap() 240 .to_lowercase() 241 .contains("s256") 242 ); 243 let res = http_client 244 .post(format!("{}/oauth/par", url)) 245 .form(&[ 246 ("response_type", "code"), 247 ("client_id", &client_id), 248 ("redirect_uri", redirect_uri), 249 ]) 250 .send() 251 .await 252 .unwrap(); 253 assert_eq!( 254 res.status(), 255 StatusCode::BAD_REQUEST, 256 "Missing PKCE challenge should be rejected" 257 ); 258 let ts = Utc::now().timestamp_millis(); 259 let handle = format!("pkce-attack-{}", ts); 260 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 261 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Pkce123pass!" })) 262 .send().await.unwrap(); 263 let account: Value = create_res.json().await.unwrap(); 264 verify_new_account(&http_client, account["did"].as_str().unwrap()).await; 265 let (_, code_challenge) = generate_pkce(); 266 let (attacker_verifier, _) = generate_pkce(); 267 let par_body: Value = http_client 268 .post(format!("{}/oauth/par", url)) 269 .form(&[ 270 ("response_type", "code"), 271 ("client_id", &client_id), 272 ("redirect_uri", redirect_uri), 273 ("code_challenge", &code_challenge), 274 ("code_challenge_method", "S256"), 275 ]) 276 .send() 277 .await 278 .unwrap() 279 .json() 280 .await 281 .unwrap(); 282 let request_uri = par_body["request_uri"].as_str().unwrap(); 283 let auth_res = http_client.post(format!("{}/oauth/authorize", url)) 284 .header("Content-Type", "application/json") 285 .header("Accept", "application/json") 286 .json(&json!({"request_uri": request_uri, "username": &handle, "password": "Pkce123pass!", "remember_device": false})) 287 .send().await.unwrap(); 288 assert_eq!(auth_res.status(), StatusCode::OK); 289 let auth_body: Value = auth_res.json().await.unwrap(); 290 let mut location = auth_body["redirect_uri"].as_str().unwrap().to_string(); 291 if location.contains("/oauth/consent") { 292 let consent_res = http_client.post(format!("{}/oauth/authorize/consent", url)) 293 .header("Content-Type", "application/json") 294 .json(&json!({"request_uri": request_uri, "approved_scopes": ["atproto"], "remember": false})) 295 .send().await.unwrap(); 296 let consent_body: Value = consent_res.json().await.unwrap(); 297 location = consent_body["redirect_uri"].as_str().unwrap().to_string(); 298 } 299 let code = location 300 .split("code=") 301 .nth(1) 302 .unwrap() 303 .split('&') 304 .next() 305 .unwrap(); 306 let token_res = http_client 307 .post(format!("{}/oauth/token", url)) 308 .form(&[ 309 ("grant_type", "authorization_code"), 310 ("code", code), 311 ("redirect_uri", redirect_uri), 312 ("code_verifier", &attacker_verifier), 313 ("client_id", &client_id), 314 ]) 315 .send() 316 .await 317 .unwrap(); 318 assert_eq!( 319 token_res.status(), 320 StatusCode::BAD_REQUEST, 321 "Wrong PKCE verifier should be rejected" 322 ); 323} 324 325#[tokio::test] 326async fn test_replay_attacks() { 327 let url = base_url().await; 328 let http_client = client(); 329 let ts = Utc::now().timestamp_millis(); 330 let handle = format!("replay-{}", ts); 331 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 332 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Replay123pass!" })) 333 .send().await.unwrap(); 334 let account: Value = create_res.json().await.unwrap(); 335 verify_new_account(&http_client, account["did"].as_str().unwrap()).await; 336 let redirect_uri = "https://example.com/replay-callback"; 337 let mock_client = setup_mock_client_metadata(redirect_uri).await; 338 let client_id = mock_client.uri(); 339 let (code_verifier, code_challenge) = generate_pkce(); 340 let par_body: Value = http_client 341 .post(format!("{}/oauth/par", url)) 342 .form(&[ 343 ("response_type", "code"), 344 ("client_id", &client_id), 345 ("redirect_uri", redirect_uri), 346 ("code_challenge", &code_challenge), 347 ("code_challenge_method", "S256"), 348 ]) 349 .send() 350 .await 351 .unwrap() 352 .json() 353 .await 354 .unwrap(); 355 let request_uri = par_body["request_uri"].as_str().unwrap(); 356 let auth_res = http_client.post(format!("{}/oauth/authorize", url)) 357 .header("Content-Type", "application/json") 358 .header("Accept", "application/json") 359 .json(&json!({"request_uri": request_uri, "username": &handle, "password": "Replay123pass!", "remember_device": false})) 360 .send().await.unwrap(); 361 assert_eq!(auth_res.status(), StatusCode::OK); 362 let auth_body: Value = auth_res.json().await.unwrap(); 363 let mut location = auth_body["redirect_uri"].as_str().unwrap().to_string(); 364 if location.contains("/oauth/consent") { 365 let consent_res = http_client.post(format!("{}/oauth/authorize/consent", url)) 366 .header("Content-Type", "application/json") 367 .json(&json!({"request_uri": request_uri, "approved_scopes": ["atproto"], "remember": false})) 368 .send().await.unwrap(); 369 let consent_body: Value = consent_res.json().await.unwrap(); 370 location = consent_body["redirect_uri"].as_str().unwrap().to_string(); 371 } 372 let code = location 373 .split("code=") 374 .nth(1) 375 .unwrap() 376 .split('&') 377 .next() 378 .unwrap() 379 .to_string(); 380 let first = http_client 381 .post(format!("{}/oauth/token", url)) 382 .form(&[ 383 ("grant_type", "authorization_code"), 384 ("code", &code), 385 ("redirect_uri", redirect_uri), 386 ("code_verifier", &code_verifier), 387 ("client_id", &client_id), 388 ]) 389 .send() 390 .await 391 .unwrap(); 392 assert_eq!(first.status(), StatusCode::OK, "First use should succeed"); 393 let first_body: Value = first.json().await.unwrap(); 394 let replay = 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 assert_eq!( 407 replay.status(), 408 StatusCode::BAD_REQUEST, 409 "Auth code replay should fail" 410 ); 411 let stolen_rt = first_body["refresh_token"].as_str().unwrap().to_string(); 412 let first_refresh: Value = http_client 413 .post(format!("{}/oauth/token", url)) 414 .form(&[ 415 ("grant_type", "refresh_token"), 416 ("refresh_token", &stolen_rt), 417 ("client_id", &client_id), 418 ]) 419 .send() 420 .await 421 .unwrap() 422 .json() 423 .await 424 .unwrap(); 425 assert!( 426 first_refresh["access_token"].is_string(), 427 "First refresh should succeed" 428 ); 429 let new_rt = first_refresh["refresh_token"].as_str().unwrap(); 430 let rt_replay = http_client 431 .post(format!("{}/oauth/token", url)) 432 .form(&[ 433 ("grant_type", "refresh_token"), 434 ("refresh_token", &stolen_rt), 435 ("client_id", &client_id), 436 ]) 437 .send() 438 .await 439 .unwrap(); 440 assert_eq!( 441 rt_replay.status(), 442 StatusCode::BAD_REQUEST, 443 "Refresh token replay should fail" 444 ); 445 let body: Value = rt_replay.json().await.unwrap(); 446 assert!( 447 body["error_description"] 448 .as_str() 449 .unwrap() 450 .to_lowercase() 451 .contains("reuse") 452 ); 453 let family_revoked = http_client 454 .post(format!("{}/oauth/token", url)) 455 .form(&[ 456 ("grant_type", "refresh_token"), 457 ("refresh_token", new_rt), 458 ("client_id", &client_id), 459 ]) 460 .send() 461 .await 462 .unwrap(); 463 assert_eq!( 464 family_revoked.status(), 465 StatusCode::BAD_REQUEST, 466 "Token family should be revoked" 467 ); 468} 469 470#[tokio::test] 471async fn test_oauth_security_boundaries() { 472 let url = base_url().await; 473 let http_client = client(); 474 let registered_redirect = "https://legitimate-app.com/callback"; 475 let mock_client = setup_mock_client_metadata(registered_redirect).await; 476 let client_id = mock_client.uri(); 477 let (_, code_challenge) = generate_pkce(); 478 let res = http_client 479 .post(format!("{}/oauth/par", url)) 480 .form(&[ 481 ("response_type", "code"), 482 ("client_id", &client_id), 483 ("redirect_uri", "https://attacker.com/steal"), 484 ("code_challenge", &code_challenge), 485 ("code_challenge_method", "S256"), 486 ]) 487 .send() 488 .await 489 .unwrap(); 490 assert_eq!( 491 res.status(), 492 StatusCode::BAD_REQUEST, 493 "Unregistered redirect_uri should be rejected" 494 ); 495 let ts = Utc::now().timestamp_millis(); 496 let handle = format!("deact-{}", ts); 497 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 498 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Deact123pass!" })) 499 .send().await.unwrap(); 500 let account: Value = create_res.json().await.unwrap(); 501 let access_jwt = verify_new_account(&http_client, account["did"].as_str().unwrap()).await; 502 http_client 503 .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url)) 504 .bearer_auth(&access_jwt) 505 .json(&json!({})) 506 .send() 507 .await 508 .unwrap(); 509 let deact_par: Value = http_client 510 .post(format!("{}/oauth/par", url)) 511 .form(&[ 512 ("response_type", "code"), 513 ("client_id", &client_id), 514 ("redirect_uri", registered_redirect), 515 ("code_challenge", &code_challenge), 516 ("code_challenge_method", "S256"), 517 ]) 518 .send() 519 .await 520 .unwrap() 521 .json() 522 .await 523 .unwrap(); 524 let auth_res = http_client.post(format!("{}/oauth/authorize", url)) 525 .header("Content-Type", "application/json") 526 .header("Accept", "application/json") 527 .json(&json!({"request_uri": deact_par["request_uri"].as_str().unwrap(), "username": &handle, "password": "Deact123pass!", "remember_device": false})) 528 .send().await.unwrap(); 529 assert_eq!( 530 auth_res.status(), 531 StatusCode::FORBIDDEN, 532 "Deactivated account should be blocked" 533 ); 534 let redirect_uri_a = "https://app-a.com/callback"; 535 let mock_a = setup_mock_client_metadata(redirect_uri_a).await; 536 let client_id_a = mock_a.uri(); 537 let mock_b = setup_mock_client_metadata("https://app-b.com/callback").await; 538 let client_id_b = mock_b.uri(); 539 let ts2 = Utc::now().timestamp_millis(); 540 let handle2 = format!("cross-{}", ts2); 541 let create_res2 = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 542 .json(&json!({ "handle": handle2, "email": format!("{}@example.com", handle2), "password": "Cross123pass!" })) 543 .send().await.unwrap(); 544 let account2: Value = create_res2.json().await.unwrap(); 545 verify_new_account(&http_client, account2["did"].as_str().unwrap()).await; 546 let (code_verifier2, code_challenge2) = generate_pkce(); 547 let par_a: Value = http_client 548 .post(format!("{}/oauth/par", url)) 549 .form(&[ 550 ("response_type", "code"), 551 ("client_id", &client_id_a), 552 ("redirect_uri", redirect_uri_a), 553 ("code_challenge", &code_challenge2), 554 ("code_challenge_method", "S256"), 555 ]) 556 .send() 557 .await 558 .unwrap() 559 .json() 560 .await 561 .unwrap(); 562 let request_uri_a = par_a["request_uri"].as_str().unwrap(); 563 let auth_a = http_client.post(format!("{}/oauth/authorize", url)) 564 .header("Content-Type", "application/json") 565 .header("Accept", "application/json") 566 .json(&json!({"request_uri": request_uri_a, "username": &handle2, "password": "Cross123pass!", "remember_device": false})) 567 .send().await.unwrap(); 568 assert_eq!(auth_a.status(), StatusCode::OK); 569 let auth_body_a: Value = auth_a.json().await.unwrap(); 570 let mut loc_a = auth_body_a["redirect_uri"].as_str().unwrap().to_string(); 571 if loc_a.contains("/oauth/consent") { 572 let consent_res = http_client.post(format!("{}/oauth/authorize/consent", url)) 573 .header("Content-Type", "application/json") 574 .json(&json!({"request_uri": request_uri_a, "approved_scopes": ["atproto"], "remember": false})) 575 .send().await.unwrap(); 576 let consent_body: Value = consent_res.json().await.unwrap(); 577 loc_a = consent_body["redirect_uri"].as_str().unwrap().to_string(); 578 } 579 let code_a = loc_a 580 .split("code=") 581 .nth(1) 582 .unwrap() 583 .split('&') 584 .next() 585 .unwrap(); 586 let cross_client = http_client 587 .post(format!("{}/oauth/token", url)) 588 .form(&[ 589 ("grant_type", "authorization_code"), 590 ("code", code_a), 591 ("redirect_uri", redirect_uri_a), 592 ("code_verifier", &code_verifier2), 593 ("client_id", &client_id_b), 594 ]) 595 .send() 596 .await 597 .unwrap(); 598 assert_eq!( 599 cross_client.status(), 600 StatusCode::BAD_REQUEST, 601 "Cross-client code exchange must be rejected" 602 ); 603} 604 605#[tokio::test] 606async fn test_malformed_tokens_and_headers() { 607 let url = base_url().await; 608 let http_client = client(); 609 let malformed = vec![ 610 "", 611 "not-a-token", 612 "one.two", 613 "one.two.three.four", 614 "....", 615 "eyJhbGciOiJIUzI1NiJ9", 616 "eyJhbGciOiJIUzI1NiJ9.", 617 "eyJhbGciOiJIUzI1NiJ9..", 618 ".eyJzdWIiOiJ0ZXN0In0.", 619 "!!invalid!!.eyJ9.sig", 620 ]; 621 for token in &malformed { 622 assert_eq!( 623 http_client 624 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 625 .bearer_auth(token) 626 .send() 627 .await 628 .unwrap() 629 .status(), 630 StatusCode::UNAUTHORIZED 631 ); 632 } 633 let wrong_types = vec!["JWT", "jwt", "at+JWT", ""]; 634 for typ in wrong_types { 635 let header = json!({ "alg": "HS256", "typ": typ }); 636 let payload = json!({ "iss": "x", "sub": "did:plc:x", "aud": "x", "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, "jti": "x" }); 637 let token = format!( 638 "{}.{}.{}", 639 URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()), 640 URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()), 641 URL_SAFE_NO_PAD.encode(&[1u8; 32]) 642 ); 643 assert_eq!( 644 http_client 645 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 646 .bearer_auth(&token) 647 .send() 648 .await 649 .unwrap() 650 .status(), 651 StatusCode::UNAUTHORIZED, 652 "typ='{}' should be rejected", 653 typ 654 ); 655 } 656 let (access_token, _, _) = get_oauth_tokens(&http_client, url).await; 657 let invalid_formats = vec![ 658 format!("Basic {}", access_token), 659 format!("Digest {}", access_token), 660 access_token.clone(), 661 format!("Bearer{}", access_token), 662 ]; 663 for auth in &invalid_formats { 664 assert_eq!( 665 http_client 666 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 667 .header("Authorization", auth) 668 .send() 669 .await 670 .unwrap() 671 .status(), 672 StatusCode::UNAUTHORIZED 673 ); 674 } 675 assert_eq!( 676 http_client 677 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 678 .send() 679 .await 680 .unwrap() 681 .status(), 682 StatusCode::UNAUTHORIZED 683 ); 684 assert_eq!( 685 http_client 686 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 687 .header("Authorization", "") 688 .send() 689 .await 690 .unwrap() 691 .status(), 692 StatusCode::UNAUTHORIZED 693 ); 694 let grants = vec![ 695 "client_credentials", 696 "password", 697 "implicit", 698 "", 699 "AUTHORIZATION_CODE", 700 ]; 701 for grant in grants { 702 assert_eq!( 703 http_client 704 .post(format!("{}/oauth/token", url)) 705 .form(&[("grant_type", grant), ("client_id", "https://example.com")]) 706 .send() 707 .await 708 .unwrap() 709 .status(), 710 StatusCode::BAD_REQUEST, 711 "Grant '{}' should be rejected", 712 grant 713 ); 714 } 715} 716 717#[tokio::test] 718async fn test_token_revocation() { 719 let url = base_url().await; 720 let http_client = client(); 721 let (access_token, refresh_token, _) = get_oauth_tokens(&http_client, url).await; 722 assert_eq!( 723 http_client 724 .post(format!("{}/oauth/revoke", url)) 725 .form(&[("token", &refresh_token)]) 726 .send() 727 .await 728 .unwrap() 729 .status(), 730 StatusCode::OK 731 ); 732 let introspect: Value = http_client 733 .post(format!("{}/oauth/introspect", url)) 734 .form(&[("token", &access_token)]) 735 .send() 736 .await 737 .unwrap() 738 .json() 739 .await 740 .unwrap(); 741 assert_eq!( 742 introspect["active"], false, 743 "Revoked token should be inactive" 744 ); 745} 746 747fn create_dpop_proof( 748 method: &str, 749 uri: &str, 750 _nonce: Option<&str>, 751 ath: Option<&str>, 752 iat_offset: i64, 753) -> String { 754 use p256::ecdsa::{Signature, SigningKey, signature::Signer}; 755 use p256::elliptic_curve::sec1::ToEncodedPoint; 756 let signing_key = SigningKey::random(&mut rand::thread_rng()); 757 let point = signing_key.verifying_key().to_encoded_point(false); 758 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap()); 759 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap()); 760 let header = json!({ "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": x, "y": y } }); 761 let mut payload = json!({ "jti": format!("unique-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)), 762 "htm": method, "htu": uri, "iat": Utc::now().timestamp() + iat_offset }); 763 if let Some(a) = ath { 764 payload["ath"] = json!(a); 765 } 766 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 767 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 768 let signing_input = format!("{}.{}", header_b64, payload_b64); 769 let signature: Signature = signing_key.sign(signing_input.as_bytes()); 770 format!( 771 "{}.{}", 772 signing_input, 773 URL_SAFE_NO_PAD.encode(signature.to_bytes()) 774 ) 775} 776 777#[test] 778fn test_dpop_nonce_security() { 779 let secret1 = b"test-dpop-secret-32-bytes-long!!"; 780 let secret2 = b"different-secret-32-bytes-long!!"; 781 let v1 = DPoPVerifier::new(secret1); 782 let v2 = DPoPVerifier::new(secret2); 783 let nonce = v1.generate_nonce(); 784 assert!(!nonce.is_empty()); 785 assert!(v1.validate_nonce(&nonce).is_ok(), "Valid nonce should pass"); 786 assert!( 787 v2.validate_nonce(&nonce).is_err(), 788 "Nonce from different secret should fail" 789 ); 790 let nonce_bytes = URL_SAFE_NO_PAD.decode(&nonce).unwrap(); 791 let mut tampered = nonce_bytes.clone(); 792 if !tampered.is_empty() { 793 tampered[0] ^= 0xFF; 794 } 795 assert!( 796 v1.validate_nonce(&URL_SAFE_NO_PAD.encode(&tampered)) 797 .is_err(), 798 "Tampered nonce should fail" 799 ); 800 assert!(v1.validate_nonce("invalid").is_err()); 801 assert!(v1.validate_nonce("").is_err()); 802 assert!(v1.validate_nonce("!!!not-base64!!!").is_err()); 803} 804 805#[test] 806fn test_dpop_proof_validation() { 807 let secret = b"test-dpop-secret-32-bytes-long!!"; 808 let verifier = DPoPVerifier::new(secret); 809 assert!( 810 verifier 811 .verify_proof("not.enough", "POST", "https://example.com", None) 812 .is_err() 813 ); 814 assert!( 815 verifier 816 .verify_proof("invalid", "POST", "https://example.com", None) 817 .is_err() 818 ); 819 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0); 820 assert!( 821 verifier 822 .verify_proof(&proof, "GET", "https://example.com/token", None) 823 .is_err(), 824 "Method mismatch" 825 ); 826 assert!( 827 verifier 828 .verify_proof(&proof, "POST", "https://other.com/token", None) 829 .is_err(), 830 "URI mismatch" 831 ); 832 assert!( 833 verifier 834 .verify_proof(&proof, "POST", "https://example.com/token?foo=bar", None) 835 .is_ok(), 836 "Query params should be ignored" 837 ); 838 let old_proof = create_dpop_proof("POST", "https://example.com/token", None, None, -600); 839 assert!( 840 verifier 841 .verify_proof(&old_proof, "POST", "https://example.com/token", None) 842 .is_err(), 843 "iat too old" 844 ); 845 let future_proof = create_dpop_proof("POST", "https://example.com/token", None, None, 600); 846 assert!( 847 verifier 848 .verify_proof(&future_proof, "POST", "https://example.com/token", None) 849 .is_err(), 850 "iat in future" 851 ); 852 let ath_proof = create_dpop_proof( 853 "GET", 854 "https://example.com/resource", 855 None, 856 Some("wrong"), 857 0, 858 ); 859 assert!( 860 verifier 861 .verify_proof( 862 &ath_proof, 863 "GET", 864 "https://example.com/resource", 865 Some("correct") 866 ) 867 .is_err(), 868 "ath mismatch" 869 ); 870 let no_ath_proof = create_dpop_proof("GET", "https://example.com/resource", None, None, 0); 871 assert!( 872 verifier 873 .verify_proof( 874 &no_ath_proof, 875 "GET", 876 "https://example.com/resource", 877 Some("expected") 878 ) 879 .is_err(), 880 "Missing ath" 881 ); 882} 883 884#[test] 885fn test_dpop_proof_signature_attacks() { 886 use p256::ecdsa::{Signature, SigningKey, signature::Signer}; 887 use p256::elliptic_curve::sec1::ToEncodedPoint; 888 let secret = b"test-dpop-secret-32-bytes-long!!"; 889 let verifier = DPoPVerifier::new(secret); 890 let signing_key = SigningKey::random(&mut rand::thread_rng()); 891 let attacker_key = SigningKey::random(&mut rand::thread_rng()); 892 let attacker_point = attacker_key.verifying_key().to_encoded_point(false); 893 let x = URL_SAFE_NO_PAD.encode(attacker_point.x().unwrap()); 894 let y = URL_SAFE_NO_PAD.encode(attacker_point.y().unwrap()); 895 let header = json!({ "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": x, "y": y } }); 896 let payload = json!({ "jti": format!("key-sub-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)), 897 "htm": "POST", "htu": "https://example.com/token", "iat": Utc::now().timestamp() }); 898 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 899 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 900 let signing_input = format!("{}.{}", header_b64, payload_b64); 901 let signature: Signature = signing_key.sign(signing_input.as_bytes()); 902 let mismatched = format!( 903 "{}.{}", 904 signing_input, 905 URL_SAFE_NO_PAD.encode(signature.to_bytes()) 906 ); 907 assert!( 908 verifier 909 .verify_proof(&mismatched, "POST", "https://example.com/token", None) 910 .is_err(), 911 "Mismatched key should fail" 912 ); 913 let point = signing_key.verifying_key().to_encoded_point(false); 914 let good_header = json!({ "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", 915 "x": URL_SAFE_NO_PAD.encode(point.x().unwrap()), "y": URL_SAFE_NO_PAD.encode(point.y().unwrap()) } }); 916 let good_header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&good_header).unwrap()); 917 let good_input = format!("{}.{}", good_header_b64, payload_b64); 918 let good_sig: Signature = signing_key.sign(good_input.as_bytes()); 919 let mut sig_bytes = good_sig.to_bytes().to_vec(); 920 sig_bytes[0] ^= 0xFF; 921 let tampered = format!("{}.{}", good_input, URL_SAFE_NO_PAD.encode(&sig_bytes)); 922 assert!( 923 verifier 924 .verify_proof(&tampered, "POST", "https://example.com/token", None) 925 .is_err(), 926 "Tampered sig should fail" 927 ); 928} 929 930#[test] 931fn test_jwk_thumbprint() { 932 let jwk = DPoPJwk { 933 kty: "EC".to_string(), 934 crv: Some("P-256".to_string()), 935 x: Some("WbbXrPhtCg66wuF0NLhzXxF5PFzNZ7wNJm9M_1pCcXY".to_string()), 936 y: Some("DubR6_2kU1H5EYhbcNpYZGy1EY6GEKKxv6PYx8VW0rA".to_string()), 937 }; 938 let tp1 = compute_jwk_thumbprint(&jwk).unwrap(); 939 let tp2 = compute_jwk_thumbprint(&jwk).unwrap(); 940 assert_eq!(tp1, tp2, "Thumbprint should be deterministic"); 941 assert!(!tp1.is_empty()); 942 assert!( 943 compute_jwk_thumbprint(&DPoPJwk { 944 kty: "EC".to_string(), 945 crv: Some("secp256k1".to_string()), 946 x: Some("x".to_string()), 947 y: Some("y".to_string()) 948 }) 949 .is_ok() 950 ); 951 assert!( 952 compute_jwk_thumbprint(&DPoPJwk { 953 kty: "OKP".to_string(), 954 crv: Some("Ed25519".to_string()), 955 x: Some("x".to_string()), 956 y: None 957 }) 958 .is_ok() 959 ); 960 assert!( 961 compute_jwk_thumbprint(&DPoPJwk { 962 kty: "EC".to_string(), 963 crv: None, 964 x: Some("x".to_string()), 965 y: Some("y".to_string()) 966 }) 967 .is_err() 968 ); 969 assert!( 970 compute_jwk_thumbprint(&DPoPJwk { 971 kty: "EC".to_string(), 972 crv: Some("P-256".to_string()), 973 x: None, 974 y: Some("y".to_string()) 975 }) 976 .is_err() 977 ); 978 assert!( 979 compute_jwk_thumbprint(&DPoPJwk { 980 kty: "EC".to_string(), 981 crv: Some("P-256".to_string()), 982 x: Some("x".to_string()), 983 y: None 984 }) 985 .is_err() 986 ); 987 assert!( 988 compute_jwk_thumbprint(&DPoPJwk { 989 kty: "RSA".to_string(), 990 crv: None, 991 x: None, 992 y: None 993 }) 994 .is_err() 995 ); 996} 997 998#[test] 999fn test_dpop_clock_skew() { 1000 use p256::ecdsa::{Signature, SigningKey, signature::Signer}; 1001 use p256::elliptic_curve::sec1::ToEncodedPoint; 1002 let secret = b"test-dpop-secret-32-bytes-long!!"; 1003 let verifier = DPoPVerifier::new(secret); 1004 let test_cases = vec![ 1005 (-600, true), 1006 (-301, true), 1007 (-299, false), 1008 (0, false), 1009 (299, false), 1010 (301, true), 1011 (600, true), 1012 ]; 1013 for (offset, should_fail) in test_cases { 1014 let signing_key = SigningKey::random(&mut rand::thread_rng()); 1015 let point = signing_key.verifying_key().to_encoded_point(false); 1016 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap()); 1017 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap()); 1018 let header = json!({ "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": x, "y": y } }); 1019 let payload = json!({ "jti": format!("clock-{}-{}", offset, Utc::now().timestamp_nanos_opt().unwrap_or(0)), 1020 "htm": "POST", "htu": "https://example.com/token", "iat": Utc::now().timestamp() + offset }); 1021 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 1022 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 1023 let signing_input = format!("{}.{}", header_b64, payload_b64); 1024 let signature: Signature = signing_key.sign(signing_input.as_bytes()); 1025 let proof = format!( 1026 "{}.{}", 1027 signing_input, 1028 URL_SAFE_NO_PAD.encode(signature.to_bytes()) 1029 ); 1030 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None); 1031 if should_fail { 1032 assert!(result.is_err(), "offset {} should fail", offset); 1033 } else { 1034 assert!(result.is_ok(), "offset {} should pass", offset); 1035 } 1036 } 1037} 1038 1039#[test] 1040fn test_dpop_http_method_case() { 1041 use p256::ecdsa::{Signature, SigningKey, signature::Signer}; 1042 use p256::elliptic_curve::sec1::ToEncodedPoint; 1043 let secret = b"test-dpop-secret-32-bytes-long!!"; 1044 let verifier = DPoPVerifier::new(secret); 1045 let signing_key = SigningKey::random(&mut rand::thread_rng()); 1046 let point = signing_key.verifying_key().to_encoded_point(false); 1047 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap()); 1048 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap()); 1049 let header = json!({ "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": x, "y": y } }); 1050 let payload = json!({ "jti": format!("case-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)), 1051 "htm": "post", "htu": "https://example.com/token", "iat": Utc::now().timestamp() }); 1052 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 1053 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 1054 let signing_input = format!("{}.{}", header_b64, payload_b64); 1055 let signature: Signature = signing_key.sign(signing_input.as_bytes()); 1056 let proof = format!( 1057 "{}.{}", 1058 signing_input, 1059 URL_SAFE_NO_PAD.encode(signature.to_bytes()) 1060 ); 1061 assert!( 1062 verifier 1063 .verify_proof(&proof, "POST", "https://example.com/token", None) 1064 .is_ok(), 1065 "HTTP method should be case-insensitive" 1066 ); 1067}