diff --git a/Cargo.lock b/Cargo.lock index e357084659f7e..fb16c0e1f59cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6314,6 +6314,8 @@ dependencies = [ "axum-server 0.7.1", "chrono", "hostname", + "http 1.1.0", + "httpmock", "lazy_static", "port_scanner", "reqwest", diff --git a/crates/turborepo-auth/Cargo.toml b/crates/turborepo-auth/Cargo.toml index 477a04ab73664..6f91a19def06a 100644 --- a/crates/turborepo-auth/Cargo.toml +++ b/crates/turborepo-auth/Cargo.toml @@ -32,4 +32,6 @@ url = { workspace = true } webbrowser = { workspace = true } [dev-dependencies] +http = "1.1.0" +httpmock = { workspace = true } port_scanner = { workspace = true } diff --git a/crates/turborepo-auth/src/lib.rs b/crates/turborepo-auth/src/lib.rs index 2bb6652653cbf..a6fb75905ce6e 100644 --- a/crates/turborepo-auth/src/lib.rs +++ b/crates/turborepo-auth/src/lib.rs @@ -111,6 +111,33 @@ impl Token { Ok(true) } + async fn handle_sso_token_error( + &self, + client: &T, + error: reqwest::Error, + ) -> Result { + if error.status() == Some(reqwest::StatusCode::FORBIDDEN) { + let metadata = self.fetch_metadata(client).await?; + if !metadata.token_type.is_empty() { + return Err(Error::APIError(turborepo_api_client::Error::InvalidToken { + status: error + .status() + .unwrap_or(reqwest::StatusCode::FORBIDDEN) + .as_u16(), + url: error + .url() + .map(|u| u.to_string()) + .unwrap_or("Unknown url".to_string()), + message: error.to_string(), + })); + } + } + + Err(Error::APIError(turborepo_api_client::Error::ReqwestError( + error, + ))) + } + /// This is the same as `is_valid`, but also checks if the token is valid /// for SSO. /// @@ -158,7 +185,12 @@ impl Token { Ok(true) } - (Err(e), _) | (_, Err(e)) => Err(Error::APIError(e)), + (Err(e), _) | (_, Err(e)) => match e { + turborepo_api_client::Error::ReqwestError(e) => { + self.handle_sso_token_error(client, e).await + } + e => Err(Error::APIError(e)), + }, } } @@ -675,4 +707,124 @@ mod tests { let result = Token::from_file(&file_path); assert!(matches!(result, Err(Error::TokenNotFound))); } + + struct MockSSOTokenClient { + metadata_response: Option, + } + + impl TokenClient for MockSSOTokenClient { + async fn get_metadata( + &self, + _token: &str, + ) -> turborepo_api_client::Result { + if let Some(metadata) = &self.metadata_response { + Ok(metadata.clone()) + } else { + Ok(ResponseTokenMetadata { + id: "test".to_string(), + name: "test".to_string(), + token_type: "".to_string(), + origin: "test".to_string(), + scopes: vec![], + active_at: current_unix_time() - 100, + created_at: 0, + }) + } + } + + async fn delete_token(&self, _token: &str) -> turborepo_api_client::Result<()> { + Ok(()) + } + } + + #[tokio::test] + async fn test_handle_sso_token_error_forbidden_with_invalid_token_error() { + let token = Token::new("test-token".to_string()); + let client = MockSSOTokenClient { + metadata_response: Some(ResponseTokenMetadata { + id: "test".to_string(), + name: "test".to_string(), + token_type: "sso".to_string(), + origin: "test".to_string(), + scopes: vec![], + active_at: current_unix_time() - 100, + created_at: 0, + }), + }; + + let errorful_response = reqwest::Response::from( + http::Response::builder() + .status(reqwest::StatusCode::FORBIDDEN) + .body("") + .unwrap(), + ); + + let result = token + .handle_sso_token_error(&client, errorful_response.error_for_status().unwrap_err()) + .await; + assert!(matches!( + result, + Err(Error::APIError( + turborepo_api_client::Error::InvalidToken { .. } + )) + )); + } + + #[tokio::test] + async fn test_handle_sso_token_error_forbidden_without_token_type() { + let token = Token::new("test-token".to_string()); + let client = MockSSOTokenClient { + metadata_response: Some(ResponseTokenMetadata { + id: "test".to_string(), + name: "test".to_string(), + token_type: "".to_string(), + origin: "test".to_string(), + scopes: vec![], + active_at: current_unix_time() - 100, + created_at: 0, + }), + }; + + let errorful_response = reqwest::Response::from( + http::Response::builder() + .status(reqwest::StatusCode::FORBIDDEN) + .body("") + .unwrap(), + ); + + let result = token + .handle_sso_token_error(&client, errorful_response.error_for_status().unwrap_err()) + .await; + assert!(matches!( + result, + Err(Error::APIError(turborepo_api_client::Error::ReqwestError( + _ + ))) + )); + } + + #[tokio::test] + async fn test_handle_sso_token_error_non_forbidden() { + let token = Token::new("test-token".to_string()); + let client = MockSSOTokenClient { + metadata_response: None, + }; + + let errorful_response = reqwest::Response::from( + http::Response::builder() + .status(reqwest::StatusCode::INTERNAL_SERVER_ERROR) + .body("") + .unwrap(), + ); + + let result = token + .handle_sso_token_error(&client, errorful_response.error_for_status().unwrap_err()) + .await; + assert!(matches!( + result, + Err(Error::APIError(turborepo_api_client::Error::ReqwestError( + _ + ))) + )); + } }