use regex::Regex; use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials}; use crate::SalesforceAuthError; use crate::salesforce_token_response::SalesforceTokenResponse; impl SalesforceCredentials { /// Creates a configuration for authenticating from an SFDX auth URL. /// /// The SFDX URL must match: /// /// `force://CLIENT_ID:CLIENT_SECRET:REFRESH_TOKEN@LOGIN_ENDPOINT` /// /// The login endpoint must not include `https://`. /// /// # Examples /// /// ```rust /// use rustsf_auth::{SalesforceCredentials, SalesforceAuthFlow}; /// /// let config = SalesforceCredentials::sfdx_url( /// "force://client_id:client_secret:refresh-token@login.salesforce.com", /// ); /// /// assert_eq!(config.flow, SalesforceAuthFlow::SfdxUrl); /// assert!(config.sfdx_url.is_some()); /// ``` pub fn sfdx_url(url: impl Into) -> Result { let regex = Regex::new(r"force://([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)") .unwrap(); let url = url.into(); let captures = regex.captures(&url) // .with_context(|| "Invalid Authentication URL expected 'force://([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)'".to_string())?; .ok_or(SalesforceAuthError::InvalidSfdxUrl)?; let client_id = captures .get(1) .ok_or(SalesforceAuthError::InvalidSfdxUrl)? .as_str() .to_string(); let client_secret = captures .get(2) .map(|capture| capture.as_str().to_string()) .filter(|value| !value.is_empty()); let refresh_token = captures .get(3) .ok_or(SalesforceAuthError::InvalidSfdxUrl)? .as_str() .to_string(); let login_endpoint = captures .get(4) .ok_or(SalesforceAuthError::InvalidSfdxUrl)? .as_str(); Ok(Self { flow: SalesforceAuthFlow::SfdxUrl, login_url: Some(format!("https://{login_endpoint}")), client_id: Some(client_id), client_secret, username: None, private_key_pem: None, access_token: None, refresh_token: Some(refresh_token), instance_url: None, }) } /// Authenticates to Salesforce from an SFDX auth URL. /// /// The SFDX URL must match: /// /// `force://([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)` /// /// Regex groups are interpreted as: /// /// 1. client ID /// 2. client secret /// 3. refresh token /// 4. login endpoint without `https://` /// /// Internally this parses the SFDX URL and then calls [`refresh_access_token`]. /// /// # Errors /// /// Returns [`SalesforceAuthError::InvalidSfdxUrl`] if the URL does not match /// the required format. Returns other [`SalesforceAuthError`] variants if the /// refresh request fails. /// /// # Examples /// /// ```rust,no_run /// use rustsf_auth::authenticate_sfdx_url; /// /// # async fn example() -> Result<(), rustsf_auth::SalesforceAuthError> { /// let session = authenticate_sfdx_url( /// "force://client_id:client_secret:refresh-token@login.salesforce.com", /// ).await?; /// /// println!("{}", session.access_token); /// # Ok(()) /// # } /// ``` pub(crate) async fn connect_sfdx_url(&self) -> Result { let client_id = required(self.client_id.as_deref(), "client_id")?; let refresh_token = required(self.refresh_token.as_deref(), "refresh_token")?; Ok(http_client()? .post(self.token_url()?.url().clone()) .form(&[ ("grant_type", "refresh_token"), ("client_id", client_id), ("refresh_token", refresh_token), // ("client_secret", client_secret.unwrap_or_default()), ]) .send() .await? .error_for_status()? .json::() .await?) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_sfdx_url_valid_with_client_secret() { let url = "force://my_client_id:my_client_secret:my_refresh_token@login.salesforce.com"; let result = SalesforceCredentials::sfdx_url(url); assert!(result.is_ok()); let credentials = result.unwrap(); assert_eq!(credentials.flow, SalesforceAuthFlow::SfdxUrl); assert_eq!(credentials.client_id, Some("my_client_id".to_string())); assert_eq!(credentials.client_secret, Some("my_client_secret".to_string())); assert_eq!(credentials.refresh_token, Some("my_refresh_token".to_string())); assert_eq!(credentials.login_url, Some("https://login.salesforce.com".to_string())); assert!(credentials.username.is_none()); assert!(credentials.private_key_pem.is_none()); assert!(credentials.access_token.is_none()); assert!(credentials.instance_url.is_none()); } #[test] fn test_sfdx_url_valid_without_client_secret() { let url = "force://PlatformCLI::refresh_token_value@test.salesforce.com"; let result = SalesforceCredentials::sfdx_url(url); assert!(result.is_ok()); let credentials = result.unwrap(); assert_eq!(credentials.flow, SalesforceAuthFlow::SfdxUrl); assert_eq!(credentials.client_id, Some("PlatformCLI".to_string())); assert_eq!(credentials.client_secret, None); assert_eq!(credentials.refresh_token, Some("refresh_token_value".to_string())); assert_eq!(credentials.login_url, Some("https://test.salesforce.com".to_string())); } #[test] fn test_sfdx_url_with_base64_refresh_token() { let url = "force://client123:secret456:dGVzdF90b2tlbl92YWx1ZQ==@login.salesforce.com"; let result = SalesforceCredentials::sfdx_url(url); assert!(result.is_ok()); let credentials = result.unwrap(); assert_eq!(credentials.client_id, Some("client123".to_string())); assert_eq!(credentials.client_secret, Some("secret456".to_string())); assert_eq!(credentials.refresh_token, Some("dGVzdF90b2tlbl92YWx1ZQ==".to_string())); } #[test] fn test_sfdx_url_invalid_format() { let url = "https://invalid-format.com"; let result = SalesforceCredentials::sfdx_url(url); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), SalesforceAuthError::InvalidSfdxUrl)); } #[test] fn test_sfdx_url_missing_components() { let url = "force://client_id:@login.salesforce.com"; let result = SalesforceCredentials::sfdx_url(url); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), SalesforceAuthError::InvalidSfdxUrl)); } #[test] fn test_sfdx_url_invalid_scheme() { let url = "http://client_id:secret:token@login.salesforce.com"; let result = SalesforceCredentials::sfdx_url(url); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), SalesforceAuthError::InvalidSfdxUrl)); } #[test] fn test_sfdx_url_with_special_characters() { let url = "force://client.id-123:secret_456:refresh-token.value@test-env.salesforce.com"; let result = SalesforceCredentials::sfdx_url(url); assert!(result.is_ok()); let credentials = result.unwrap(); assert_eq!(credentials.client_id, Some("client.id-123".to_string())); assert_eq!(credentials.client_secret, Some("secret_456".to_string())); assert_eq!(credentials.refresh_token, Some("refresh-token.value".to_string())); assert_eq!(credentials.login_url, Some("https://test-env.salesforce.com".to_string())); } }