Files
rustsf_auth/src/credentials/sfdx_auth_url.rs
T
2026-09-21 21:19:02 +01:00

222 lines
6.9 KiB
Rust

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<String>) -> Result<Self, SalesforceAuthError> {
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<SalesforceTokenResponse, SalesforceAuthError> {
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::<SalesforceTokenResponse>()
.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()));
}
}