222 lines
6.9 KiB
Rust
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()));
|
|
}
|
|
}
|
|
|
|
|
|
|