An easy-to-host PDS on the ATProtocol, MacOS. Grandma-approved.

fix(relay): address deep security review findings F1-F10

Critical fixes:
- F1: Add typ header discrimination between ES256 and HS256 tokens
- F2: Validate code_challenge_method is "S256" at token endpoint
- F10: Validate EC curve matches algorithm (P-256 for ES256)

Important fixes:
- F3: Reject refresh tokens with NULL jkt binding (treat as invalid_grant)
- F4: Add jti claim to AT+JWT access tokens (RFC 7519)
- F5: Add Cache-Control: no-store headers to token responses
- F6: Add cleanup functions for expired auth codes and refresh tokens
- F7: Validate code_verifier length (43-128 chars, unreserved charset)
- F8: Use constant-time comparison for jkt values (subtle::ConstantTimeEq)
- F9: Add ath claim validation at resource endpoints (RFC 9449 §4.3)

Implementation details:
- Updated OAuthSigningKey to include public_key_jwk for ES256 verification
- Added peek_jwt_typ helper for algorithm discrimination
- Split verify_access_token into ES256 and HS256 variants
- Added make_dpop_proof_with_iat_and_ath test helper for ath validation
- Updated all token response headers with Cache-Control headers
- Call cleanup functions on every token request for DB maintenance
- Updated test utilities to use valid 43-char code_verifier

+279 -16
+12
crates/relay/src/app.rs
··· 188 188 189 189 // Generate a fresh ephemeral P-256 keypair for tests (no DB persistence needed). 190 190 let test_signing_key = { 191 + use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; 191 192 let sk = p256::ecdsa::SigningKey::random(&mut OsRng); 192 193 let pkcs8 = sk 193 194 .to_pkcs8_der() 194 195 .expect("PKCS#8 encoding must succeed for test key"); 196 + let vk = sk.verifying_key(); 197 + let point = vk.to_encoded_point(false); 198 + let x = URL_SAFE_NO_PAD.encode(point.x().expect("P-256 x")); 199 + let y = URL_SAFE_NO_PAD.encode(point.y().expect("P-256 y")); 200 + let public_key_jwk = serde_json::json!({ 201 + "kty": "EC", 202 + "crv": "P-256", 203 + "x": x, 204 + "y": y, 205 + }); 195 206 OAuthSigningKey { 196 207 key_id: "test-oauth-key-01".to_string(), 197 208 encoding_key: jsonwebtoken::EncodingKey::from_ec_der(pkcs8.as_bytes()), 209 + public_key_jwk, 198 210 } 199 211 }; 200 212 let dpop_nonces = new_nonce_store();
+161 -4
crates/relay/src/auth/mod.rs
··· 74 74 pub key_id: String, 75 75 /// PKCS#8 DER ES256 encoding key for JWT signing. 76 76 pub encoding_key: jsonwebtoken::EncodingKey, 77 + /// Public JWK for verifying ES256 AT+JWT tokens at resource endpoints. 78 + pub public_key_jwk: serde_json::Value, 77 79 } 78 80 79 81 /// In-memory store for server-issued DPoP nonces. ··· 131 133 /// Server-issued DPoP nonce (RFC 9449 §8). Required when the server has issued one. 132 134 #[serde(default)] 133 135 nonce: Option<String>, 136 + /// Access token hash (RFC 9449 §4.3). Required at resource endpoints. 137 + /// `ath = base64url(SHA-256(ASCII(access_token)))`. 138 + #[serde(default)] 139 + ath: Option<String>, 134 140 } 135 141 136 142 // ── Extractor implementation ───────────────────────────────────────────────── ··· 205 211 &parts.uri, 206 212 &state.config.public_url, 207 213 &claims, 214 + token_str, 208 215 )?; 209 216 } 210 217 ··· 288 295 289 296 // Attempt to load an existing key. 290 297 if let Some(row) = get_oauth_signing_key(pool).await? { 291 - let key = decode_oauth_signing_key(&row.id, &row.private_key_encrypted, master_key)?; 298 + let key = decode_oauth_signing_key( 299 + &row.id, 300 + &row.private_key_encrypted, 301 + &row.public_key_jwk, 302 + master_key, 303 + )?; 292 304 tracing::info!(key_id = %row.id, "OAuth signing key loaded from database"); 293 305 return Ok(key); 294 306 } ··· 334 346 } 335 347 336 348 let encoding_key = build_encoding_key(&signing_key)?; 349 + let public_key_jwk_json: serde_json::Value = serde_json::from_str(&public_key_jwk) 350 + .map_err(|e| anyhow::anyhow!("JWK JSON invalid after serialization: {e}"))?; 337 351 Ok(OAuthSigningKey { 338 352 key_id, 339 353 encoding_key, 354 + public_key_jwk: public_key_jwk_json, 340 355 }) 341 356 } 342 357 ··· 344 359 fn decode_oauth_signing_key( 345 360 key_id: &str, 346 361 private_key_encrypted: &str, 362 + public_key_jwk_str: &str, 347 363 master_key: Option<&[u8; 32]>, 348 364 ) -> anyhow::Result<OAuthSigningKey> { 349 365 let master_key = master_key.ok_or_else(|| { ··· 361 377 .map_err(|e| anyhow::anyhow!("invalid stored P-256 private key: {e}"))?; 362 378 363 379 let encoding_key = build_encoding_key(&signing_key)?; 380 + let public_key_jwk: serde_json::Value = serde_json::from_str(public_key_jwk_str) 381 + .map_err(|e| anyhow::anyhow!("public JWK JSON invalid: {e}"))?; 364 382 Ok(OAuthSigningKey { 365 383 key_id: key_id.to_string(), 366 384 encoding_key, 385 + public_key_jwk, 367 386 }) 368 387 } 369 388 ··· 426 445 )); 427 446 } 428 447 448 + // Validate that the embedded JWK curve matches the declared algorithm (F10). 449 + if dpop_header.alg == "ES256" 450 + && dpop_header.jwk.get("crv").and_then(|v| v.as_str()) != Some("P-256") 451 + { 452 + return Err(DpopTokenEndpointError::InvalidProof( 453 + "DPoP JWK crv must be P-256 for ES256", 454 + )); 455 + } 456 + 429 457 // Verify the signature against the embedded JWK. 430 458 let jwk: jsonwebtoken::jwk::Jwk = serde_json::from_value(dpop_header.jwk.clone()) 431 459 .map_err(|_| DpopTokenEndpointError::InvalidProof("DPoP JWK parse failed"))?; ··· 525 553 Ok(&auth_value[BEARER_LEN..]) 526 554 } 527 555 528 - /// Decode and verify the HS256 access/refresh JWT issued by this server. 556 + /// Peek at the JWT header's `typ` field without verifying the signature. 557 + /// Returns the `typ` value in lowercase, or `None` if parsing fails. 558 + fn peek_jwt_typ(token: &str) -> Option<String> { 559 + let header_b64 = token.split('.').next()?; 560 + let header_bytes = URL_SAFE_NO_PAD.decode(header_b64).ok()?; 561 + let header: serde_json::Value = serde_json::from_slice(&header_bytes).ok()?; 562 + header["typ"].as_str().map(|s| s.to_ascii_lowercase()) 563 + } 564 + 565 + /// Dispatch to the correct verification function based on token type. 566 + /// Uses `typ` header as algorithm discriminator to prevent algorithm confusion attacks. 529 567 fn verify_access_token(token: &str, state: &AppState) -> Result<AccessTokenClaims, ApiError> { 568 + if peek_jwt_typ(token).as_deref() == Some("at+jwt") { 569 + verify_es256_access_token(token, state) 570 + } else { 571 + verify_hs256_access_token(token, state) 572 + } 573 + } 574 + 575 + /// Verify ES256 AT+JWT tokens issued by the OAuth token endpoint. 576 + fn verify_es256_access_token(token: &str, state: &AppState) -> Result<AccessTokenClaims, ApiError> { 577 + let invalid = || ApiError::new(ErrorCode::InvalidToken, "invalid token"); 578 + let jwk: jsonwebtoken::jwk::Jwk = serde_json::from_value( 579 + state.oauth_signing_keypair.public_key_jwk.clone(), 580 + ) 581 + .map_err(|_| { 582 + tracing::error!("failed to parse OAuth signing key JWK for ES256 token verification"); 583 + invalid() 584 + })?; 585 + let decoding_key = DecodingKey::from_jwk(&jwk).map_err(|e| { 586 + tracing::error!(error = %e, "failed to build ES256 DecodingKey from OAuth signing key JWK"); 587 + invalid() 588 + })?; 589 + let mut validation = Validation::new(Algorithm::ES256); 590 + validation.set_required_spec_claims(&["exp", "sub"]); 591 + validation.leeway = 0; 592 + validation.set_audience(&[state.config.public_url.as_str()]); 593 + decode::<AccessTokenClaims>(token, &decoding_key, &validation) 594 + .map(|data| data.claims) 595 + .map_err(|e| { 596 + use jsonwebtoken::errors::ErrorKind; 597 + match e.kind() { 598 + ErrorKind::ExpiredSignature => ApiError::new(ErrorCode::TokenExpired, "token has expired"), 599 + _ => { 600 + tracing::debug!(error = %e, error_kind = ?e.kind(), "ES256 access token verification failed"); 601 + invalid() 602 + } 603 + } 604 + }) 605 + } 606 + 607 + /// Verify HS256 access/refresh JWT issued by this server (legacy tokens). 608 + fn verify_hs256_access_token(token: &str, state: &AppState) -> Result<AccessTokenClaims, ApiError> { 530 609 let decoding_key = DecodingKey::from_secret(&state.jwt_secret); 531 610 532 611 let mut validation = Validation::new(Algorithm::HS256); ··· 584 663 /// - `jti` is present and non-empty 585 664 /// - `iat` is within the 60-second freshness window 586 665 /// - Access token `cnf.jkt` matches the computed JWK thumbprint 666 + /// - `ath` claim is present and matches the access token 587 667 fn validate_dpop( 588 668 dpop_token: &str, 589 669 method: &Method, 590 670 uri: &axum::http::Uri, 591 671 public_url: &str, 592 672 access_claims: &AccessTokenClaims, 673 + access_token_str: &str, 593 674 ) -> Result<(), ApiError> { 594 675 let invalid = || ApiError::new(ErrorCode::InvalidToken, "DPoP proof invalid"); 595 676 ··· 610 691 return Err(ApiError::new( 611 692 ErrorCode::InvalidToken, 612 693 "DPoP proof typ must be dpop+jwt", 694 + )); 695 + } 696 + 697 + // Validate that the embedded JWK curve matches the declared algorithm (F10). 698 + if dpop_header.alg == "ES256" 699 + && dpop_header.jwk.get("crv").and_then(|v| v.as_str()) != Some("P-256") 700 + { 701 + return Err(ApiError::new( 702 + ErrorCode::InvalidToken, 703 + "DPoP JWK crv must be P-256 for ES256", 613 704 )); 614 705 } 615 706 ··· 660 751 ErrorCode::InvalidToken, 661 752 "DPoP key thumbprint does not match token binding", 662 753 )); 754 + } 755 + 756 + // Validate ath (RFC 9449 §4.3): binds the proof to a specific access token. 757 + let expected_ath = { 758 + let hash = Sha256::digest(access_token_str.as_bytes()); 759 + URL_SAFE_NO_PAD.encode(hash) 760 + }; 761 + match dpop_claims.ath.as_deref() { 762 + None | Some("") => { 763 + return Err(ApiError::new( 764 + ErrorCode::InvalidToken, 765 + "DPoP proof missing ath claim", 766 + )); 767 + } 768 + Some(ath) => { 769 + use subtle::ConstantTimeEq; 770 + if !bool::from(ath.as_bytes().ct_eq(expected_ath.as_bytes())) { 771 + tracing::debug!("DPoP proof ath does not match access token hash"); 772 + return Err(ApiError::new( 773 + ErrorCode::InvalidToken, 774 + "DPoP proof ath does not match access token", 775 + )); 776 + } 777 + } 663 778 } 664 779 665 780 // Require `jti` for replay protection. Full deduplication per RFC 9449 §11.1 ··· 853 968 } 854 969 855 970 /// Build a valid DPoP proof JWT signed with the given P-256 key using current time as `iat`. 971 + /// Includes the ath (access token hash) claim for use at resource endpoints. 856 972 fn make_dpop_proof(key: &SigningKey, htm: &str, htu: &str) -> String { 857 - make_dpop_proof_with_iat(key, htm, htu, now_secs() as i64) 973 + // Use a dummy access token for tests — the ath is computed from this. 974 + let dummy_access_token = "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJpc3MiOiJodHRwczovL3Rlc3QuZXhhbXBsZS5jb20iLCJqdGkiOiIxMjM0NTY3OC1hYmNkLWVmZ2gtaWprbCIsInN1YiI6ImRpZDpwbGM6YWxpY2UiLCJhdWQiOiJodHRwczovL3Rlc3QuZXhhbXBsZS5jb20iLCJpYXQiOjE2NzcwMDAwMDAsImV4cCI6MTY3NzAwMzAwMCwic2NvcGUiOiJjb20uYXRwcm90by5hY2Nlc3MiLCJjbmYiOnsianRrIjoiMTIzNDU2Nzg5MCJ9fQ.signature"; 975 + make_dpop_proof_with_iat_and_ath(key, htm, htu, now_secs() as i64, dummy_access_token) 858 976 } 859 977 860 978 /// Build a DPoP proof JWT with an explicit `iat` — used to test freshness rejection. 979 + /// Does NOT include ath claim (for token endpoint tests where ath is not required). 861 980 fn make_dpop_proof_with_iat(key: &SigningKey, htm: &str, htu: &str, iat: i64) -> String { 862 981 let jwk = dpop_key_to_jwk(key); 863 982 let header = serde_json::json!({ ··· 870 989 "htu": htu, 871 990 "iat": iat, 872 991 "jti": uuid::Uuid::new_v4().to_string(), 992 + }); 993 + 994 + let hdr_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap().as_bytes()); 995 + let pay_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap().as_bytes()); 996 + let signing_input = format!("{hdr_b64}.{pay_b64}"); 997 + 998 + let sig: Signature = key.sign(signing_input.as_bytes()); 999 + let sig_b64 = URL_SAFE_NO_PAD.encode(sig.to_bytes().as_ref() as &[u8]); 1000 + 1001 + format!("{hdr_b64}.{pay_b64}.{sig_b64}") 1002 + } 1003 + 1004 + /// Build a DPoP proof JWT with explicit `iat` and `ath` claim (for resource endpoint testing). 1005 + fn make_dpop_proof_with_iat_and_ath( 1006 + key: &SigningKey, 1007 + htm: &str, 1008 + htu: &str, 1009 + iat: i64, 1010 + access_token: &str, 1011 + ) -> String { 1012 + let jwk = dpop_key_to_jwk(key); 1013 + let header = serde_json::json!({ 1014 + "typ": "dpop+jwt", 1015 + "alg": "ES256", 1016 + "jwk": jwk, 1017 + }); 1018 + let ath = { 1019 + let hash = Sha256::digest(access_token.as_bytes()); 1020 + URL_SAFE_NO_PAD.encode(hash) 1021 + }; 1022 + let payload = serde_json::json!({ 1023 + "htm": htm, 1024 + "htu": htu, 1025 + "iat": iat, 1026 + "jti": uuid::Uuid::new_v4().to_string(), 1027 + "ath": ath, 873 1028 }); 874 1029 875 1030 let hdr_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap().as_bytes()); ··· 1179 1334 ); 1180 1335 // htu = public_url + path (matches how the extractor builds expected_htu) 1181 1336 let htu = format!("{}/protected", state.config.public_url); 1182 - let dpop_proof = make_dpop_proof(&dpop_key, "GET", &htu); 1337 + // DPoP proof needs the ath (access token hash) claim for resource endpoint verification. 1338 + let dpop_proof = 1339 + make_dpop_proof_with_iat_and_ath(&dpop_key, "GET", &htu, now_secs() as i64, &token); 1183 1340 1184 1341 let req = Request::builder() 1185 1342 .uri("/protected")
+22 -1
crates/relay/src/db/oauth.rs
··· 168 168 pub client_id: String, 169 169 pub did: String, 170 170 pub code_challenge: String, 171 - #[allow(dead_code)] 172 171 pub code_challenge_method: String, 173 172 pub redirect_uri: String, 174 173 #[allow(dead_code)] ··· 361 360 ) -> Result<(), sqlx::Error> { 362 361 sqlx::query("DELETE FROM oauth_tokens WHERE id = ?") 363 362 .bind(token_hash) 363 + .execute(pool) 364 + .await?; 365 + Ok(()) 366 + } 367 + 368 + /// Delete all expired authorization codes from the database. 369 + /// 370 + /// Call alongside `cleanup_expired_nonces` on every token request to prevent unbounded 371 + /// DB growth from abandoned authorization flows. 372 + pub async fn cleanup_expired_auth_codes(pool: &SqlitePool) -> Result<(), sqlx::Error> { 373 + sqlx::query("DELETE FROM oauth_authorization_codes WHERE expires_at <= datetime('now')") 374 + .execute(pool) 375 + .await?; 376 + Ok(()) 377 + } 378 + 379 + /// Delete all expired refresh tokens from the database. 380 + /// 381 + /// Call alongside `cleanup_expired_nonces` on every token request to prevent unbounded 382 + /// DB growth from expired sessions. 383 + pub async fn cleanup_expired_refresh_tokens(pool: &SqlitePool) -> Result<(), sqlx::Error> { 384 + sqlx::query("DELETE FROM oauth_tokens WHERE expires_at <= datetime('now')") 364 385 .execute(pool) 365 386 .await?; 366 387 Ok(())
+84 -11
crates/relay/src/routes/oauth_token.rs
··· 22 22 cleanup_expired_nonces, issue_nonce, validate_dpop_for_token_endpoint, DpopTokenEndpointError, 23 23 }; 24 24 use crate::db::oauth::{ 25 - delete_authorization_code, delete_oauth_refresh_token, get_authorization_code, 26 - get_oauth_refresh_token, store_oauth_refresh_token, 25 + cleanup_expired_auth_codes, cleanup_expired_refresh_tokens, delete_authorization_code, 26 + delete_oauth_refresh_token, get_authorization_code, get_oauth_refresh_token, 27 + store_oauth_refresh_token, 27 28 }; 28 29 use crate::routes::token::generate_token; 29 30 ··· 124 125 struct AccessTokenClaims { 125 126 /// Issuer (RFC 9068 §2.2): the server's public URL. 126 127 iss: String, 128 + /// Unique JWT identifier (RFC 7519). 129 + jti: String, 127 130 /// Subject (RFC 9068 §2.2): the authenticated user's DID. 128 131 sub: String, 129 132 /// Audience (RFC 9068 §2.2): typically the server's URL; used for token binding validation. ··· 150 153 jkt: &str, 151 154 public_url: &str, 152 155 ) -> Result<String, OAuthTokenError> { 156 + use uuid::Uuid; 157 + 153 158 let now = SystemTime::now() 154 159 .duration_since(UNIX_EPOCH) 155 160 .map_err(|_| OAuthTokenError::new("server_error", "system clock error"))? ··· 157 162 158 163 let claims = AccessTokenClaims { 159 164 iss: public_url.to_string(), 165 + jti: Uuid::new_v4().to_string(), 160 166 sub: did.to_string(), 161 167 aud: public_url.to_string(), 162 168 iat: now, ··· 224 230 ) -> Response { 225 231 // Prune stale nonces on every request. 226 232 cleanup_expired_nonces(&state.dpop_nonces).await; 233 + // F6: Clean up expired auth codes and refresh tokens to prevent DB growth. 234 + cleanup_expired_auth_codes(&state.db) 235 + .await 236 + .unwrap_or_else(|e| { 237 + tracing::warn!(error = %e, "failed to clean up expired auth codes"); 238 + }); 239 + cleanup_expired_refresh_tokens(&state.db) 240 + .await 241 + .unwrap_or_else(|e| { 242 + tracing::warn!(error = %e, "failed to clean up expired refresh tokens"); 243 + }); 227 244 228 245 // Required fields: code, redirect_uri, client_id, code_verifier. 229 246 let code = match form.code.as_deref() { ··· 255 272 } 256 273 }; 257 274 275 + // RFC 7636 §4.1: 43–128 unreserved characters [A-Za-z0-9\-._~] (F7). 276 + { 277 + const CV_UNRESERVED: fn(u8) -> bool = 278 + |b: u8| b.is_ascii_alphanumeric() || b == b'-' || b == b'.' || b == b'_' || b == b'~'; 279 + if code_verifier.len() < 43 280 + || code_verifier.len() > 128 281 + || !code_verifier.bytes().all(CV_UNRESERVED) 282 + { 283 + return OAuthTokenError::new( 284 + "invalid_grant", 285 + "code_verifier must be 43–128 unreserved characters [A-Za-z0-9-._~]", 286 + ) 287 + .into_response(); 288 + } 289 + } 290 + 258 291 // Reject multiple DPoP headers (RFC 9449 §11.1). 259 292 if headers.get_all("DPoP").iter().count() > 1 { 260 293 return OAuthTokenError::new( ··· 332 365 return OAuthTokenError::new("invalid_grant", "redirect_uri mismatch").into_response(); 333 366 } 334 367 368 + // Enforce S256 — reject plain (or any other method) in case it ever enters the DB (F2). 369 + if auth_code.code_challenge_method != "S256" { 370 + return OAuthTokenError::new("invalid_grant", "unsupported code_challenge_method") 371 + .into_response(); 372 + } 373 + 335 374 // Verify PKCE S256 challenge before consuming. 336 375 if !verify_pkce_s256(code_verifier, &auth_code.code_challenge) { 337 376 return OAuthTokenError::new( ··· 391 430 .into_response(); 392 431 } 393 432 } 433 + // F5: Add Cache-Control headers to prevent caching of sensitive token response. 434 + response_headers.insert( 435 + axum::http::header::CACHE_CONTROL, 436 + axum::http::HeaderValue::from_static("no-store"), 437 + ); 438 + response_headers.insert("Pragma", axum::http::HeaderValue::from_static("no-cache")); 394 439 395 440 ( 396 441 StatusCode::OK, ··· 413 458 ) -> Response { 414 459 // Prune stale nonces on every request. 415 460 cleanup_expired_nonces(&state.dpop_nonces).await; 461 + // F6: Clean up expired auth codes and refresh tokens to prevent DB growth. 462 + cleanup_expired_auth_codes(&state.db) 463 + .await 464 + .unwrap_or_else(|e| { 465 + tracing::warn!(error = %e, "failed to clean up expired auth codes"); 466 + }); 467 + cleanup_expired_refresh_tokens(&state.db) 468 + .await 469 + .unwrap_or_else(|e| { 470 + tracing::warn!(error = %e, "failed to clean up expired refresh tokens"); 471 + }); 416 472 417 473 // Required fields. 418 474 let refresh_token_plaintext = match form.refresh_token.as_deref() { ··· 503 559 } 504 560 505 561 // DPoP binding check before consuming: if the refresh token was bound to a specific key, the same key must be used. 506 - if let Some(ref stored_jkt) = stored.jkt { 507 - if *stored_jkt != jkt { 508 - return OAuthTokenError::new("invalid_grant", "DPoP key mismatch").into_response(); 562 + // F3: NULL jkt on refresh token bypasses DPoP binding — treat None jkt as invalid_grant. 563 + match stored.jkt.as_deref() { 564 + None => { 565 + // Refresh tokens issued after V012 always have a jkt. A NULL jkt means 566 + // the token predates DPoP binding enforcement — reject rather than 567 + // silently accepting any key. 568 + return OAuthTokenError::new("invalid_grant", "refresh token not found or expired") 569 + .into_response(); 570 + } 571 + Some(stored_jkt) => { 572 + use subtle::ConstantTimeEq; 573 + if !bool::from(stored_jkt.as_bytes().ct_eq(jkt.as_bytes())) { 574 + return OAuthTokenError::new("invalid_grant", "DPoP key mismatch").into_response(); 575 + } 509 576 } 510 577 } 511 578 ··· 559 626 .into_response(); 560 627 } 561 628 } 629 + // F5: Add Cache-Control headers to prevent caching of sensitive token response. 630 + response_headers.insert( 631 + axum::http::header::CACHE_CONTROL, 632 + axum::http::HeaderValue::from_static("no-store"), 633 + ); 634 + response_headers.insert("Pragma", axum::http::HeaderValue::from_static("no-cache")); 562 635 563 636 ( 564 637 StatusCode::OK, ··· 776 849 async fn missing_dpop_header_returns_invalid_dpop_proof() { 777 850 let resp = app(test_state().await) 778 851 .oneshot(post_token( 779 - "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=x", 852 + "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", 780 853 )) 781 854 .await 782 855 .unwrap(); ··· 800 873 801 874 let resp = app(state) 802 875 .oneshot(post_token_with_dpop( 803 - "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=x", 876 + "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", 804 877 &dpop, 805 878 )) 806 879 .await ··· 828 901 829 902 let resp = app(state) 830 903 .oneshot(post_token_with_dpop( 831 - "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=x", 904 + "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", 832 905 &dpop, 833 906 )) 834 907 .await ··· 853 926 854 927 let resp = app(state) 855 928 .oneshot(post_token_with_dpop( 856 - "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=x", 929 + "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", 857 930 &dpop, 858 931 )) 859 932 .await ··· 879 952 880 953 let resp = app(state) 881 954 .oneshot(post_token_with_dpop( 882 - "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=x", 955 + "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", 883 956 &dpop, 884 957 )) 885 958 .await ··· 908 981 909 982 let resp = app(state) 910 983 .oneshot(post_token_with_dpop( 911 - "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=x", 984 + "grant_type=authorization_code&code=x&redirect_uri=x&client_id=x&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", 912 985 &dpop, 913 986 )) 914 987 .await