SFDX Auth url completed
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+3189
File diff suppressed because it is too large
Load Diff
+13
-1
@@ -1,11 +1,23 @@
|
||||
[package]
|
||||
name = "rustsf_auth"
|
||||
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"
|
||||
rust-version = "1.93.0"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.104"
|
||||
base64 = "0.22"
|
||||
env_logger = "0.11.11"
|
||||
log = "0.4.34"
|
||||
oauth2 = { version = "5", features = ["reqwest"] }
|
||||
regex = "1"
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||
@@ -15,8 +27,8 @@ serde_json = "1"
|
||||
sha2 = { version = "0.10", features = ["oid"] }
|
||||
thiserror = "2"
|
||||
time = "0.3"
|
||||
url = "2"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
url = "2.5.8"
|
||||
|
||||
[dev-dependencies]
|
||||
httpmock = "0.7"
|
||||
|
||||
@@ -14,10 +14,9 @@
|
||||
//! - Export the SFDX Auth url from the CLI
|
||||
//! ```bash
|
||||
//! 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::{HeaderMap, HeaderValue};
|
||||
use rustsf_auth::credentials::SalesforceCredentials;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
max_width = 120
|
||||
hard_tabs = true
|
||||
@@ -1,3 +1,4 @@
|
||||
use log::trace;
|
||||
use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials};
|
||||
use crate::salesforce_token_response::SalesforceTokenResponse;
|
||||
use crate::SalesforceAuthError;
|
||||
@@ -89,17 +90,27 @@ impl SalesforceCredentials {
|
||||
let client_id = required(self.client_id.as_deref(), "client_id")?;
|
||||
let client_secret = required(self.client_secret.as_deref(), "client_secret")?;
|
||||
|
||||
Ok(http_client()?
|
||||
.post(self.token_url()?.url().clone())
|
||||
.form(&[
|
||||
("grant_type", "client_credentials"),
|
||||
("client_id", client_id),
|
||||
("client_secret", client_secret),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<SalesforceTokenResponse>()
|
||||
.await?)
|
||||
let url = self.token_url()?.url().clone();
|
||||
let data = &[
|
||||
("grant_type", "client_credentials"),
|
||||
("client_id", client_id),
|
||||
("client_secret", client_secret),
|
||||
];
|
||||
trace!("Client Credentials auth request: POST {}", url);
|
||||
|
||||
let response = http_client()?
|
||||
.post(url)
|
||||
.form(data)
|
||||
.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
@@ -1,5 +1,6 @@
|
||||
use std::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
use log::trace;
|
||||
use oauth2::TokenUrl;
|
||||
use reqwest::Client;
|
||||
use crate::{SalesforceAuthError, SalesforceAuthSession, SalesforceAuthToken};
|
||||
@@ -8,7 +9,7 @@ use crate::salesforce_token_response::SalesforceTokenResponse;
|
||||
mod access_token;
|
||||
mod client_credentials;
|
||||
mod jwt_bearer;
|
||||
mod sfdx_auth_url;
|
||||
pub(crate) mod sfdx_auth_url;
|
||||
|
||||
/// Supported Salesforce OAuth authentication flows.
|
||||
///
|
||||
@@ -170,28 +171,35 @@ impl SalesforceCredentials {
|
||||
pub(crate) async fn refresh(&self) -> Result<SalesforceAuthToken, SalesforceAuthError> {
|
||||
|
||||
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 => {
|
||||
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()))?)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
use log::trace;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials};
|
||||
use crate::salesforce_auth_token::SalesforceAuthToken;
|
||||
use crate::SalesforceAuthError;
|
||||
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 {
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// The SFDX URL must match:
|
||||
@@ -112,19 +154,27 @@ impl SalesforceCredentials {
|
||||
let client_id = required(self.client_id.as_deref(), "client_id")?;
|
||||
let refresh_token = required(self.refresh_token.as_deref(), "refresh_token")?;
|
||||
|
||||
Ok(http_client()?
|
||||
.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()),
|
||||
])
|
||||
let url = self.token_url()?.url().clone();
|
||||
let data = &[
|
||||
("grant_type", "refresh_token"),
|
||||
("client_id", client_id),
|
||||
("refresh_token", refresh_token),
|
||||
];
|
||||
trace!("SFDX authentication request: POST {} : {:?}", url, data);
|
||||
let response = http_client()?
|
||||
.post(url)
|
||||
.form(data)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<SalesforceTokenResponse>()
|
||||
.await?)
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
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 {
|
||||
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]
|
||||
fn test_sfdx_url_valid_with_client_secret() {
|
||||
let url = "force://my_client_id:my_client_secret:my_refresh_token@login.salesforce.com";
|
||||
|
||||
@@ -36,6 +36,9 @@ pub enum SalesforceAuthError {
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
|
||||
#[error("Token exchange failure: {0}")]
|
||||
TokenExchange(String),
|
||||
|
||||
/// Error creating a Salesforce JWT assertion.
|
||||
#[error("JWT error: {0}")]
|
||||
Jwt(String),
|
||||
|
||||
+3
-1
@@ -5,10 +5,12 @@ pub mod error;
|
||||
pub(crate) mod salesforce_auth_token;
|
||||
pub mod salesforce_token_response;
|
||||
|
||||
use self::credentials::SalesforceCredentials;
|
||||
use self::error::SalesforceAuthError;
|
||||
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.
|
||||
///
|
||||
/// Use this value when authenticating against a production Salesforce org.
|
||||
|
||||
+10
-6
@@ -1,5 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use log::LevelFilter;
|
||||
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
|
||||
use rustsf_auth::credentials::SalesforceCredentials;
|
||||
|
||||
@@ -18,6 +18,10 @@ pub fn get_http_client() -> Result<reqwest::Client> {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
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 customer_id = "3MVG9xj60O9CjKHpQMaI0gAqe_BibSxfGHYQBII24Bif2Nri7ewsMsz3kEbMr4vch36.q4I.B37EpwOOtg90e";
|
||||
@@ -39,15 +43,15 @@ async fn main() {
|
||||
/// # Ok(())
|
||||
|
||||
// SFDX AUTH URL
|
||||
let config = SalesforceCredentials::sfdx_url(sfdx_auth_url).unwrap();
|
||||
/*
|
||||
// let config = SalesforceCredentials::sfdx_url(sfdx_auth_url).unwrap();
|
||||
// /*
|
||||
// CLIENT CREDENTIALS
|
||||
let config = SalesforceCredentials::client_credentials(
|
||||
"https://computing-platform-9537--qa.sandbox.my.salesforce.com",
|
||||
customer_id,
|
||||
customer_secret,
|
||||
);
|
||||
*/
|
||||
// */
|
||||
println!("Config: {:?}", config);
|
||||
|
||||
let session = config.connect().await.unwrap();
|
||||
@@ -74,7 +78,7 @@ async fn main() {
|
||||
} else {
|
||||
println!("ERROR Response: {:?}", response.text().await.unwrap());
|
||||
}
|
||||
|
||||
/*
|
||||
session.refresh_access_token().await.unwrap();
|
||||
let auth_value = format!("Bearer {}", session.access_token().await.unwrap());
|
||||
println!("token: {}", auth_value);
|
||||
@@ -92,5 +96,5 @@ async fn main() {
|
||||
} else {
|
||||
println!("ERROR Response 2: {:?}", response.text().await.unwrap());
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::salesforce_token_response::SalesforceTokenResponse;
|
||||
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(crate) access_token: String,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user