From a20e4e05e6a5201e810c6eb1182ede18e5352ef0 Mon Sep 17 00:00:00 2001 From: Lewis Date: Fri, 17 Apr 2026 14:14:03 +0300 Subject: [PATCH] fix(oauth): indigo client send empty assertions Lewis: May this revision serve well! --- .../src/endpoints/par.rs | 20 ++++++++---- .../src/endpoints/token/types.rs | 8 +++-- crates/tranquil-pds/tests/oauth.rs | 31 +++++++++++++++++++ 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/crates/tranquil-oauth-server/src/endpoints/par.rs b/crates/tranquil-oauth-server/src/endpoints/par.rs index e693597..516f82e 100644 --- a/crates/tranquil-oauth-server/src/endpoints/par.rs +++ b/crates/tranquil-oauth-server/src/endpoints/par.rs @@ -138,21 +138,29 @@ pub async fn pushed_authorization_request( } fn determine_client_auth(request: &ParRequest) -> Result { - if let (Some(assertion), Some(assertion_type)) = - (&request.client_assertion, &request.client_assertion_type) - { + let assertion = request + .client_assertion + .as_deref() + .filter(|s| !s.is_empty()); + let assertion_type = request + .client_assertion_type + .as_deref() + .filter(|s| !s.is_empty()); + let secret = request.client_secret.as_deref().filter(|s| !s.is_empty()); + + if let (Some(assertion), Some(assertion_type)) = (assertion, assertion_type) { if assertion_type != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" { return Err(OAuthError::InvalidRequest( "Unsupported client_assertion_type".to_string(), )); } return Ok(ClientAuth::PrivateKeyJwt { - client_assertion: assertion.clone(), + client_assertion: assertion.to_string(), }); } - if let Some(secret) = &request.client_secret { + if let Some(secret) = secret { return Ok(ClientAuth::SecretPost { - client_secret: secret.clone(), + client_secret: secret.to_string(), }); } Ok(ClientAuth::None) diff --git a/crates/tranquil-oauth-server/src/endpoints/token/types.rs b/crates/tranquil-oauth-server/src/endpoints/token/types.rs index 8f7342f..84d3b16 100644 --- a/crates/tranquil-oauth-server/src/endpoints/token/types.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/types.rs @@ -138,13 +138,17 @@ impl TokenRequest { } }; - let client_auth = match (self.client_assertion, self.client_assertion_type) { + let assertion = self.client_assertion.filter(|s| !s.is_empty()); + let assertion_type = self.client_assertion_type.filter(|s| !s.is_empty()); + let client_secret = self.client_secret.filter(|s| !s.is_empty()); + + let client_auth = match (assertion, assertion_type) { (Some(assertion), Some(assertion_type)) => RequestClientAuth::PrivateKeyJwt { client_id: self.client_id, assertion, assertion_type, }, - _ => match self.client_secret { + _ => match client_secret { Some(secret) => RequestClientAuth::SecretPost { client_id: self.client_id, client_secret: secret, diff --git a/crates/tranquil-pds/tests/oauth.rs b/crates/tranquil-pds/tests/oauth.rs index 1832ec1..284ac81 100644 --- a/crates/tranquil-pds/tests/oauth.rs +++ b/crates/tranquil-pds/tests/oauth.rs @@ -187,6 +187,37 @@ async fn test_par_and_authorize() { ); } +#[tokio::test] +async fn test_par_public_client_empty_assertion_fields() { + let url = base_url().await; + let client = client(); + let redirect_uri = "https://nels.evil.oauth.pet/callback"; + let mock_client = setup_mock_client_metadata(redirect_uri).await; + let client_id = mock_client.uri(); + let (_, code_challenge) = generate_pkce(); + let par_res = client + .post(format!("{}/oauth/par", url)) + .form(&[ + ("response_type", "code"), + ("client_id", &client_id), + ("redirect_uri", redirect_uri), + ("code_challenge", &code_challenge), + ("code_challenge_method", "S256"), + ("scope", "atproto"), + ("state", "test-state"), + ("client_assertion", ""), + ("client_assertion_type", ""), + ]) + .send() + .await + .unwrap(); + assert_eq!( + par_res.status(), + StatusCode::CREATED, + "PAR with empty assertion fields from a public client should succeed" + ); +} + #[tokio::test] async fn test_full_oauth_flow() { let url = base_url().await;