use rsa::pkcs8::DecodePrivateKey;
use rsa::RsaPrivateKey;
use url::Url;
use zeroize::Zeroizing;
use crate::error::{SalesforceAuthError, SalesforceAuthResult};
#[derive(Clone)]
pub enum AuthMode {
Password {
username: String,
password: Zeroizing<String>,
},
PrivateKey {
username: String,
private_key: Box<RsaPrivateKey>,
},
RefreshToken {
refresh_token: Zeroizing<String>,
},
}
impl AuthMode {
pub fn password(username: impl Into<String>, password: impl Into<String>) -> Self {
AuthMode::Password {
username: username.into(),
password: Zeroizing::new(password.into()),
}
}
pub fn private_key(
username: impl Into<String>,
private_key_pem: &str,
) -> SalesforceAuthResult<Self> {
let private_key = RsaPrivateKey::from_pkcs8_pem(private_key_pem).map_err(|e| {
SalesforceAuthError::PrivateKey(format!(
"failed to parse private key (expected PKCS#8 PEM format): {e}"
))
})?;
Ok(AuthMode::PrivateKey {
username: username.into(),
private_key: Box::new(private_key),
})
}
pub fn refresh_token(refresh_token: impl Into<String>) -> Self {
AuthMode::RefreshToken {
refresh_token: Zeroizing::new(refresh_token.into()),
}
}
#[must_use]
pub fn username(&self) -> Option<&str> {
match self {
AuthMode::Password { username, .. } => Some(username),
AuthMode::PrivateKey { username, .. } => Some(username),
AuthMode::RefreshToken { .. } => None,
}
}
}
impl std::fmt::Debug for AuthMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthMode::Password { username, .. } => f
.debug_struct("Password")
.field("username", username)
.field("password", &"[REDACTED]")
.finish(),
AuthMode::PrivateKey { username, .. } => f
.debug_struct("PrivateKey")
.field("username", username)
.field("private_key", &"[REDACTED]")
.finish(),
AuthMode::RefreshToken { .. } => f
.debug_struct("RefreshToken")
.field("refresh_token", &"[REDACTED]")
.finish(),
}
}
}
#[derive(Debug, Clone)]
pub struct SalesforceAuthConfig {
pub(crate) login_url: Url,
pub(crate) client_id: String,
pub(crate) client_secret: Option<Zeroizing<String>>,
pub(crate) auth_mode: Option<AuthMode>,
pub(crate) dataspace: Option<String>,
pub(crate) timeout_secs: u64,
pub(crate) max_retries: u32,
}
impl SalesforceAuthConfig {
pub fn new(
login_url: impl AsRef<str>,
client_id: impl Into<String>,
) -> SalesforceAuthResult<Self> {
let login_url = Url::parse(login_url.as_ref())?;
if login_url.scheme() != "https" && login_url.scheme() != "http" {
return Err(SalesforceAuthError::Config(
"login_url must use http or https scheme".to_string(),
));
}
if login_url.host().is_none() {
return Err(SalesforceAuthError::Config(
"login_url must have a host".to_string(),
));
}
Ok(SalesforceAuthConfig {
login_url,
client_id: client_id.into(),
client_secret: None,
auth_mode: None,
dataspace: None,
timeout_secs: 30,
max_retries: 3,
})
}
#[must_use]
pub fn auth_mode(mut self, mode: AuthMode) -> Self {
self.auth_mode = Some(mode);
self
}
#[must_use]
pub fn client_secret(mut self, secret: impl Into<String>) -> Self {
self.client_secret = Some(Zeroizing::new(secret.into()));
self
}
#[must_use]
pub fn dataspace(mut self, dataspace: impl Into<String>) -> Self {
self.dataspace = Some(dataspace.into());
self
}
#[must_use]
pub fn timeout_secs(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
#[must_use]
pub fn max_retries(mut self, retries: u32) -> Self {
self.max_retries = retries;
self
}
#[must_use]
pub fn login_url(&self) -> &Url {
&self.login_url
}
#[must_use]
pub fn client_id(&self) -> &str {
&self.client_id
}
#[must_use]
pub fn dataspace_value(&self) -> Option<&str> {
self.dataspace.as_deref()
}
pub(crate) fn validate(&self) -> SalesforceAuthResult<()> {
let auth_mode = self
.auth_mode
.as_ref()
.ok_or_else(|| SalesforceAuthError::Config("auth_mode is required".to_string()))?;
match auth_mode {
AuthMode::Password { .. } | AuthMode::RefreshToken { .. } => {
if self.client_secret.is_none() {
return Err(SalesforceAuthError::Config(
"client_secret is required for Password and RefreshToken auth modes"
.to_string(),
));
}
}
AuthMode::PrivateKey { .. } => {
if self.client_secret.is_some() {
tracing::warn!(
"client_secret is set but not used for PrivateKey (JWT Bearer) mode"
);
}
}
}
Ok(())
}
}
#[expect(
dead_code,
reason = "retained for upcoming login URL warning surface; keep wired up so it stays compiled"
)]
pub(crate) fn is_known_salesforce_host(host: &str) -> bool {
let patterns = ["login.salesforce.com", "test.salesforce.com"];
let suffix_patterns = [
".my.salesforce.com",
".my.site.com",
".sandbox.my.salesforce.com",
];
if patterns.contains(&host) {
return true;
}
for suffix in suffix_patterns {
if host.ends_with(suffix) {
return true;
}
}
if host.starts_with("login.test") && host.ends_with(".pc-rnd.salesforce.com") {
return true;
}
false
}