stuff
This commit is contained in:
+145
-32
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user