diff --git a/crates/tranquil-oauth-server/src/endpoints/token/grants.rs b/crates/tranquil-oauth-server/src/endpoints/token/grants.rs index e87035f..cf412e1 100644 --- a/crates/tranquil-oauth-server/src/endpoints/token/grants.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/grants.rs @@ -1,4 +1,6 @@ -use super::helpers::{create_access_token_with_delegation, verify_pkce}; +use super::helpers::{ + ACCESS_TOKEN_EXPIRY_SECONDS, create_access_token_with_delegation, verify_pkce, +}; use super::types::{ RequestClientAuth, TokenGrant, TokenResponse, TokenType, ValidatedTokenRequest, }; @@ -16,7 +18,6 @@ use tranquil_pds::oauth::{ use tranquil_pds::state::AppState; use tranquil_types::{ClientId, JwkThumbprint}; -const ACCESS_TOKEN_EXPIRY_SECONDS: u64 = 300; const REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL: i64 = 60; const REFRESH_TOKEN_EXPIRY_DAYS_PUBLIC: i64 = 14; diff --git a/crates/tranquil-oauth-server/src/endpoints/token/helpers.rs b/crates/tranquil-oauth-server/src/endpoints/token/helpers.rs index 3a56e08..f01584e 100644 --- a/crates/tranquil-oauth-server/src/endpoints/token/helpers.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/helpers.rs @@ -7,7 +7,7 @@ use subtle::ConstantTimeEq; use tranquil_pds::config::AuthConfig; use tranquil_pds::oauth::OAuthError; -const ACCESS_TOKEN_EXPIRY_SECONDS: i64 = 300; +pub(super) const ACCESS_TOKEN_EXPIRY_SECONDS: u64 = 60 * 60; pub struct TokenClaims { pub jti: String, @@ -42,7 +42,7 @@ pub fn create_access_token_with_delegation( let pds_hostname = &tranquil_config::get().server.hostname; let issuer = format!("https://{}", pds_hostname); let now = Utc::now().timestamp(); - let exp = now + ACCESS_TOKEN_EXPIRY_SECONDS; + let exp = now + ACCESS_TOKEN_EXPIRY_SECONDS as i64; let actual_scope = tranquil_pds::auth::encode_scope(scope.unwrap_or("atproto")) .map_err(|_| OAuthError::InvalidScope("Scope too large".to_string()))?; let mut payload = json!({ @@ -145,3 +145,13 @@ pub fn extract_token_claims(token: &str) -> Result { .ok_or_else(|| OAuthError::InvalidToken("Missing iat claim".to_string()))?; Ok(TokenClaims { jti, sid, exp, iat }) } + +#[cfg(test)] +mod tests { + use super::ACCESS_TOKEN_EXPIRY_SECONDS; + + #[test] + fn oauth_access_token_lifetime_matches_reference_pds() { + assert_eq!(ACCESS_TOKEN_EXPIRY_SECONDS, 60 * 60); + } +} diff --git a/crates/tranquil-pds/src/api/proxy.rs b/crates/tranquil-pds/src/api/proxy.rs index 3853d3d..14cde76 100644 --- a/crates/tranquil-pds/src/api/proxy.rs +++ b/crates/tranquil-pds/src/api/proxy.rs @@ -110,6 +110,12 @@ static PROTECTED_METHODS: LazyLock> = LazyLock::new(|| { methods }); +fn target_origin(target_url: &str) -> Option { + let url = reqwest::Url::parse(target_url).ok()?; + let origin = url.origin().ascii_serialization(); + (origin != "null").then_some(origin) +} + fn is_protected_method(method: &str) -> bool { PROTECTED_METHODS.contains(method) } @@ -394,6 +400,11 @@ async fn proxy_handler( let client = proxy_client(); let mut request_builder = client.request(method_verb.clone(), &target_url); + if !matches!(method_verb, Method::GET | Method::HEAD) + && let Some(origin) = target_origin(&target_url) + { + request_builder = request_builder.header(http::header::ORIGIN, origin); + } if let Some(val) = auth_header_val { request_builder = request_builder.header(http::header::AUTHORIZATION, val); } @@ -444,3 +455,21 @@ async fn proxy_handler( } } } + +#[cfg(test)] +mod tests { + use super::target_origin; + + #[test] + fn proxy_origin_matches_resolved_service_target() { + assert_eq!( + target_origin("https://wisp.place/xrpc/place.wisp.v2.domain.claim"), + Some("https://wisp.place".to_string()) + ); + assert_eq!( + target_origin("https://example.com:8443/xrpc/example.method"), + Some("https://example.com:8443".to_string()) + ); + assert_eq!(target_origin("not a URL"), None); + } +} diff --git a/crates/tranquil-pds/tests/oauth_lifecycle.rs b/crates/tranquil-pds/tests/oauth_lifecycle.rs index 4f10ad5..68fed9b 100644 --- a/crates/tranquil-pds/tests/oauth_lifecycle.rs +++ b/crates/tranquil-pds/tests/oauth_lifecycle.rs @@ -473,7 +473,22 @@ async fn test_oauth_token_refresh_maintains_access() { .unwrap(); assert_eq!(refresh_res.status(), StatusCode::OK); let refresh_body: Value = refresh_res.json().await.unwrap(); + assert_eq!(refresh_body["expires_in"], 60 * 60); let new_access_token = refresh_body["access_token"].as_str().unwrap(); + let payload_segment = new_access_token + .split('.') + .nth(1) + .expect("access token should contain a JWT payload"); + let payload: Value = serde_json::from_slice( + &URL_SAFE_NO_PAD + .decode(payload_segment) + .expect("access token payload should be base64url encoded"), + ) + .expect("access token payload should be JSON"); + assert_eq!( + payload["exp"].as_i64().unwrap() - payload["iat"].as_i64().unwrap(), + 60 * 60 + ); assert_ne!( new_access_token, session.access_token, "New token should be different"