stuff
This commit is contained in:
+160
-47
@@ -10,6 +10,7 @@ use rsa::pkcs8::DecodePrivateKey;
|
|||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use rsa::pkcs1v15::SigningKey;
|
use rsa::pkcs1v15::SigningKey;
|
||||||
|
use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
use rsa::signature::{RandomizedSigner, SignatureEncoding};
|
use rsa::signature::{RandomizedSigner, SignatureEncoding};
|
||||||
use rsa::RsaPrivateKey;
|
use rsa::RsaPrivateKey;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -19,6 +20,7 @@ use thiserror::Error;
|
|||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
|
||||||
/// The default Salesforce production login URL.
|
/// The default Salesforce production login URL.
|
||||||
///
|
///
|
||||||
/// Use this value when authenticating against a production Salesforce org.
|
/// 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";
|
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.
|
/// Supported Salesforce OAuth authentication flows.
|
||||||
///
|
///
|
||||||
/// This enum is used by [`SalesforceAuthConfig`] to decide which authentication
|
/// This enum is used by [`SalesforceAuthConfig`] to decide which authentication
|
||||||
@@ -306,7 +310,8 @@ impl SalesforceAuthConfig {
|
|||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn connect(&self) -> Result<SalesforceAuthSession, SalesforceAuthError> {
|
pub async fn connect(&self) -> Result<SalesforceAuthSession, SalesforceAuthError> {
|
||||||
match self.flow {
|
println!("connect");
|
||||||
|
let result = match self.flow {
|
||||||
SalesforceAuthFlow::ClientCredentials => {
|
SalesforceAuthFlow::ClientCredentials => {
|
||||||
authenticate_client_credentials(
|
authenticate_client_credentials(
|
||||||
required(self.login_url.as_deref(), "login_url")?,
|
required(self.login_url.as_deref(), "login_url")?,
|
||||||
@@ -335,8 +340,12 @@ impl SalesforceAuthConfig {
|
|||||||
self.client_secret.clone(),
|
self.client_secret.clone(),
|
||||||
self.login_url.clone(),
|
self.login_url.clone(),
|
||||||
)),
|
)),
|
||||||
}
|
};
|
||||||
|
println!("connect: {:?}", result);
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Raw OAuth token response returned by Salesforce.
|
/// Raw OAuth token response returned by Salesforce.
|
||||||
@@ -371,11 +380,11 @@ struct SalesforceTokenResponse {
|
|||||||
/// None,
|
/// 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 struct SalesforceAuthSession {
|
||||||
pub access_token: String,
|
token: RwLock<SalesforceAuthToken>,
|
||||||
|
|
||||||
/// Salesforce instance URL associated with the authenticated org.
|
/// Salesforce instance URL associated with the authenticated org.
|
||||||
pub instance_url: String,
|
pub instance_url: String,
|
||||||
@@ -383,15 +392,6 @@ pub struct SalesforceAuthSession {
|
|||||||
/// Salesforce identity service URL, when returned by Salesforce.
|
/// Salesforce identity service URL, when returned by Salesforce.
|
||||||
pub id: Option<String>,
|
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.
|
/// OAuth refresh token, when available.
|
||||||
pub refresh_token: Option<String>,
|
pub refresh_token: Option<String>,
|
||||||
|
|
||||||
@@ -405,7 +405,59 @@ pub struct SalesforceAuthSession {
|
|||||||
pub login_url: Option<String>,
|
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 {
|
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.
|
/// Refreshes this session's access token.
|
||||||
///
|
///
|
||||||
/// The session must contain `login_url`, `client_id`, and `refresh_token`.
|
/// The session must contain `login_url`, `client_id`, and `refresh_token`.
|
||||||
@@ -438,14 +490,40 @@ impl SalesforceAuthSession {
|
|||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn refresh_access_token(&self) -> Result<SalesforceAuthSession, SalesforceAuthError> {
|
pub async fn refresh_access_token(&self) -> Result<(), SalesforceAuthError> {
|
||||||
refresh_access_token(
|
let refreshed = refresh_access_token(
|
||||||
required(self.login_url.as_deref(), "login_url")?,
|
required(self.login_url.as_deref(), "login_url")?,
|
||||||
required(self.client_id.as_deref(), "client_id")?,
|
required(self.client_id.as_deref(), "client_id")?,
|
||||||
self.client_secret.as_deref(),
|
self.client_secret.as_deref(),
|
||||||
required(self.refresh_token.as_deref(), "refresh_token")?,
|
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(())
|
/// # 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(
|
pub async fn authenticate_client_credentials(
|
||||||
login_url: &str,
|
login_url: &str,
|
||||||
client_id: &str,
|
client_id: &str,
|
||||||
client_secret: &str,
|
client_secret: &str,
|
||||||
) -> Result<SalesforceAuthSession, SalesforceAuthError> {
|
) -> 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()
|
let http_client = Client::builder()
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
.timeout(Duration::from_secs(30))
|
.timeout(Duration::from_secs(30))
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let token_url = TokenUrl::new(format!(
|
let token_url = TokenUrl::new(format!(
|
||||||
"{}/services/oauth2/token",
|
"{}/services/oauth2/token",
|
||||||
login_url.trim_end_matches('/')
|
login_url.trim_end_matches('/')
|
||||||
|
// "http://localhost:3000"
|
||||||
))
|
))
|
||||||
.map_err(|error| SalesforceAuthError::OAuth2(error.to_string()))?;
|
.map_err(|error| SalesforceAuthError::OAuth2(error.to_string()))?;
|
||||||
|
|
||||||
|
|
||||||
let oauth_client = BasicClient::new(ClientId::new(client_id.to_string()))
|
let oauth_client = BasicClient::new(ClientId::new(client_id.to_string()))
|
||||||
.set_client_secret(ClientSecret::new(client_secret.to_string()))
|
.set_client_secret(ClientSecret::new(client_secret.to_string()))
|
||||||
.set_token_uri(token_url);
|
.set_token_uri(token_url);
|
||||||
@@ -579,29 +683,30 @@ pub async fn authenticate_client_credentials(
|
|||||||
let token_response: StandardTokenResponse<oauth2::EmptyExtraTokenFields, BasicTokenType> =
|
let token_response: StandardTokenResponse<oauth2::EmptyExtraTokenFields, BasicTokenType> =
|
||||||
oauth_client
|
oauth_client
|
||||||
.exchange_client_credentials()
|
.exchange_client_credentials()
|
||||||
.add_scope(Scope::new("api".to_string()))
|
// .add_scope(Scope::new("api".to_string()))
|
||||||
.request_async(&http_client)
|
.request_async(&http_client)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| SalesforceAuthError::OAuth2(error.to_string()))?;
|
.map_err(|error| SalesforceAuthError::OAuth2(error.to_string()))?;
|
||||||
|
|
||||||
let access_token = token_response.access_token().secret().to_string();
|
let access_token = token_response.access_token().secret().to_string();
|
||||||
|
|
||||||
fetch_salesforce_token_response_from_access_token(login_url, &access_token)
|
println!("asfasf {}", client_id);
|
||||||
.await
|
|
||||||
.or_else(|_| {
|
|
||||||
Ok(SalesforceAuthSession {
|
Ok(SalesforceAuthSession {
|
||||||
access_token,
|
token: RwLock::new(SalesforceAuthToken {
|
||||||
instance_url: login_url.trim_end_matches('/').to_string(),
|
access_token: "asdf".to_string(),
|
||||||
id: None,
|
token_type: Some("Bearer".to_string()),
|
||||||
token_type: Some("Bearer".to_string()),
|
issued_at: None,
|
||||||
issued_at: None,
|
signature: None,
|
||||||
signature: None,
|
}),
|
||||||
refresh_token: None,
|
instance_url: login_url.trim_end_matches('/').to_string(),
|
||||||
client_id: Some(client_id.to_string()),
|
id: None,
|
||||||
client_secret: Some(client_secret.to_string()),
|
refresh_token: None,
|
||||||
login_url: Some(login_url.to_string()),
|
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.
|
/// Authenticates to Salesforce using the JWT bearer flow.
|
||||||
@@ -680,12 +785,14 @@ fn to_session(
|
|||||||
.ok_or(SalesforceAuthError::MissingRequiredField("instance_url"))?;
|
.ok_or(SalesforceAuthError::MissingRequiredField("instance_url"))?;
|
||||||
|
|
||||||
Ok(SalesforceAuthSession {
|
Ok(SalesforceAuthSession {
|
||||||
access_token: response.access_token,
|
token: RwLock::new(SalesforceAuthToken {
|
||||||
|
access_token: response.access_token,
|
||||||
|
token_type: response.token_type,
|
||||||
|
issued_at: response.issued_at,
|
||||||
|
signature: response.signature,
|
||||||
|
}),
|
||||||
instance_url,
|
instance_url,
|
||||||
id: response.id,
|
id: response.id,
|
||||||
token_type: response.token_type,
|
|
||||||
issued_at: response.issued_at,
|
|
||||||
signature: response.signature,
|
|
||||||
refresh_token: response.refresh_token.or(refresh_token),
|
refresh_token: response.refresh_token.or(refresh_token),
|
||||||
client_id,
|
client_id,
|
||||||
client_secret,
|
client_secret,
|
||||||
@@ -724,12 +831,14 @@ pub fn authenticate_access_token(
|
|||||||
login_url: Option<String>,
|
login_url: Option<String>,
|
||||||
) -> SalesforceAuthSession {
|
) -> SalesforceAuthSession {
|
||||||
SalesforceAuthSession {
|
SalesforceAuthSession {
|
||||||
access_token: access_token.to_string(),
|
token: RwLock::new(SalesforceAuthToken {
|
||||||
|
access_token: access_token.to_string(),
|
||||||
|
token_type: Some("Bearer".to_string()),
|
||||||
|
issued_at: None,
|
||||||
|
signature: None,
|
||||||
|
}),
|
||||||
instance_url: instance_url.trim_end_matches('/').to_string(),
|
instance_url: instance_url.trim_end_matches('/').to_string(),
|
||||||
id: None,
|
id: None,
|
||||||
token_type: Some("Bearer".to_string()),
|
|
||||||
issued_at: None,
|
|
||||||
signature: None,
|
|
||||||
refresh_token,
|
refresh_token,
|
||||||
client_id,
|
client_id,
|
||||||
client_secret,
|
client_secret,
|
||||||
@@ -1017,12 +1126,14 @@ async fn fetch_salesforce_token_response_from_access_token(
|
|||||||
access_token: &str,
|
access_token: &str,
|
||||||
) -> Result<SalesforceAuthSession, SalesforceAuthError> {
|
) -> Result<SalesforceAuthSession, SalesforceAuthError> {
|
||||||
Ok(SalesforceAuthSession {
|
Ok(SalesforceAuthSession {
|
||||||
access_token: access_token.to_string(),
|
token: RwLock::new(SalesforceAuthToken {
|
||||||
|
access_token: access_token.to_string(),
|
||||||
|
token_type: Some("Bearer".to_string()),
|
||||||
|
issued_at: None,
|
||||||
|
signature: None,
|
||||||
|
}),
|
||||||
instance_url: login_url.trim_end_matches('/').to_string(),
|
instance_url: login_url.trim_end_matches('/').to_string(),
|
||||||
id: None,
|
id: None,
|
||||||
token_type: Some("Bearer".to_string()),
|
|
||||||
issued_at: None,
|
|
||||||
signature: None,
|
|
||||||
refresh_token: None,
|
refresh_token: None,
|
||||||
client_id: None,
|
client_id: None,
|
||||||
client_secret: None,
|
client_secret: None,
|
||||||
@@ -1035,6 +1146,8 @@ fn required<'a>(
|
|||||||
value: Option<&'a str>,
|
value: Option<&'a str>,
|
||||||
field_name: &'static str,
|
field_name: &'static str,
|
||||||
) -> Result<&'a str, SalesforceAuthError> {
|
) -> Result<&'a str, SalesforceAuthError> {
|
||||||
|
println!("Checking {:?} {:?}", value, field_name);
|
||||||
|
|
||||||
value.ok_or(SalesforceAuthError::MissingRequiredField(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 url = "https://computing-platform-9537--qa.sandbox.my.salesforce.com/";
|
||||||
let customer_id = "3MVG9xj60O9CjKHpQMaI0gAqe_BibSxfGHYQBII24Bif2Nri7ewsMsz3kEbMr4vch36.q4I.B37EpwOOtg90e";
|
let customer_id = "3MVG9xj60O9CjKHpQMaI0gAqe_BibSxfGHYQBII24Bif2Nri7ewsMsz3kEbMr4vch36.q4I.B37EpwOOtg90e";
|
||||||
let customer_secret = "68182180245EEBC7C54804536914BBEA32C0C347D83A67EB43A6225D342C95B9";
|
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";
|
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);
|
/// println!("{}", session.access_token);
|
||||||
/// # Ok(())
|
/// # 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();
|
let session = config.connect().await.unwrap();
|
||||||
|
|
||||||
println!("{}", session.access_token);
|
println!("token: {}", session.access_token().await.unwrap());
|
||||||
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
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());
|
headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value).unwrap());
|
||||||
|
|
||||||
//Default header
|
//Default header
|
||||||
|
|||||||
Reference in New Issue
Block a user