This commit is contained in:
Wim Velzeboer
2026-09-21 13:45:30 +01:00
parent ef57ba0da9
commit e92bec2e70
2 changed files with 172 additions and 50 deletions
+145 -32
View File
@@ -10,6 +10,7 @@ use rsa::pkcs8::DecodePrivateKey;
use regex::Regex;
use reqwest::Client;
use rsa::pkcs1v15::SigningKey;
use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use rsa::signature::{RandomizedSigner, SignatureEncoding};
use rsa::RsaPrivateKey;
use serde::{Deserialize, Serialize};
@@ -19,6 +20,7 @@ use thiserror::Error;
use time::OffsetDateTime;
use url::Url;
/// The default Salesforce production login URL.
///
/// Use this value when authenticating against a production Salesforce org.
@@ -32,6 +34,8 @@ use url::Url;
/// ```
pub const DEFAULT_SALESFORCE_LOGIN_URL: &str = "https://login.salesforce.com";
const DEFAULT_ACCESS_TOKEN_TTL_SECONDS: i64 = 2 * 60 * 60;
/// Supported Salesforce OAuth authentication flows.
///
/// This enum is used by [`SalesforceAuthConfig`] to decide which authentication
@@ -306,7 +310,8 @@ impl SalesforceAuthConfig {
/// # }
/// ```
pub async fn connect(&self) -> Result<SalesforceAuthSession, SalesforceAuthError> {
match self.flow {
println!("connect");
let result = match self.flow {
SalesforceAuthFlow::ClientCredentials => {
authenticate_client_credentials(
required(self.login_url.as_deref(), "login_url")?,
@@ -335,8 +340,12 @@ impl SalesforceAuthConfig {
self.client_secret.clone(),
self.login_url.clone(),
)),
};
println!("connect: {:?}", result);
result
}
}
}
/// Raw OAuth token response returned by Salesforce.
@@ -371,11 +380,11 @@ struct SalesforceTokenResponse {
/// None,
/// );
///
/// assert_eq!(session.access_token, "access-token");
/// assert_eq!(session.access_token(), "access-token");
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
pub struct SalesforceAuthSession {
pub access_token: String,
token: RwLock<SalesforceAuthToken>,
/// Salesforce instance URL associated with the authenticated org.
pub instance_url: String,
@@ -383,15 +392,6 @@ pub struct SalesforceAuthSession {
/// Salesforce identity service URL, when returned by Salesforce.
pub id: Option<String>,
/// OAuth token type, usually `Bearer`.
pub token_type: Option<String>,
/// Token issue timestamp, when returned by Salesforce.
pub issued_at: Option<String>,
/// Salesforce response signature, when returned by Salesforce.
pub signature: Option<String>,
/// OAuth refresh token, when available.
pub refresh_token: Option<String>,
@@ -405,7 +405,59 @@ pub struct SalesforceAuthSession {
pub login_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct SalesforceAuthToken {
access_token: String,
/// OAuth token type, usually `Bearer`.
token_type: Option<String>,
/// Token issue timestamp, when returned by Salesforce.
issued_at: Option<String>,
/// Salesforce response signature, when returned by Salesforce.
signature: Option<String>,
}
impl SalesforceAuthSession {
fn token_read(&self) -> RwLockReadGuard<'_, SalesforceAuthToken> {
self.token
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn token_write(&self) -> RwLockWriteGuard<'_, SalesforceAuthToken> {
self.token
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn token(&self) -> SalesforceAuthToken {
self.token_read().clone()
}
pub async fn access_token(&self) -> Result<String, SalesforceAuthError> {
if self.is_access_token_expired() {
self.refresh_access_token().await?;
}
Ok(self.token().access_token)
}
pub fn token_type(&self) -> Option<String> {
self.token().token_type
}
pub fn issued_at(&self) -> Option<String> {
self.token().issued_at
}
pub fn signature(&self) -> Option<String> {
self.token().signature
}
/// Refreshes this session's access token.
///
/// The session must contain `login_url`, `client_id`, and `refresh_token`.
@@ -438,14 +490,40 @@ impl SalesforceAuthSession {
/// # Ok(())
/// # }
/// ```
pub async fn refresh_access_token(&self) -> Result<SalesforceAuthSession, SalesforceAuthError> {
refresh_access_token(
pub async fn refresh_access_token(&self) -> Result<(), SalesforceAuthError> {
let refreshed = refresh_access_token(
required(self.login_url.as_deref(), "login_url")?,
required(self.client_id.as_deref(), "client_id")?,
self.client_secret.as_deref(),
required(self.refresh_token.as_deref(), "refresh_token")?,
)
.await
.await?;
*self.token_write() = refreshed.token();
Ok(())
}
/// Returns `true` when this session's access token should be refreshed.
///
/// If `issued_at` is missing or cannot be parsed, the token is treated as
/// expired so callers fail safe and obtain a fresh token.
fn is_access_token_expired(&self) -> bool {
let token = self.token();
let Some(issued_at) = token.issued_at.as_deref() else {
return true;
};
let Ok(issued_at_millis) = issued_at.parse::<i128>() else {
return true;
};
let issued_at_seconds = issued_at_millis / 1_000;
let expires_at = issued_at_seconds + i128::from(DEFAULT_ACCESS_TOKEN_TTL_SECONDS);
let now = i128::from(OffsetDateTime::now_utc().unix_timestamp());
now >= expires_at
}
}
@@ -556,22 +634,48 @@ pub enum SalesforceAuthError {
/// # Ok(())
/// # }
/// ```
///
/// Configure Salesforce
///
/// 1. Create the External Client App
/// - Navigate to Setup > External Client App Manager.
/// - Click New External Client App and enter the app name and contact email.
/// - Expand the API (Enable OAuth Settings) section:
/// - Check Enable OAuth.
/// - Check Enable Client Credentials Flow.
/// - Add the Manage user data via APIs (api) scope. Do not add refresh_token or offline_access as these are invalid for this flow.
/// - Click Create and note the Consumer Key (Client ID) and Consumer Secret.
///
/// 2. Configure Policies and Run As User
/// - In the External Client App Manager, find your new app and click Edit.
/// - Go to the Policies tab.
/// - Under OAuth Flows and External Client App Enhancements:
/// - Ensure Enable Client Credentials Flow is checked.
/// - In the Run As field, select the integration user (a dedicated service account with necessary API permissions).
/// - Save the changes.
///
pub async fn authenticate_client_credentials(
login_url: &str,
client_id: &str,
client_secret: &str,
) -> Result<SalesforceAuthSession, SalesforceAuthError> {
println!("authenticate_client_credentials");
println!("login url {}", login_url);
println!("client Id {}", client_id);
println!("client secret {}", client_secret);
let http_client = Client::builder()
.redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(30))
.build()?;
let token_url = TokenUrl::new(format!(
"{}/services/oauth2/token",
login_url.trim_end_matches('/')
// "http://localhost:3000"
))
.map_err(|error| SalesforceAuthError::OAuth2(error.to_string()))?;
let oauth_client = BasicClient::new(ClientId::new(client_id.to_string()))
.set_client_secret(ClientSecret::new(client_secret.to_string()))
.set_token_uri(token_url);
@@ -579,29 +683,30 @@ pub async fn authenticate_client_credentials(
let token_response: StandardTokenResponse<oauth2::EmptyExtraTokenFields, BasicTokenType> =
oauth_client
.exchange_client_credentials()
.add_scope(Scope::new("api".to_string()))
// .add_scope(Scope::new("api".to_string()))
.request_async(&http_client)
.await
.map_err(|error| SalesforceAuthError::OAuth2(error.to_string()))?;
let access_token = token_response.access_token().secret().to_string();
fetch_salesforce_token_response_from_access_token(login_url, &access_token)
.await
.or_else(|_| {
println!("asfasf {}", client_id);
Ok(SalesforceAuthSession {
access_token,
instance_url: login_url.trim_end_matches('/').to_string(),
id: None,
token: RwLock::new(SalesforceAuthToken {
access_token: "asdf".to_string(),
token_type: Some("Bearer".to_string()),
issued_at: None,
signature: None,
}),
instance_url: login_url.trim_end_matches('/').to_string(),
id: None,
refresh_token: None,
client_id: Some(client_id.to_string()),
client_secret: Some(client_secret.to_string()),
login_url: Some(login_url.to_string()),
})
})
}
/// Authenticates to Salesforce using the JWT bearer flow.
@@ -680,12 +785,14 @@ fn to_session(
.ok_or(SalesforceAuthError::MissingRequiredField("instance_url"))?;
Ok(SalesforceAuthSession {
token: RwLock::new(SalesforceAuthToken {
access_token: response.access_token,
instance_url,
id: response.id,
token_type: response.token_type,
issued_at: response.issued_at,
signature: response.signature,
}),
instance_url,
id: response.id,
refresh_token: response.refresh_token.or(refresh_token),
client_id,
client_secret,
@@ -724,12 +831,14 @@ pub fn authenticate_access_token(
login_url: Option<String>,
) -> SalesforceAuthSession {
SalesforceAuthSession {
token: RwLock::new(SalesforceAuthToken {
access_token: access_token.to_string(),
instance_url: instance_url.trim_end_matches('/').to_string(),
id: None,
token_type: Some("Bearer".to_string()),
issued_at: None,
signature: None,
}),
instance_url: instance_url.trim_end_matches('/').to_string(),
id: None,
refresh_token,
client_id,
client_secret,
@@ -1017,12 +1126,14 @@ async fn fetch_salesforce_token_response_from_access_token(
access_token: &str,
) -> Result<SalesforceAuthSession, SalesforceAuthError> {
Ok(SalesforceAuthSession {
token: RwLock::new(SalesforceAuthToken {
access_token: access_token.to_string(),
instance_url: login_url.trim_end_matches('/').to_string(),
id: None,
token_type: Some("Bearer".to_string()),
issued_at: None,
signature: None,
}),
instance_url: login_url.trim_end_matches('/').to_string(),
id: None,
refresh_token: None,
client_id: None,
client_secret: None,
@@ -1035,6 +1146,8 @@ fn required<'a>(
value: Option<&'a str>,
field_name: &'static str,
) -> Result<&'a str, SalesforceAuthError> {
println!("Checking {:?} {:?}", value, field_name);
value.ok_or(SalesforceAuthError::MissingRequiredField(field_name))
}
+12 -3
View File
@@ -21,6 +21,8 @@ async fn main() {
let url = "https://computing-platform-9537--qa.sandbox.my.salesforce.com/";
let customer_id = "3MVG9xj60O9CjKHpQMaI0gAqe_BibSxfGHYQBII24Bif2Nri7ewsMsz3kEbMr4vch36.q4I.B37EpwOOtg90e";
let customer_secret = "68182180245EEBC7C54804536914BBEA32C0C347D83A67EB43A6225D342C95B9";
let username = "wvelzeb1@its.jnj.com.im-qa";
let password = "pi#TU&4wk!1IzN^N";
let sfdx_auth_url = "force://PlatformCLI::5Aep8618kS0Qqf3GEL4qVTX2VSz2qiUtn3Grl0qSlJjRoSGz03Uv86qbbx2MOhcIyj.OTqFKFH.olbk5LdlYEAv@computing-platform-9537--qa.sandbox.my.salesforce.com";
@@ -35,15 +37,22 @@ async fn main() {
/// println!("{}", session.access_token);
/// # Ok(())
let config = SalesforceAuthConfig::sfdx_url(sfdx_auth_url);
// let config = SalesforceAuthConfig::sfdx_url(sfdx_auth_url);
let config = SalesforceAuthConfig::client_credentials(
"https://computing-platform-9537--qa.sandbox.my.salesforce.com",
customer_id,
customer_secret,
);
println!("Config: {:?}", config);
let session = config.connect().await.unwrap();
println!("{}", session.access_token);
println!("token: {}", session.access_token().await.unwrap());
let mut headers = HeaderMap::new();
let auth_value = format!("Bearer {}", session.access_token);
let auth_value = format!("Bearer {}", session.access_token().await.unwrap());
headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value).unwrap());
//Default header