SFDX Auth url completed

This commit is contained in:
Wim Velzeboer
2026-09-22 11:50:59 +01:00
parent 88ad6d3e3e
commit 120dd21233
13 changed files with 3427 additions and 56 deletions
+1
View File
@@ -0,0 +1 @@
/target
Generated
+3189
View File
File diff suppressed because it is too large Load Diff
+13 -1
View File
@@ -1,11 +1,23 @@
[package] [package]
name = "rustsf_auth" name = "rustsf_auth"
version = "0.1.0" version = "0.1.0"
authors = ["Wim Velzeboer <wim@velzeboer.nl>"]
license = "MIT"
description = "Salesforce OAuth2 authentication SDK for Rust, supporting JWT, sfdxAuthUrl, ClientCredentials and Web"
documentation = "https://docs.rs/rustsf"
homepage = "https://github.com/wimvelzeboer/rustsf"
repository = "https://github.com/wimvelzeboer/rustsf"
readme = "README.md"
keywords = ["Salesforce", "API", "sdk", "sfdx", "REST"]
categories = ["web-programming", "web-programming::http-client", "database", "development-tools::build-utils"]
edition = "2024" edition = "2024"
rust-version = "1.93.0"
[dependencies] [dependencies]
anyhow = "1.0.104" anyhow = "1.0.104"
base64 = "0.22" base64 = "0.22"
env_logger = "0.11.11"
log = "0.4.34"
oauth2 = { version = "5", features = ["reqwest"] } oauth2 = { version = "5", features = ["reqwest"] }
regex = "1" regex = "1"
reqwest = { version = "0.12", features = ["json", "rustls-tls"] } reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
@@ -15,8 +27,8 @@ serde_json = "1"
sha2 = { version = "0.10", features = ["oid"] } sha2 = { version = "0.10", features = ["oid"] }
thiserror = "2" thiserror = "2"
time = "0.3" time = "0.3"
url = "2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
url = "2.5.8"
[dev-dependencies] [dev-dependencies]
httpmock = "0.7" httpmock = "0.7"
+2 -3
View File
@@ -14,10 +14,9 @@
//! - Export the SFDX Auth url from the CLI //! - Export the SFDX Auth url from the CLI
//! ```bash //! ```bash
//! sf org auth show-sfdx-auth-url --target-org my-org-alias //! sf org auth show-sfdx-auth-url --target-org my-org-alias
//! # or
//! sf org auth show-sfdx-auth-url --target-org my-org-alias --json > sfdx_auth_url.json
//! ``` //! ```
//! //! - Copy the sfdxAuthUrl and import it into your application in a save and secure manner,
//! and never hardcode it! (like in the example below)
use oauth2::http::header::AUTHORIZATION; use oauth2::http::header::AUTHORIZATION;
use oauth2::http::{HeaderMap, HeaderValue}; use oauth2::http::{HeaderMap, HeaderValue};
use rustsf_auth::credentials::SalesforceCredentials; use rustsf_auth::credentials::SalesforceCredentials;
+64
View File
@@ -0,0 +1,64 @@
//! RustSF Authentication library - SFDX Auth URL Example
//!
//! In this example, we demonstrate how to use the RustSF Authentication library to authenticate
//! with Salesforce using an SFDX auth URL.
//! The example includes the necessary imports, constants, and a main function that performs the
//! authentication and makes a REST API request.
//!
//! To create a SFDX Auth url, follow these steps:
//! - Download and install the SFDX CLI
//! - [Authenticate a Salesforce environment with the SFDX CLI](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_org_login_web.html)
//! ```bash
//! sf org login web --alias my-org-alias
//! ```
//! - Export the SFDX Auth url from the CLI
//! ```bash
//! sf org auth show-sfdx-auth-url --target-org my-org-alias --json > sfdx_auth_url.json
//! ```
//! - Copy the sfdxAuthUrl and import it into your application in a save and secure manner,
//! and never hardcode it! (like in the example below)
use oauth2::http::header::AUTHORIZATION;
use oauth2::http::{HeaderMap, HeaderValue};
use rustsf_auth::{SalesforceCredentials, SfdxAuthJson};
use serde::Deserialize;
use std::fs;
pub const CONNECT_TIMEOUT: u64 = 15;
pub const REQUEST_TIMEOUT: u64 = 30;
#[tokio::main]
async fn main(){
// Load the sfdx auth url
let file = fs::File::open("my-org-alias.json")?;
let sfdx_auth_json: SfdxAuthJson = serde_json::from_reader(file)?;
// The Credentials configuration
let config = SalesforceCredentials::sfdx_url_json(sfdx_auth_json).unwrap();
// Constructing the authentication session and connecting to Salesforce
let session = config.connect().await.unwrap();
// Build the headers to include the access token
let mut headers = HeaderMap::new();
let auth_value = format!("Bearer {}", session.access_token().await.unwrap());
headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value).unwrap());
// A REST API request
let response = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.connect_timeout(std::time::Duration::from_secs(CONNECT_TIMEOUT))
.timeout(std::time::Duration::from_secs(REQUEST_TIMEOUT))
.build()
.unwrap()
.get(format!("{}/services/data", session.instance_url))
.headers(headers.clone())
.send()
.await
.unwrap();
if response.status().is_success() {
println!("SUCCESS Response: {:?}", response.text().await.unwrap());
} else {
println!("ERROR Response: {:?}", response.text().await.unwrap());
}
}
+2
View File
@@ -0,0 +1,2 @@
max_width = 120
hard_tabs = true
+23 -12
View File
@@ -1,3 +1,4 @@
use log::trace;
use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials}; use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials};
use crate::salesforce_token_response::SalesforceTokenResponse; use crate::salesforce_token_response::SalesforceTokenResponse;
use crate::SalesforceAuthError; use crate::SalesforceAuthError;
@@ -89,17 +90,27 @@ impl SalesforceCredentials {
let client_id = required(self.client_id.as_deref(), "client_id")?; let client_id = required(self.client_id.as_deref(), "client_id")?;
let client_secret = required(self.client_secret.as_deref(), "client_secret")?; let client_secret = required(self.client_secret.as_deref(), "client_secret")?;
Ok(http_client()? let url = self.token_url()?.url().clone();
.post(self.token_url()?.url().clone()) let data = &[
.form(&[ ("grant_type", "client_credentials"),
("grant_type", "client_credentials"), ("client_id", client_id),
("client_id", client_id), ("client_secret", client_secret),
("client_secret", client_secret), ];
]) trace!("Client Credentials auth request: POST {}", url);
.send()
.await? let response = http_client()?
.error_for_status()? .post(url)
.json::<SalesforceTokenResponse>() .form(data)
.await?) .send()
.await?;
let status = response.status();
let body = response.text().await.map_err(|e| SalesforceAuthError::Http(e))?;
trace!("Client Credentials auth response: {}", body);
if !status.is_success() { return Err(SalesforceAuthError::OAuth2(body)) }
Ok(serde_json::from_str::<SalesforceTokenResponse>(&body)
.map_err(|e| SalesforceAuthError::TokenExchange(e.to_string()))?)
} }
} }
+28 -20
View File
@@ -1,5 +1,6 @@
use std::sync::RwLock; use std::sync::RwLock;
use std::time::Duration; use std::time::Duration;
use log::trace;
use oauth2::TokenUrl; use oauth2::TokenUrl;
use reqwest::Client; use reqwest::Client;
use crate::{SalesforceAuthError, SalesforceAuthSession, SalesforceAuthToken}; use crate::{SalesforceAuthError, SalesforceAuthSession, SalesforceAuthToken};
@@ -8,7 +9,7 @@ use crate::salesforce_token_response::SalesforceTokenResponse;
mod access_token; mod access_token;
mod client_credentials; mod client_credentials;
mod jwt_bearer; mod jwt_bearer;
mod sfdx_auth_url; pub(crate) mod sfdx_auth_url;
/// Supported Salesforce OAuth authentication flows. /// Supported Salesforce OAuth authentication flows.
/// ///
@@ -170,28 +171,35 @@ impl SalesforceCredentials {
pub(crate) async fn refresh(&self) -> Result<SalesforceAuthToken, SalesforceAuthError> { pub(crate) async fn refresh(&self) -> Result<SalesforceAuthToken, SalesforceAuthError> {
match &self.refresh_token { match &self.refresh_token {
Some(refresh_token) => {
let client_id = required(self.client_id.as_deref(), "client_id")?;
let response = Client::new()
.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?;
Ok(SalesforceAuthToken::from_token_response(response))
},
None => { None => {
self.reconnect().await self.reconnect().await
} }
Some(refresh_token) => {
let client_id = required(self.client_id.as_deref(), "client_id")?;
let url = self.token_url()?.url().clone();
let data = &[
("grant_type", "refresh_token"),
("client_id", client_id),
("refresh_token", refresh_token),
];
trace!("Token exchange request: POST {} : {:?}", url, data);
let response = Client::new()
.post(url)
.form(data)
.send()
.await?;
let status = response.status();
let body = response.text().await.map_err(|e| SalesforceAuthError::Http(e))?;
trace!("Token exchange response: {}", body);
if !status.is_success() { return Err(SalesforceAuthError::OAuth2(body)) }
Ok(serde_json::from_str::<SalesforceAuthToken>(&body)
.map_err(|e| SalesforceAuthError::TokenExchange(e.to_string()))?)
},
} }
} }
+87 -12
View File
@@ -1,8 +1,30 @@
use log::trace;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials}; use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials};
use crate::salesforce_auth_token::SalesforceAuthToken;
use crate::SalesforceAuthError; use crate::SalesforceAuthError;
use crate::salesforce_token_response::SalesforceTokenResponse; use crate::salesforce_token_response::SalesforceTokenResponse;
/// Struct used to deserialize the SFDX auth URL json file,
/// which can be created via:.
/// ```bash
/// sf org auth show-sfdx-auth-url --target-org $ORG-ALIAS --json > sfdx_auth_url.json
/// ```
#[derive(Deserialize, Serialize)]
pub struct SfdxAuthJson {
pub status: String,
pub result: SfdxAuthJsonResult,
pub warnings: Vec<String>,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SfdxAuthJsonResult {
pub sfdx_auth_url: String,
}
impl SalesforceCredentials { impl SalesforceCredentials {
/// Creates a configuration for authenticating from an SFDX auth URL. /// Creates a configuration for authenticating from an SFDX auth URL.
@@ -72,6 +94,26 @@ impl SalesforceCredentials {
}) })
} }
/// Creates a configuration for authenticating from an SFDX auth URL.
///
/// # Example
///
/// Export the SFDX auth URL in a json via:
/// ```bash
/// sf org auth show-sfdx-auth-url --target-org $ORG-ALIAS --json > sfdx_auth_url.json
/// ```
/// Then use the generated json file in your application
/// ```rust
/// use rustsf_auth::{SalesforceCredentials, SfdxAuthJson};
///
/// let file = fs::File::open("sfdx_auth_url.json")?;
/// let sfdx_auth_json: SfdxAuthJson = serde_json::from_reader(file)?;
/// let session = config.connect().await.unwrap();
/// ```
pub fn sfdx_url_json(sfdx_auth_json: SfdxAuthJson) -> Result<Self, SalesforceAuthError> {
SalesforceCredentials::sfdx_url(sfdx_auth_json.result.sfdx_auth_url)
}
/// Authenticates to Salesforce from an SFDX auth URL. /// Authenticates to Salesforce from an SFDX auth URL.
/// ///
/// The SFDX URL must match: /// The SFDX URL must match:
@@ -112,19 +154,27 @@ impl SalesforceCredentials {
let client_id = required(self.client_id.as_deref(), "client_id")?; let client_id = required(self.client_id.as_deref(), "client_id")?;
let refresh_token = required(self.refresh_token.as_deref(), "refresh_token")?; let refresh_token = required(self.refresh_token.as_deref(), "refresh_token")?;
Ok(http_client()? let url = self.token_url()?.url().clone();
.post(self.token_url()?.url().clone()) let data = &[
.form(&[ ("grant_type", "refresh_token"),
("grant_type", "refresh_token"), ("client_id", client_id),
("client_id", client_id), ("refresh_token", refresh_token),
("refresh_token", refresh_token), ];
// ("client_secret", client_secret.unwrap_or_default()), trace!("SFDX authentication request: POST {} : {:?}", url, data);
]) let response = http_client()?
.post(url)
.form(data)
.send() .send()
.await? .await?;
.error_for_status()?
.json::<SalesforceTokenResponse>() let status = response.status();
.await?) let body = response.text().await.map_err(|e| SalesforceAuthError::Http(e))?;
trace!("SFDX authentication response: {}", body);
if !status.is_success() { return Err(SalesforceAuthError::OAuth2(body)) }
Ok(serde_json::from_str::<SalesforceTokenResponse>(&body)
.map_err(|e| SalesforceAuthError::TokenExchange(e.to_string()))?)
} }
} }
@@ -132,6 +182,31 @@ impl SalesforceCredentials {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn test_sfdx_url_json_valid() {
let sfdx_auth_json = SfdxAuthJson {
status: "0".to_string(),
result: SfdxAuthJsonResult {
sfdx_auth_url: "force://my_client_id:my_client_secret:my_refresh_token@login.salesforce.com".to_string(),
},
warnings: vec![],
};
let result = SalesforceCredentials::sfdx_url_json(sfdx_auth_json);
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] #[test]
fn test_sfdx_url_valid_with_client_secret() { fn test_sfdx_url_valid_with_client_secret() {
let url = "force://my_client_id:my_client_secret:my_refresh_token@login.salesforce.com"; let url = "force://my_client_id:my_client_secret:my_refresh_token@login.salesforce.com";
+3
View File
@@ -36,6 +36,9 @@ pub enum SalesforceAuthError {
#[error("HTTP error: {0}")] #[error("HTTP error: {0}")]
Http(#[from] reqwest::Error), Http(#[from] reqwest::Error),
#[error("Token exchange failure: {0}")]
TokenExchange(String),
/// Error creating a Salesforce JWT assertion. /// Error creating a Salesforce JWT assertion.
#[error("JWT error: {0}")] #[error("JWT error: {0}")]
Jwt(String), Jwt(String),
+3 -1
View File
@@ -5,10 +5,12 @@ pub mod error;
pub(crate) mod salesforce_auth_token; pub(crate) mod salesforce_auth_token;
pub mod salesforce_token_response; pub mod salesforce_token_response;
use self::credentials::SalesforceCredentials;
use self::error::SalesforceAuthError; use self::error::SalesforceAuthError;
use self::salesforce_auth_token::SalesforceAuthToken; use self::salesforce_auth_token::SalesforceAuthToken;
pub use self::credentials::SalesforceCredentials;
pub use self::credentials::sfdx_auth_url::SfdxAuthJson;
/// 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.
+10 -6
View File
@@ -1,5 +1,5 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::LevelFilter;
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue}; use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
use rustsf_auth::credentials::SalesforceCredentials; use rustsf_auth::credentials::SalesforceCredentials;
@@ -18,6 +18,10 @@ pub fn get_http_client() -> Result<reqwest::Client> {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
println!("Hello, world!"); println!("Hello, world!");
env_logger::builder()
.filter_level(LevelFilter::Trace)
.format_target(false)
.init();
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";
@@ -39,15 +43,15 @@ async fn main() {
/// # Ok(()) /// # Ok(())
// SFDX AUTH URL // SFDX AUTH URL
let config = SalesforceCredentials::sfdx_url(sfdx_auth_url).unwrap(); // let config = SalesforceCredentials::sfdx_url(sfdx_auth_url).unwrap();
/* // /*
// CLIENT CREDENTIALS // CLIENT CREDENTIALS
let config = SalesforceCredentials::client_credentials( let config = SalesforceCredentials::client_credentials(
"https://computing-platform-9537--qa.sandbox.my.salesforce.com", "https://computing-platform-9537--qa.sandbox.my.salesforce.com",
customer_id, customer_id,
customer_secret, customer_secret,
); );
*/ // */
println!("Config: {:?}", config); println!("Config: {:?}", config);
let session = config.connect().await.unwrap(); let session = config.connect().await.unwrap();
@@ -74,7 +78,7 @@ async fn main() {
} else { } else {
println!("ERROR Response: {:?}", response.text().await.unwrap()); println!("ERROR Response: {:?}", response.text().await.unwrap());
} }
/*
session.refresh_access_token().await.unwrap(); session.refresh_access_token().await.unwrap();
let auth_value = format!("Bearer {}", session.access_token().await.unwrap()); let auth_value = format!("Bearer {}", session.access_token().await.unwrap());
println!("token: {}", auth_value); println!("token: {}", auth_value);
@@ -92,5 +96,5 @@ async fn main() {
} else { } else {
println!("ERROR Response 2: {:?}", response.text().await.unwrap()); println!("ERROR Response 2: {:?}", response.text().await.unwrap());
} }
*/
} }
+2 -1
View File
@@ -1,7 +1,8 @@
use crate::salesforce_token_response::SalesforceTokenResponse; use crate::salesforce_token_response::SalesforceTokenResponse;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::Deserialize;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
pub struct SalesforceAuthToken { pub struct SalesforceAuthToken {
pub(crate) access_token: String, pub(crate) access_token: String,