Compare commits
10
Commits
ef57ba0da9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3efcb85d6e | ||
|
|
5def68ef61 | ||
|
|
8010d33b11 | ||
|
|
29594990c3 | ||
|
|
120dd21233 | ||
|
|
88ad6d3e3e | ||
|
|
850b3c3ca7 | ||
|
|
512b60d9f4 | ||
|
|
c1c69168d7 | ||
|
|
e92bec2e70 |
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+3189
File diff suppressed because it is too large
Load Diff
+13
-1
@@ -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 Server flow"
|
||||||
|
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"
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
[](https://crates.io/crates/rustsf)
|
||||||
|
[](https://docs.rs/rustsf_auth)
|
||||||
|
|
||||||
|
## RustSF Auth
|
||||||
|
|
||||||
|
Salesforce authentication SDK for Rust, supporting the OAuth2.0 flows:
|
||||||
|
|
||||||
|
- [Client Credentials](#client-credentials)
|
||||||
|
- JWT,
|
||||||
|
- [sfdxAuthUrl](#sfdx-authentication-url),
|
||||||
|
- [Web Server](#oauth-web-server-flow),
|
||||||
|
(this flow waits for user interaction in a browser)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Client Credentials
|
||||||
|
The flow is using the login url, client id, and secret for authentication.
|
||||||
|
This requires creating an [External Client App](https://trailhead.salesforce.com/content/learn/projects/create-an-external-client-app-using-metadata-api/create-an-external-client-app) in Salesforce.
|
||||||
|
```rust
|
||||||
|
use oauth2::http::header::AUTHORIZATION;
|
||||||
|
use oauth2::http::{HeaderMap, HeaderValue};
|
||||||
|
use rustsf_auth::credentials::SalesforceCredentials;
|
||||||
|
|
||||||
|
pub const CONNECT_TIMEOUT: u64 = 15;
|
||||||
|
pub const REQUEST_TIMEOUT: u64 = 30;
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main(){
|
||||||
|
// The client id and secret, (should never be hardcoded)
|
||||||
|
let login_url = "https://example.my.salesforce.com/";
|
||||||
|
let client_id = "client_id";
|
||||||
|
let client_secret = "client_secret";
|
||||||
|
|
||||||
|
// The Credentials configuration
|
||||||
|
let config = SalesforceCredentials::client_credentials(
|
||||||
|
login_url,
|
||||||
|
client_id,
|
||||||
|
client_secret);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
println!("SUCCESS Response: {:?}", response.text().await.unwrap());
|
||||||
|
} else {
|
||||||
|
println!("ERROR Response: {:?}", response.text().await.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SFDX Authentication URL
|
||||||
|
Export the SFDX authentication URL using the [SFDX CLI](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_install_cli.htm), via the following command
|
||||||
|
```bash
|
||||||
|
sf org auth show-sfdx-auth-url --target-org my-org-alias --json > sfdx_auth_url.json
|
||||||
|
```
|
||||||
|
Use the exported SFDX authentication URL to authenticate with Salesforce using the `SalesforceCredentials::sfdx_url_json` method.
|
||||||
|
```rust
|
||||||
|
use oauth2::http::header::AUTHORIZATION;
|
||||||
|
use oauth2::http::{HeaderMap, HeaderValue};
|
||||||
|
use rustsf_auth::{SalesforceCredentials, SfdxAuthJson};
|
||||||
|
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)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
println!("SUCCESS Response: {:?}", response.text().await.unwrap());
|
||||||
|
} else {
|
||||||
|
println!("ERROR Response: {:?}", response.text().await.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively you could enter the sfdxAuthUrl directly as a string via:
|
||||||
|
```rust
|
||||||
|
let config = SalesforceCredentials::sfdx_url("force://PlatformCLI::4sdf2348kS0Qqf3GEL....@example.my.salesforce.com").unwrap();
|
||||||
|
let session = config.connect().await.unwrap();
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### OAuth Web Server Flow
|
||||||
|
With This authorization flow, users can authenticate themselves using a browser.
|
||||||
|
The callback url is then invoked by Salesforce that sends a code with which the access_token and refresh token can be requested.
|
||||||
|
|
||||||
|
<<<<<<<<<<<<<<<<<<<<<<<<< Continue HERE
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//! RustSF Authentication library - OAuth2.0 Client Credentials Example
|
||||||
|
//!
|
||||||
|
//! In this example, we demonstrate how to use the RustSF Authentication library to authenticate
|
||||||
|
//! with a client id and secret.
|
||||||
|
//! The example includes the necessary imports, constants, and a main function that performs the
|
||||||
|
//! authentication and makes a REST API request.
|
||||||
|
//!
|
||||||
|
//! The flow is using the login url, client id, and secret for authentication.
|
||||||
|
//! This requires creating an External Client App in Salesforce.
|
||||||
|
//! Follow these steps below to create one;
|
||||||
|
//!
|
||||||
|
//! - Login into Salesforce and goto "Setup"
|
||||||
|
//! - Goto "Apps" -> "App Manager"
|
||||||
|
//! - Click on "New App" -> "New External Client App" and populate the following fields":
|
||||||
|
//! - External Client App Name: "MyApp"
|
||||||
|
//! - API Name: "MyApp"
|
||||||
|
//! - Contact Email: "you@example.com"
|
||||||
|
//! - Expand the "API (Enable OAuth Settings)" section, check the "Enable OAuth" checkbox,
|
||||||
|
//! and populate the following fields:
|
||||||
|
//! - Callback URL: "http://localhost/" (its actually not used)
|
||||||
|
//! - Add "Manage user data APIs (api)" to the selected OAuth Scopes, (and any other you might need)
|
||||||
|
//! - Check the "Enable Client Credentials Flow" checkbox, in the "Flow Enablement" section
|
||||||
|
//! - Click on the "Create" button.
|
||||||
|
//! - In Setup goto "Apps" -> "External Client Apps" -> "External Client App Manager",
|
||||||
|
//! and click on the name of your newly created App, e.g. "MyApp"
|
||||||
|
//! - Goto the "Settings" tab, and expand the "OAuth Settings" section
|
||||||
|
//! - Click the "Customer Key and Secret" button, follow the instructions,
|
||||||
|
//! and copy the "Consumer Key" and "Consumer Secret" to a save location.
|
||||||
|
//! - Goto the "Policies" tab, and expand the "OAuth Policies" section.
|
||||||
|
//! - Check the checkbox "Enable Client Credentials Flow" and assign a user to the Connected App.
|
||||||
|
//!
|
||||||
|
use oauth2::http::header::AUTHORIZATION;
|
||||||
|
use oauth2::http::{HeaderMap, HeaderValue};
|
||||||
|
use rustsf_auth::credentials::SalesforceCredentials;
|
||||||
|
|
||||||
|
pub const CONNECT_TIMEOUT: u64 = 15;
|
||||||
|
pub const REQUEST_TIMEOUT: u64 = 30;
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main(){
|
||||||
|
// The client id and secret, (should never be hardcoded)
|
||||||
|
let login_url = "https://example.my.salesforce.com/";
|
||||||
|
let client_id = "client_id";
|
||||||
|
let client_secret = "client_secret";
|
||||||
|
|
||||||
|
// The Credentials configuration
|
||||||
|
let config = SalesforceCredentials::client_credentials(
|
||||||
|
login_url,
|
||||||
|
client_id,
|
||||||
|
client_secret);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
.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,60 @@
|
|||||||
|
//! RustSF Authentication library - OAuth2.0 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
|
||||||
|
//! ```
|
||||||
|
//! - 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;
|
||||||
|
|
||||||
|
pub const CONNECT_TIMEOUT: u64 = 15;
|
||||||
|
pub const REQUEST_TIMEOUT: u64 = 30;
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main(){
|
||||||
|
// The sfdx auth url, normally never hardcoded
|
||||||
|
let sfdx_auth_url = "force://PlatformCLI::5Aep8618kS0Qq2451324asdge5z2qiUtr6yudfghjfdfdSGz03Uv86qbbx2MOhcIyj.OTqFKFH.olbk5LdlYEAv@computing-platform.sandbox.my.salesforce.com";
|
||||||
|
|
||||||
|
// The Credentials configuration
|
||||||
|
let config = SalesforceCredentials::sfdx_url(sfdx_auth_url).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,63 @@
|
|||||||
|
//! RustSF Authentication library - OAuth2.0 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 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,102 @@
|
|||||||
|
//! RustSF Authentication library - OAuth2.0 Web Server Flow Example
|
||||||
|
//!
|
||||||
|
//! In this example, we demonstrate how to use the RustSF Authentication library to authenticate
|
||||||
|
//! with a OAuth2.0 Web Server flow.
|
||||||
|
//! A flow where the user is redirected to Salesforce to authenticate,
|
||||||
|
//! and then redirected back to the application.
|
||||||
|
//! The library will listen to the redirect url (http://localhost:1717/OauthRedirect),
|
||||||
|
//! and then exchange the code for an access token.
|
||||||
|
//!
|
||||||
|
//! The user must have API access enabled to be able to make API calls to Salesforce.
|
||||||
|
//!
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use oauth2::http::header::AUTHORIZATION;
|
||||||
|
use oauth2::http::{HeaderMap, HeaderValue};
|
||||||
|
use rustsf_auth::OAuthWebService;
|
||||||
|
|
||||||
|
pub const CONNECT_TIMEOUT: u64 = 15;
|
||||||
|
pub const REQUEST_TIMEOUT: u64 = 30;
|
||||||
|
|
||||||
|
fn open_browser(url: &str) {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let _ = Command::new("cmd")
|
||||||
|
.args(["/C", "start", "", url])
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let _ = Command::new("open")
|
||||||
|
.arg(url)
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
|
let _ = Command::new("xdg-open")
|
||||||
|
.arg(url)
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// The client id, urls and scopes.
|
||||||
|
let login_url = "https://example.my.salesforce.com/";
|
||||||
|
let client_id = "PlatformCLI"; // The connected app the SFDX CLI is using
|
||||||
|
let client_secret = None;
|
||||||
|
let scopes = None;
|
||||||
|
let redirect_url = "http://localhost:1717/OauthRedirect";
|
||||||
|
|
||||||
|
// Prepare the Web Service
|
||||||
|
let web_service = OAuthWebService::new(
|
||||||
|
login_url,
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
redirect_url,
|
||||||
|
scopes,
|
||||||
|
)
|
||||||
|
// Optionally: add a custom callback response the user will see after authentication
|
||||||
|
.with_callback_response(
|
||||||
|
r#"<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head><title>Authenticated</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Authentication complete</h1>
|
||||||
|
<p>You can close this tab.</p>
|
||||||
|
</body>
|
||||||
|
</html>"#,
|
||||||
|
);
|
||||||
|
let auth_url = web_service.authorization_url().await.unwrap();
|
||||||
|
|
||||||
|
// Ask user to authenticate themselves
|
||||||
|
println!("Open this URL in your browser if it did not open automatically:\n{auth_url}");
|
||||||
|
open_browser(&auth_url);
|
||||||
|
let session = web_service.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)
|
||||||
|
.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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"status": 0,
|
||||||
|
"result": {
|
||||||
|
"sfdxAuthUrl": "force://PlatformCLI::5Aep8618kS0Qqf3GEL4qVTX2VSz2qiUtn3Grl0qSlJjRoSGz03Uv86qbbx2MOhcIyj.OTqFKFH.olbk5LdlYEAv@computing-platform-9537--qa.sandbox.my.salesforce.com"
|
||||||
|
},
|
||||||
|
"warnings": [
|
||||||
|
"This command exposes an SFDX Auth URL. Unlike an access token, this credential contains a refresh token that allows extended access to an org. Avoid sharing or logging this URL. For additional information about org authorization, review https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_url.htm."
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
//! Access token authentication module.
|
||||||
|
//!
|
||||||
|
//! This module provides functionality for authenticating with Salesforce using an existing
|
||||||
|
//! access token. It supports both simple access token authentication and refresh token
|
||||||
|
//! capabilities when additional credentials are provided.
|
||||||
|
|
||||||
|
use crate::credentials::{required, SalesforceAuthFlow, SalesforceCredentials};
|
||||||
|
use crate::error::SalesforceAuthError;
|
||||||
|
use crate::salesforce_token_response::SalesforceTokenResponse;
|
||||||
|
|
||||||
|
impl SalesforceCredentials {
|
||||||
|
/// Creates a configuration for using an existing Salesforce access token.
|
||||||
|
///
|
||||||
|
/// If `refresh_token`, `client_id`, `client_secret`, and `login_url` are
|
||||||
|
/// supplied, the resulting [`SalesforceAuthSession`] can later refresh its
|
||||||
|
/// access token with [`SalesforceAuthSession::refresh_access_token`].
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// * `access_token` - The existing Salesforce access token
|
||||||
|
/// * `instance_url` - The Salesforce instance URL (e.g., "https://example.my.salesforce.com")
|
||||||
|
/// * `refresh_token` - Optional refresh token for renewing the access token
|
||||||
|
/// * `client_id` - Optional client ID from the connected app
|
||||||
|
/// * `client_secret` - Optional client secret from the connected app
|
||||||
|
/// * `login_url` - Optional Salesforce login URL (e.g., "https://login.salesforce.com")
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use rustsf_auth::{SalesforceCredentials, SalesforceAuthFlow};
|
||||||
|
///
|
||||||
|
/// let config = SalesforceCredentials::access_token(
|
||||||
|
/// "access-token",
|
||||||
|
/// "https://example.my.salesforce.com",
|
||||||
|
/// Some("refresh-token".to_string()),
|
||||||
|
/// Some("client-id".to_string()),
|
||||||
|
/// Some("client-secret".to_string()),
|
||||||
|
/// Some("https://login.salesforce.com".to_string()),
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(config.flow, SalesforceAuthFlow::AccessToken);
|
||||||
|
/// assert_eq!(config.access_token.as_deref(), Some("access-token"));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
/// An access token has a limited lifetime and will expire, since there is no refresh_token,
|
||||||
|
/// the application will panic when its expired.
|
||||||
|
/// Only use this authentication flow for short-lived applications
|
||||||
|
pub fn access_token(
|
||||||
|
access_token: impl Into<String>,
|
||||||
|
instance_url: impl Into<String>,
|
||||||
|
refresh_token: Option<String>,
|
||||||
|
client_id: Option<String>,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
login_url: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
flow: SalesforceAuthFlow::AccessToken,
|
||||||
|
login_url,
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
username: None,
|
||||||
|
private_key_pem: None,
|
||||||
|
access_token: Some(access_token.into()),
|
||||||
|
refresh_token,
|
||||||
|
instance_url: Some(instance_url.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Salesforce authentication session from an existing access token.
|
||||||
|
///
|
||||||
|
/// This function does not validate the access token with Salesforce. It simply
|
||||||
|
/// wraps the token and related metadata in a [`SalesforceAuthSession`].
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// This method uses the following fields from `self`:
|
||||||
|
/// * `access_token` - The existing Salesforce access token (required)
|
||||||
|
/// * `login_url` - The Salesforce login URL (required)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` containing a `SalesforceTokenResponse` on success, or a
|
||||||
|
/// `SalesforceAuthError` if required fields are missing.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use rustsf_auth::authenticate_access_token;
|
||||||
|
///
|
||||||
|
/// let session = authenticate_access_token(
|
||||||
|
/// "access-token",
|
||||||
|
/// "https://example.my.salesforce.com/",
|
||||||
|
/// Some("refresh-token".to_string()),
|
||||||
|
/// Some("client-id".to_string()),
|
||||||
|
/// Some("client-secret".to_string()),
|
||||||
|
/// Some("https://login.salesforce.com".to_string()),
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(session.access_token, "access-token");
|
||||||
|
/// assert_eq!(session.instance_url, "https://example.my.salesforce.com");
|
||||||
|
/// ```
|
||||||
|
pub(crate) fn connect_access_token(&self) -> Result<SalesforceTokenResponse, SalesforceAuthError> {
|
||||||
|
let access_token = required(self.access_token.as_deref(), "access_token")?;
|
||||||
|
let login_url = required(self.login_url.as_deref(), "login_url")?;
|
||||||
|
|
||||||
|
Ok(SalesforceTokenResponse {
|
||||||
|
access_token: access_token.to_string(),
|
||||||
|
instance_url: Some(login_url.trim_end_matches('/').to_string()),
|
||||||
|
id: None,
|
||||||
|
token_type: Some("Bearer".to_string()),
|
||||||
|
issued_at: None,
|
||||||
|
signature: None,
|
||||||
|
refresh_token: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
//! OAuth 2.0 Client Credentials flow implementation for Salesforce authentication.
|
||||||
|
//!
|
||||||
|
//! This module provides functionality to authenticate with Salesforce using the
|
||||||
|
//! OAuth 2.0 Client Credentials grant type, which is suitable for server-to-server
|
||||||
|
//! integrations where no user interaction is required.
|
||||||
|
|
||||||
|
use log::trace;
|
||||||
|
use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials};
|
||||||
|
use crate::salesforce_token_response::SalesforceTokenResponse;
|
||||||
|
use crate::SalesforceAuthError;
|
||||||
|
|
||||||
|
impl SalesforceCredentials {
|
||||||
|
/// Connects to Salesforce using the OAuth 2.0 Client Credentials flow.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// - `login_url`: The Salesforce login url, e.g. `https://login.salesforce.com`
|
||||||
|
/// - `client_id`: The Connected App Client Id
|
||||||
|
/// - `client_secret`: The Connected App Client Secret
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use oauth2::http::header::AUTHORIZATION;
|
||||||
|
/// use oauth2::http::{HeaderMap, HeaderValue};
|
||||||
|
/// use crate::credentials::SalesforceCredentials;
|
||||||
|
///
|
||||||
|
/// pub const CONNECT_TIMEOUT: u64 = 15;
|
||||||
|
/// pub const REQUEST_TIMEOUT: u64 = 30;
|
||||||
|
/// #[tokio::main]
|
||||||
|
/// async fn main(){
|
||||||
|
/// // The client id and secret, (should never be hardcoded)
|
||||||
|
/// let login_url = "https://example.my.salesforce.com/";
|
||||||
|
/// let client_id = "client_id";
|
||||||
|
/// let client_secret = "client_secret";
|
||||||
|
///
|
||||||
|
/// // The Credentials configuration
|
||||||
|
/// let config = SalesforceCredentials::client_credentials(
|
||||||
|
/// login_url,
|
||||||
|
/// client_id,
|
||||||
|
/// client_secret);
|
||||||
|
///
|
||||||
|
/// // 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)
|
||||||
|
/// .send()
|
||||||
|
/// .await
|
||||||
|
/// .unwrap();
|
||||||
|
///
|
||||||
|
/// if response.status().is_success() {
|
||||||
|
/// println!("SUCCESS Response: {:?}", response.text().await.unwrap());
|
||||||
|
/// } else {
|
||||||
|
/// println!("ERROR Response: {:?}", response.text().await.unwrap());
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn client_credentials(
|
||||||
|
login_url: impl Into<String>,
|
||||||
|
client_id: impl Into<String>,
|
||||||
|
client_secret: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
flow: SalesforceAuthFlow::ClientCredentials,
|
||||||
|
login_url: Some(login_url.into()),
|
||||||
|
client_id: Some(client_id.into()),
|
||||||
|
client_secret: Some(client_secret.into()),
|
||||||
|
username: None,
|
||||||
|
private_key_pem: None,
|
||||||
|
access_token: None,
|
||||||
|
refresh_token: None,
|
||||||
|
instance_url: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticates to Salesforce using the OAuth 2.0 Client Credentials flow.
|
||||||
|
///
|
||||||
|
/// This method sends a client credentials token request to:
|
||||||
|
///
|
||||||
|
/// `{login_url}/services/oauth2/token`
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// This method uses the following fields from `self`:
|
||||||
|
/// - `client_id` - The Connected App Client ID (required)
|
||||||
|
/// - `client_secret` - The Connected App Client Secret (required)
|
||||||
|
/// - `login_url` - The Salesforce login URL (required)
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError`] if:
|
||||||
|
/// - Required fields (`client_id`, `client_secret`, or `login_url`) are missing
|
||||||
|
/// - The HTTP request fails
|
||||||
|
/// - Salesforce rejects the token request
|
||||||
|
/// - The response cannot be parsed
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rustsf_auth::{SalesforceCredentials, SalesforceAuthError};
|
||||||
|
///
|
||||||
|
/// # async fn example() -> Result<(), SalesforceAuthError> {
|
||||||
|
/// let credentials = SalesforceCredentials::client_credentials(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "your-client-id",
|
||||||
|
/// "your-client-secret",
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// let session = credentials.connect().await?;
|
||||||
|
/// println!("Access token: {}", session.access_token().await?);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub async fn connect_client_credentials(&self) -> Result<SalesforceTokenResponse, SalesforceAuthError> {
|
||||||
|
let client_id = required(self.client_id.as_deref(), "client_id")?;
|
||||||
|
let client_secret = required(self.client_secret.as_deref(), "client_secret")?;
|
||||||
|
|
||||||
|
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()))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
use base64::Engine;
|
||||||
|
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||||
|
|
||||||
|
use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials};
|
||||||
|
use crate::error::SalesforceAuthError;
|
||||||
|
use crate::salesforce_token_response::SalesforceTokenResponse;
|
||||||
|
|
||||||
|
use rsa::pkcs1::DecodeRsaPrivateKey;
|
||||||
|
use rsa::pkcs8::DecodePrivateKey;
|
||||||
|
use rsa::pkcs1v15::SigningKey;
|
||||||
|
use rsa::signature::{RandomizedSigner, SignatureEncoding};
|
||||||
|
use rsa::RsaPrivateKey;
|
||||||
|
use serde::Serialize;
|
||||||
|
use sha2::Sha256;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use crate::salesforce_token_endpoint;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct JwtHeader<'a> {
|
||||||
|
alg: &'a str,
|
||||||
|
typ: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct JwtClaims<'a> {
|
||||||
|
iss: &'a str,
|
||||||
|
sub: &'a str,
|
||||||
|
aud: &'a str,
|
||||||
|
exp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SalesforceCredentials {
|
||||||
|
/// Creates a configuration for the Salesforce JWT bearer flow.
|
||||||
|
///
|
||||||
|
/// The private key must be an RSA private key in PEM format. It should
|
||||||
|
/// correspond to the certificate configured on the Salesforce connected app.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use rustsf_auth::{SalesforceCredentials, SalesforceAuthFlow};
|
||||||
|
///
|
||||||
|
/// let config = SalesforceCredentials::jwt_bearer(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "client-id",
|
||||||
|
/// "user@example.com",
|
||||||
|
/// "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(config.flow, SalesforceAuthFlow::JwtBearer);
|
||||||
|
/// assert_eq!(config.username.as_deref(), Some("user@example.com"));
|
||||||
|
/// ```
|
||||||
|
pub fn jwt_bearer(
|
||||||
|
login_url: impl Into<String>,
|
||||||
|
client_id: impl Into<String>,
|
||||||
|
username: impl Into<String>,
|
||||||
|
private_key_pem: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
flow: SalesforceAuthFlow::JwtBearer,
|
||||||
|
login_url: Some(login_url.into()),
|
||||||
|
client_id: Some(client_id.into()),
|
||||||
|
client_secret: None,
|
||||||
|
username: Some(username.into()),
|
||||||
|
private_key_pem: Some(private_key_pem.into()),
|
||||||
|
access_token: None,
|
||||||
|
refresh_token: None,
|
||||||
|
instance_url: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticates to Salesforce using the JWT bearer flow.
|
||||||
|
///
|
||||||
|
/// This function creates a signed RS256 JWT assertion and exchanges it for a
|
||||||
|
/// Salesforce access token.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError`] if the private key cannot be parsed, the JWT
|
||||||
|
/// cannot be signed, the token URL is invalid, the HTTP request fails, or
|
||||||
|
/// Salesforce rejects the assertion.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rustsf_auth::authenticate_jwt_bearer;
|
||||||
|
///
|
||||||
|
/// # async fn example() -> Result<(), rustsf_auth::SalesforceAuthError> {
|
||||||
|
/// let private_key_pem = "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----";
|
||||||
|
///
|
||||||
|
/// let session = authenticate_jwt_bearer(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "client-id",
|
||||||
|
/// "user@example.com",
|
||||||
|
/// private_key_pem,
|
||||||
|
/// ).await?;
|
||||||
|
///
|
||||||
|
/// println!("{}", session.access_token);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub(crate) async fn connect_jwt(&self) -> Result<SalesforceTokenResponse, SalesforceAuthError> {
|
||||||
|
let login_url = required(self.login_url.as_deref(), "login_url")?;
|
||||||
|
let client_id = required(self.client_id.as_deref(), "client_id")?;
|
||||||
|
let username = required(self.username.as_deref(), "username")?;
|
||||||
|
let private_key_pem = required(self.private_key_pem.as_deref(), "private_key_pem")?;
|
||||||
|
|
||||||
|
let assertion = create_salesforce_jwt_assertion(login_url, client_id, username, private_key_pem)?;
|
||||||
|
|
||||||
|
Ok(http_client()?
|
||||||
|
.post(salesforce_token_endpoint(login_url.trim_end_matches('/')))
|
||||||
|
.form(&[
|
||||||
|
("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
|
||||||
|
("assertion", assertion.as_str()),
|
||||||
|
])
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<SalesforceTokenResponse>()
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a signed Salesforce JWT bearer assertion.
|
||||||
|
///
|
||||||
|
/// The returned string is a compact JWT signed with RS256. It is intended for
|
||||||
|
/// the Salesforce OAuth 2.0 JWT bearer grant.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError::Jwt`] if the private key cannot be parsed,
|
||||||
|
/// the JWT claims cannot be serialized, or signing fails.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rustsf_auth::create_salesforce_jwt_assertion;
|
||||||
|
///
|
||||||
|
/// let private_key_pem = "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----";
|
||||||
|
///
|
||||||
|
/// let assertion = create_salesforce_jwt_assertion(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "client-id",
|
||||||
|
/// "user@example.com",
|
||||||
|
/// private_key_pem,
|
||||||
|
/// )?;
|
||||||
|
///
|
||||||
|
/// assert_eq!(assertion.split('.').count(), 3);
|
||||||
|
/// # Ok::<(), rustsf_auth::SalesforceAuthError>(())
|
||||||
|
/// ```
|
||||||
|
pub fn create_salesforce_jwt_assertion(
|
||||||
|
login_url: &str,
|
||||||
|
client_id: &str,
|
||||||
|
username: &str,
|
||||||
|
private_key_pem: &str,
|
||||||
|
) -> Result<String, SalesforceAuthError> {
|
||||||
|
let header = JwtHeader {
|
||||||
|
alg: "RS256",
|
||||||
|
typ: "JWT",
|
||||||
|
};
|
||||||
|
|
||||||
|
let claims = JwtClaims {
|
||||||
|
iss: client_id,
|
||||||
|
sub: username,
|
||||||
|
aud: login_url.trim_end_matches('/'),
|
||||||
|
exp: OffsetDateTime::now_utc().unix_timestamp() + 180,
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoded_header = encode_json_base64_url(&header)?;
|
||||||
|
let encoded_claims = encode_json_base64_url(&claims)?;
|
||||||
|
let signing_input = format!("{encoded_header}.{encoded_claims}");
|
||||||
|
|
||||||
|
let private_key = RsaPrivateKey::from_pkcs8_pem(private_key_pem)
|
||||||
|
.or_else(|_| RsaPrivateKey::from_pkcs1_pem(private_key_pem))
|
||||||
|
.map_err(|error| SalesforceAuthError::Jwt(error.to_string()))?;
|
||||||
|
|
||||||
|
let signing_key = SigningKey::<Sha256>::new(private_key);
|
||||||
|
let mut rng = rsa::rand_core::OsRng;
|
||||||
|
let signature = signing_key.sign_with_rng(&mut rng, signing_input.as_bytes());
|
||||||
|
let encoded_signature = URL_SAFE_NO_PAD.encode(signature.to_bytes());
|
||||||
|
|
||||||
|
Ok(format!("{signing_input}.{encoded_signature}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_json_base64_url<T: Serialize>(value: &T) -> Result<String, SalesforceAuthError> {
|
||||||
|
let json = serde_json::to_vec(value).map_err(|error| SalesforceAuthError::Jwt(error.to_string()))?;
|
||||||
|
Ok(URL_SAFE_NO_PAD.encode(json))
|
||||||
|
}
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
//! Credentials and authentication flow management for Salesforce.
|
||||||
|
//!
|
||||||
|
//! This module provides the core types and methods for authenticating with Salesforce
|
||||||
|
//! using various OAuth 2.0 flows, including client credentials, JWT bearer, SFDX auth URLs,
|
||||||
|
//! and existing access tokens.
|
||||||
|
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::time::Duration;
|
||||||
|
use log::trace;
|
||||||
|
use oauth2::TokenUrl;
|
||||||
|
use reqwest::Client;
|
||||||
|
use crate::{SalesforceAuthError, SalesforceAuthSession, SalesforceAuthToken};
|
||||||
|
use crate::salesforce_token_response::SalesforceTokenResponse;
|
||||||
|
|
||||||
|
mod access_token;
|
||||||
|
mod client_credentials;
|
||||||
|
mod jwt_bearer;
|
||||||
|
pub(crate) mod sfdx_auth_url;
|
||||||
|
pub mod web_server;
|
||||||
|
|
||||||
|
/// Supported Salesforce OAuth authentication flows.
|
||||||
|
///
|
||||||
|
/// This enum is used by [`SalesforceCredentials`] to decide which authentication
|
||||||
|
/// method should be executed when [`SalesforceCredentials::connect`] is called.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use rustsf_auth::SalesforceAuthFlow;
|
||||||
|
///
|
||||||
|
/// let flow = SalesforceAuthFlow::ClientCredentials;
|
||||||
|
/// assert!(matches!(flow, SalesforceAuthFlow::ClientCredentials));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum SalesforceAuthFlow {
|
||||||
|
/// Authenticate using the OAuth 2.0 client mod grant.
|
||||||
|
ClientCredentials,
|
||||||
|
|
||||||
|
/// Authenticate using the Salesforce JWT bearer grant.
|
||||||
|
JwtBearer,
|
||||||
|
|
||||||
|
/// Authenticate from an SFDX auth URL.
|
||||||
|
SfdxUrl,
|
||||||
|
|
||||||
|
/// Use an already available Salesforce access token.
|
||||||
|
AccessToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Credentials details for authenticating to Salesforce.
|
||||||
|
///
|
||||||
|
/// This struct provides a single, convenient way to pass the required information for authentication
|
||||||
|
///
|
||||||
|
/// Prefer using one of the constructor methods below instead of manually constructing the struct.
|
||||||
|
/// - [`SalesforceCredentials::client_credentials`],
|
||||||
|
/// - [`SalesforceCredentials::jwt_bearer`],
|
||||||
|
/// - [`SalesforceCredentials::sfdx_url`],
|
||||||
|
/// - [`SalesforceCredentials::access_token`]
|
||||||
|
/// instead of manually constructing the struct.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SalesforceCredentials {
|
||||||
|
/// The authentication flow to execute.
|
||||||
|
pub flow: SalesforceAuthFlow,
|
||||||
|
|
||||||
|
/// Salesforce login URL, for example `https://login.salesforce.com`.
|
||||||
|
pub login_url: Option<String>,
|
||||||
|
|
||||||
|
/// Salesforce connected app client ID.
|
||||||
|
pub client_id: Option<String>,
|
||||||
|
|
||||||
|
/// Salesforce connected app client secret.
|
||||||
|
pub client_secret: Option<String>,
|
||||||
|
|
||||||
|
/// Salesforce username used by the JWT bearer flow.
|
||||||
|
pub username: Option<String>,
|
||||||
|
|
||||||
|
/// RSA private key in PEM format used by the JWT bearer flow.
|
||||||
|
pub private_key_pem: Option<String>,
|
||||||
|
|
||||||
|
/// Existing Salesforce access token.
|
||||||
|
pub access_token: Option<String>,
|
||||||
|
|
||||||
|
/// Salesforce refresh token.
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
|
||||||
|
/// Salesforce instance URL, for example `https://example.my.salesforce.com`.
|
||||||
|
pub instance_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SalesforceCredentials {
|
||||||
|
|
||||||
|
/// Authenticates to Salesforce using the configured flow.
|
||||||
|
///
|
||||||
|
/// This method dispatches to one of:
|
||||||
|
///
|
||||||
|
/// - [`authenticate_client_credentials`]
|
||||||
|
/// - [`authenticate_jwt_bearer`]
|
||||||
|
/// - [`authenticate_sfdx_url`]
|
||||||
|
/// - [`authenticate_access_token`]
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError`] if required fields are missing, if the
|
||||||
|
/// configured URL is invalid, if Salesforce rejects the OAuth request, or if
|
||||||
|
/// the underlying HTTP request fails.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rustsf_auth::{SalesforceCredentials, SalesforceAuthError};
|
||||||
|
///
|
||||||
|
/// async fn example() -> Result<(), SalesforceAuthError> {
|
||||||
|
/// let config = SalesforceCredentials::client_credentials(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "client-id",
|
||||||
|
/// "client-secret",
|
||||||
|
/// );
|
||||||
|
/// let session = config.connect().await?;
|
||||||
|
/// println!("{}", session.access_token().await?);
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
/// The OAuth2.0 Web Server flow cannot be used with this method as that type is a two-step flow.
|
||||||
|
pub async fn connect(self) -> Result<SalesforceAuthSession, SalesforceAuthError> {
|
||||||
|
let token_response = self.token_response_from_flow().await?;
|
||||||
|
|
||||||
|
let (org_id, user_id) = parse_salesforce_identity_ids(token_response.id.as_deref());
|
||||||
|
|
||||||
|
let instance_url = match (&token_response.instance_url, &self.instance_url, &self.login_url) {
|
||||||
|
| (Some(url), _, _ )
|
||||||
|
| (_, Some(url), None)
|
||||||
|
| (_, None, Some(url))
|
||||||
|
| (_, Some(url), _) => { url },
|
||||||
|
(_, _, _) => "http://login.salesforce.com",
|
||||||
|
}.to_string();
|
||||||
|
|
||||||
|
let mut credentials = self;
|
||||||
|
if credentials.refresh_token.is_none() {
|
||||||
|
credentials.refresh_token = token_response.refresh_token.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SalesforceAuthSession {
|
||||||
|
token: RwLock::new(SalesforceAuthToken {
|
||||||
|
access_token: token_response.access_token,
|
||||||
|
token_type: token_response.token_type,
|
||||||
|
issued_at: token_response.issued_at,
|
||||||
|
signature: token_response.signature,
|
||||||
|
}),
|
||||||
|
credentials,
|
||||||
|
instance_url,
|
||||||
|
org_id,
|
||||||
|
user_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconnects to Salesforce by re-executing the configured authentication flow.
|
||||||
|
///
|
||||||
|
/// This internal method is used to obtain a fresh access token by running the
|
||||||
|
/// authentication flow again from scratch.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError`] if the authentication flow fails.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// # use rustsf_auth::{SalesforceCredentials, SalesforceAuthError};
|
||||||
|
/// # async fn example() -> Result<(), SalesforceAuthError> {
|
||||||
|
/// let credentials = SalesforceCredentials::client_credentials(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "client-id",
|
||||||
|
/// "client-secret",
|
||||||
|
/// );
|
||||||
|
/// let token = credentials.reconnect().await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub(crate) async fn reconnect(&self) -> Result<SalesforceAuthToken, SalesforceAuthError> {
|
||||||
|
let token_response = self.token_response_from_flow().await?;
|
||||||
|
Ok(SalesforceAuthToken::from_token_response(token_response))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the configured authentication flow and returns the token response.
|
||||||
|
///
|
||||||
|
/// This internal method dispatches to the appropriate authentication method based
|
||||||
|
/// on the configured [`SalesforceAuthFlow`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError`] if the authentication flow fails.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// # use rustsf_auth::{SalesforceCredentials, SalesforceAuthError};
|
||||||
|
/// # async fn example() -> Result<(), SalesforceAuthError> {
|
||||||
|
/// let credentials = SalesforceCredentials::client_credentials(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "client-id",
|
||||||
|
/// "client-secret",
|
||||||
|
/// );
|
||||||
|
/// let response = credentials.token_response_from_flow().await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
async fn token_response_from_flow(&self) -> Result<SalesforceTokenResponse, SalesforceAuthError> {
|
||||||
|
match self.flow {
|
||||||
|
SalesforceAuthFlow::AccessToken => self.connect_access_token(),
|
||||||
|
SalesforceAuthFlow::ClientCredentials => self.connect_client_credentials().await,
|
||||||
|
SalesforceAuthFlow::JwtBearer => self.connect_jwt().await,
|
||||||
|
SalesforceAuthFlow::SfdxUrl => self.connect_sfdx_url().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refreshes the Salesforce access token using a refresh token or by reconnecting.
|
||||||
|
///
|
||||||
|
/// If a refresh token is available, this method uses it to obtain a new access token
|
||||||
|
/// via the OAuth 2.0 refresh token grant. Otherwise, it falls back to reconnecting
|
||||||
|
/// using the original authentication flow.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError`] if the refresh token grant fails, if required
|
||||||
|
/// fields are missing, or if the HTTP request fails.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// # use rustsf_auth::{SalesforceCredentials, SalesforceAuthError};
|
||||||
|
/// # async fn example() -> Result<(), SalesforceAuthError> {
|
||||||
|
/// let credentials = SalesforceCredentials::client_credentials(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "client-id",
|
||||||
|
/// "client-secret",
|
||||||
|
/// );
|
||||||
|
/// let token = credentials.refresh().await?;
|
||||||
|
/// println!("Access token: {}", token.access_token);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub(crate) async fn refresh(&self) -> Result<SalesforceAuthToken, SalesforceAuthError> {
|
||||||
|
|
||||||
|
match &self.refresh_token {
|
||||||
|
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()))?)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs the OAuth 2.0 token endpoint URL from the login URL.
|
||||||
|
///
|
||||||
|
/// This method builds the full token URL by appending `/services/oauth2/token`
|
||||||
|
/// to the configured login URL.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError::MissingRequiredField`] if `login_url` is not set,
|
||||||
|
/// or [`SalesforceAuthError::InvalidUrl`] if the constructed URL is invalid.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// # use rustsf_auth::{SalesforceCredentials, SalesforceAuthError};
|
||||||
|
/// # fn example() -> Result<(), SalesforceAuthError> {
|
||||||
|
/// let credentials = SalesforceCredentials::client_credentials(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "client-id",
|
||||||
|
/// "client-secret",
|
||||||
|
/// );
|
||||||
|
/// let token_url = credentials.token_url()?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
fn token_url(&self) -> Result<TokenUrl, SalesforceAuthError> {
|
||||||
|
match &self.login_url {
|
||||||
|
Some(login_url) => {
|
||||||
|
let normalized = login_url.trim_end_matches('/');
|
||||||
|
let url = format!("{normalized}/services/oauth2/token");
|
||||||
|
Ok(TokenUrl::new(url.clone()).map_err(|source| SalesforceAuthError::InvalidUrl { url, source })?)
|
||||||
|
}
|
||||||
|
None => Err(SalesforceAuthError::MissingRequiredField("login_url")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a configured HTTP client for Salesforce API requests.
|
||||||
|
///
|
||||||
|
/// This function builds a [`reqwest::Client`] with no automatic redirects and a 30-second timeout.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError`] if the HTTP client cannot be built.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// # use rustsf_auth::{SalesforceAuthError};
|
||||||
|
/// # use rustsf_auth::credentials::http_client;
|
||||||
|
/// # fn example() -> Result<(), SalesforceAuthError> {
|
||||||
|
/// let client = http_client()?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub(crate) fn http_client() -> Result<reqwest::Client, SalesforceAuthError> {
|
||||||
|
Ok(Client::builder()
|
||||||
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.timeout(Duration::from_secs(30))
|
||||||
|
.build()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Validates that a required field is present.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// * `value` - The optional field value to check
|
||||||
|
/// * `field_name` - The name of the field for error reporting
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError::MissingRequiredField`] if `value` is `None`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// # use rustsf_auth::SalesforceAuthError;
|
||||||
|
/// # use rustsf_auth::credentials::required;
|
||||||
|
/// # fn example() -> Result<(), SalesforceAuthError> {
|
||||||
|
/// let client_id = Some("my-client-id");
|
||||||
|
/// let validated = required(client_id.as_deref(), "client_id")?;
|
||||||
|
/// assert_eq!(validated, "my-client-id");
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the organization ID and user ID from a Salesforce identity URL.
|
||||||
|
///
|
||||||
|
/// Salesforce identity URLs have the format:
|
||||||
|
/// `https://login.salesforce.com/id/{org_id}/{user_id}`
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// * `id_url` - The optional Salesforce identity URL to parse
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A tuple of `(org_id, user_id)`, where both are `None` if the URL is invalid or missing.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// # use rustsf_auth::credentials::parse_salesforce_identity_ids;
|
||||||
|
/// let url = Some("https://login.salesforce.com/id/00Dxx0000001gPLEAY/005xx000001SwiUAAS");
|
||||||
|
/// let (org_id, user_id) = parse_salesforce_identity_ids(url);
|
||||||
|
/// assert_eq!(org_id, Some("00Dxx0000001gPLEAY".to_string()));
|
||||||
|
/// assert_eq!(user_id, Some("005xx000001SwiUAAS".to_string()));
|
||||||
|
/// ```
|
||||||
|
fn parse_salesforce_identity_ids(id_url: Option<&str>) -> (Option<String>, Option<String>) {
|
||||||
|
let Some(id_url) = id_url else {
|
||||||
|
return (None, None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let segments = id_url
|
||||||
|
.trim_end_matches('/')
|
||||||
|
.split('/')
|
||||||
|
.filter(|segment| !segment.is_empty())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if segments.len() < 2 {
|
||||||
|
return (None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let org_id = segments
|
||||||
|
.get(segments.len() - 2)
|
||||||
|
.map(|value| (*value).to_string());
|
||||||
|
|
||||||
|
let user_id = segments
|
||||||
|
.last()
|
||||||
|
.map(|value| (*value).to_string());
|
||||||
|
|
||||||
|
(org_id, user_id)
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
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.
|
||||||
|
///
|
||||||
|
/// The SFDX URL must match:
|
||||||
|
///
|
||||||
|
/// `force://CLIENT_ID:CLIENT_SECRET:REFRESH_TOKEN@LOGIN_ENDPOINT`
|
||||||
|
///
|
||||||
|
/// The login endpoint must not include `https://`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use rustsf_auth::{SalesforceCredentials, SalesforceAuthFlow};
|
||||||
|
///
|
||||||
|
/// let config = SalesforceCredentials::sfdx_url(
|
||||||
|
/// "force://client_id:client_secret:refresh-token@login.salesforce.com",
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(config.flow, SalesforceAuthFlow::SfdxUrl);
|
||||||
|
/// assert!(config.sfdx_url.is_some());
|
||||||
|
/// ```
|
||||||
|
pub fn sfdx_url(url: impl Into<String>) -> Result<Self, SalesforceAuthError> {
|
||||||
|
|
||||||
|
let regex = Regex::new(r"force://([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let url = url.into();
|
||||||
|
|
||||||
|
let captures = regex.captures(&url)
|
||||||
|
// .with_context(|| "Invalid Authentication URL expected 'force://([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)'".to_string())?;
|
||||||
|
.ok_or(SalesforceAuthError::InvalidSfdxUrl)?;
|
||||||
|
|
||||||
|
let client_id = captures
|
||||||
|
.get(1)
|
||||||
|
|
||||||
|
.ok_or(SalesforceAuthError::InvalidSfdxUrl)?
|
||||||
|
.as_str()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let client_secret = captures
|
||||||
|
.get(2)
|
||||||
|
.map(|capture| capture.as_str().to_string())
|
||||||
|
.filter(|value| !value.is_empty());
|
||||||
|
|
||||||
|
let refresh_token = captures
|
||||||
|
.get(3)
|
||||||
|
.ok_or(SalesforceAuthError::InvalidSfdxUrl)?
|
||||||
|
.as_str()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let login_endpoint = captures
|
||||||
|
.get(4)
|
||||||
|
.ok_or(SalesforceAuthError::InvalidSfdxUrl)?
|
||||||
|
.as_str();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
flow: SalesforceAuthFlow::SfdxUrl,
|
||||||
|
login_url: Some(format!("https://{login_endpoint}")),
|
||||||
|
client_id: Some(client_id),
|
||||||
|
client_secret,
|
||||||
|
username: None,
|
||||||
|
private_key_pem: None,
|
||||||
|
access_token: None,
|
||||||
|
refresh_token: Some(refresh_token),
|
||||||
|
instance_url: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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:
|
||||||
|
///
|
||||||
|
/// `force://([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)`
|
||||||
|
///
|
||||||
|
/// Regex groups are interpreted as:
|
||||||
|
///
|
||||||
|
/// 1. client ID
|
||||||
|
/// 2. client secret
|
||||||
|
/// 3. refresh token
|
||||||
|
/// 4. login endpoint without `https://`
|
||||||
|
///
|
||||||
|
/// Internally this parses the SFDX URL and then calls [`refresh_access_token`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError::InvalidSfdxUrl`] if the URL does not match
|
||||||
|
/// the required format. Returns other [`SalesforceAuthError`] variants if the
|
||||||
|
/// refresh request fails.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rustsf_auth::authenticate_sfdx_url;
|
||||||
|
///
|
||||||
|
/// # async fn example() -> Result<(), rustsf_auth::SalesforceAuthError> {
|
||||||
|
/// let session = authenticate_sfdx_url(
|
||||||
|
/// "force://client_id:client_secret:refresh-token@login.salesforce.com",
|
||||||
|
/// ).await?;
|
||||||
|
///
|
||||||
|
/// println!("{}", session.access_token);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub(crate) async fn connect_sfdx_url(&self) -> Result<SalesforceTokenResponse, SalesforceAuthError> {
|
||||||
|
|
||||||
|
let client_id = required(self.client_id.as_deref(), "client_id")?;
|
||||||
|
let refresh_token = required(self.refresh_token.as_deref(), "refresh_token")?;
|
||||||
|
|
||||||
|
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?;
|
||||||
|
|
||||||
|
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()))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
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";
|
||||||
|
let result = SalesforceCredentials::sfdx_url(url);
|
||||||
|
|
||||||
|
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_without_client_secret() {
|
||||||
|
let url = "force://PlatformCLI::refresh_token_value@test.salesforce.com";
|
||||||
|
let result = SalesforceCredentials::sfdx_url(url);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let credentials = result.unwrap();
|
||||||
|
assert_eq!(credentials.flow, SalesforceAuthFlow::SfdxUrl);
|
||||||
|
assert_eq!(credentials.client_id, Some("PlatformCLI".to_string()));
|
||||||
|
assert_eq!(credentials.client_secret, None);
|
||||||
|
assert_eq!(credentials.refresh_token, Some("refresh_token_value".to_string()));
|
||||||
|
assert_eq!(credentials.login_url, Some("https://test.salesforce.com".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sfdx_url_with_base64_refresh_token() {
|
||||||
|
let url = "force://client123:secret456:dGVzdF90b2tlbl92YWx1ZQ==@login.salesforce.com";
|
||||||
|
let result = SalesforceCredentials::sfdx_url(url);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let credentials = result.unwrap();
|
||||||
|
assert_eq!(credentials.client_id, Some("client123".to_string()));
|
||||||
|
assert_eq!(credentials.client_secret, Some("secret456".to_string()));
|
||||||
|
assert_eq!(credentials.refresh_token, Some("dGVzdF90b2tlbl92YWx1ZQ==".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sfdx_url_invalid_format() {
|
||||||
|
let url = "https://invalid-format.com";
|
||||||
|
let result = SalesforceCredentials::sfdx_url(url);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), SalesforceAuthError::InvalidSfdxUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sfdx_url_missing_components() {
|
||||||
|
let url = "force://client_id:@login.salesforce.com";
|
||||||
|
let result = SalesforceCredentials::sfdx_url(url);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), SalesforceAuthError::InvalidSfdxUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sfdx_url_invalid_scheme() {
|
||||||
|
let url = "http://client_id:secret:token@login.salesforce.com";
|
||||||
|
let result = SalesforceCredentials::sfdx_url(url);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), SalesforceAuthError::InvalidSfdxUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sfdx_url_with_special_characters() {
|
||||||
|
let url = "force://client.id-123:secret_456:refresh-token.value@test-env.salesforce.com";
|
||||||
|
let result = SalesforceCredentials::sfdx_url(url);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let credentials = result.unwrap();
|
||||||
|
assert_eq!(credentials.client_id, Some("client.id-123".to_string()));
|
||||||
|
assert_eq!(credentials.client_secret, Some("secret_456".to_string()));
|
||||||
|
assert_eq!(credentials.refresh_token, Some("refresh-token.value".to_string()));
|
||||||
|
assert_eq!(credentials.login_url, Some("https://test-env.salesforce.com".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use log::trace;
|
||||||
|
use oauth2::TokenUrl;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::credentials::{
|
||||||
|
http_client, parse_salesforce_identity_ids, required, SalesforceAuthFlow, SalesforceCredentials,
|
||||||
|
};
|
||||||
|
use crate::salesforce_token_response::SalesforceTokenResponse;
|
||||||
|
use crate::{SalesforceAuthError, SalesforceAuthSession, SalesforceAuthToken};
|
||||||
|
|
||||||
|
const DEFAULT_WEB_SERVER_SCOPES: &[&str] = &["api", "refresh_token", "offline_access"];
|
||||||
|
|
||||||
|
const DEFAULT_CALLBACK_RESPONSE: &str = concat!(
|
||||||
|
"<!doctype html>",
|
||||||
|
"<html>",
|
||||||
|
"<head><title>Salesforce Login Complete</title></head>",
|
||||||
|
"<body>",
|
||||||
|
"<h1>Salesforce login complete</h1>",
|
||||||
|
"<p>You can close this browser window and return to your application.</p>",
|
||||||
|
"</body>",
|
||||||
|
"</html>"
|
||||||
|
);
|
||||||
|
|
||||||
|
/// OAuth 2.0 Web Server Flow helper.
|
||||||
|
///
|
||||||
|
/// This type prepares a Salesforce authorization URL, lets the caller decide how
|
||||||
|
/// to open it, then listens for the OAuth callback and exchanges the received
|
||||||
|
/// authorization code for a Salesforce session.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OAuthWebService {
|
||||||
|
login_url: String,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
redirect_uri: String,
|
||||||
|
scopes: Vec<String>,
|
||||||
|
state: String,
|
||||||
|
callback_response: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OAuthWebService {
|
||||||
|
/// Creates a new OAuth Web Server Flow helper.
|
||||||
|
///
|
||||||
|
/// The caller should call [`OAuthWebService::authorization_url`] first, open
|
||||||
|
/// the returned URL in a browser, then call [`OAuthWebService::connect`] to
|
||||||
|
/// wait for the callback and receive a [`SalesforceAuthSession`].
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// - `login_url`: Salesforce login URL (e.g., `https://login.salesforce.com`)
|
||||||
|
/// - `client_id`: Connected app client ID
|
||||||
|
/// - `client_secret`: Optional connected app client secret
|
||||||
|
/// - `redirect_uri`: OAuth callback URL (e.g., `http://localhost:8080/callback`)
|
||||||
|
/// - `scopes`: Optional list of OAuth scopes. Defaults to `["api", "refresh_token", "offline_access"]`
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rustsf_auth::credentials::web_server::OAuthWebService;
|
||||||
|
///
|
||||||
|
/// let service = OAuthWebService::new(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "my-client-id",
|
||||||
|
/// Some("my-client-secret".to_string()),
|
||||||
|
/// "http://localhost:8080/callback",
|
||||||
|
/// None,
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn new(
|
||||||
|
login_url: impl Into<String>,
|
||||||
|
client_id: impl Into<String>,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
redirect_uri: impl Into<String>,
|
||||||
|
scopes: Option<Vec<String>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
login_url: login_url.into(),
|
||||||
|
client_id: client_id.into(),
|
||||||
|
client_secret,
|
||||||
|
redirect_uri: redirect_uri.into(),
|
||||||
|
scopes: scopes.unwrap_or_else(|| {
|
||||||
|
DEFAULT_WEB_SERVER_SCOPES
|
||||||
|
.iter()
|
||||||
|
.map(|scope| (*scope).to_string())
|
||||||
|
.collect()
|
||||||
|
}),
|
||||||
|
state: create_state(),
|
||||||
|
callback_response: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the Salesforce authorization URL.
|
||||||
|
///
|
||||||
|
/// This method does not open a browser. The caller is responsible for opening
|
||||||
|
/// the returned URL or presenting it to the user.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns the authorization URL that the user should visit to grant access.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError::InvalidUrl`] if the login URL cannot be parsed.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rustsf_auth::credentials::web_server::OAuthWebService;
|
||||||
|
///
|
||||||
|
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let service = OAuthWebService::new(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "my-client-id",
|
||||||
|
/// None,
|
||||||
|
/// "http://localhost:8080/callback",
|
||||||
|
/// None,
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// let auth_url = service.authorization_url().await?;
|
||||||
|
/// println!("Visit this URL: {}", auth_url);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub async fn authorization_url(&self) -> Result<String, SalesforceAuthError> {
|
||||||
|
let normalized = self.login_url.trim_end_matches('/');
|
||||||
|
let authorize_url = format!("{normalized}/services/oauth2/authorize");
|
||||||
|
|
||||||
|
let mut url = Url::parse(&authorize_url)
|
||||||
|
.map_err(|source| SalesforceAuthError::InvalidUrl {
|
||||||
|
url: authorize_url,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair("response_type", "code")
|
||||||
|
.append_pair("client_id", &self.client_id)
|
||||||
|
.append_pair("redirect_uri", &self.redirect_uri)
|
||||||
|
.append_pair("scope", &self.scopes.join(" "))
|
||||||
|
.append_pair("state", &self.state)
|
||||||
|
.append_pair("prompt", "login");
|
||||||
|
|
||||||
|
Ok(url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts listening for the OAuth callback and exchanges the authorization
|
||||||
|
/// code for a Salesforce authentication session.
|
||||||
|
///
|
||||||
|
/// Call [`OAuthWebService::authorization_url`] first and open that URL in a
|
||||||
|
/// browser before awaiting this method.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a [`SalesforceAuthSession`] containing the access token and other
|
||||||
|
/// authentication details.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SalesforceAuthError`] if:
|
||||||
|
/// - The redirect URI is invalid or missing a port
|
||||||
|
/// - The TCP listener cannot be bound
|
||||||
|
/// - The OAuth callback is malformed
|
||||||
|
/// - The state parameter doesn't match
|
||||||
|
/// - Token exchange with Salesforce fails
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rustsf_auth::credentials::web_server::OAuthWebService;
|
||||||
|
///
|
||||||
|
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let service = OAuthWebService::new(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "my-client-id",
|
||||||
|
/// Some("my-client-secret".to_string()),
|
||||||
|
/// "http://localhost:8080/callback",
|
||||||
|
/// None,
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// let auth_url = service.authorization_url().await?;
|
||||||
|
/// println!("Visit this URL: {}", auth_url);
|
||||||
|
/// // User opens the URL in a browser and authorizes
|
||||||
|
///
|
||||||
|
/// let session = service.connect().await?;
|
||||||
|
/// println!("Access token: {}", session.access_token().await?);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub async fn connect(&self) -> Result<SalesforceAuthSession, SalesforceAuthError> {
|
||||||
|
let callback_url = Url::parse(&self.redirect_uri)
|
||||||
|
.map_err(|source| SalesforceAuthError::InvalidUrl {
|
||||||
|
url: self.redirect_uri.clone(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let host = callback_url.host_str().unwrap_or("127.0.0.1");
|
||||||
|
let port = callback_url
|
||||||
|
.port_or_known_default()
|
||||||
|
.ok_or(SalesforceAuthError::MissingRequiredField("redirect_uri port"))?;
|
||||||
|
|
||||||
|
let bind_host = if host == "localhost" { "127.0.0.1" } else { host };
|
||||||
|
let listener = TcpListener::bind((bind_host, port)).await?;
|
||||||
|
|
||||||
|
let callback = receive_oauth_callback(
|
||||||
|
listener,
|
||||||
|
self.callback_response.as_deref(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
let callback_state = callback
|
||||||
|
.get("state")
|
||||||
|
.ok_or(SalesforceAuthError::InvalidOAuthCallback)?;
|
||||||
|
|
||||||
|
if callback_state != &self.state {
|
||||||
|
return Err(SalesforceAuthError::OAuthStateMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = callback
|
||||||
|
.get("code")
|
||||||
|
.ok_or(SalesforceAuthError::InvalidOAuthCallback)?;
|
||||||
|
|
||||||
|
let token_response = self.exchange_authorization_code(code).await?;
|
||||||
|
|
||||||
|
self.session_from_token_response(token_response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the HTML response returned to the browser after Salesforce redirects
|
||||||
|
/// back to the local OAuth callback listener.
|
||||||
|
///
|
||||||
|
/// If this method is not called, a default "Salesforce login complete" page is
|
||||||
|
/// returned.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// - `response`: HTML content to display in the browser after successful authentication
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rustsf_auth::credentials::web_server::OAuthWebService;
|
||||||
|
///
|
||||||
|
/// let custom_html = r#"<!doctype html>
|
||||||
|
/// <html>
|
||||||
|
/// <head><title>Success</title></head>
|
||||||
|
/// <body><h1>Authentication successful!</h1></body>
|
||||||
|
/// </html>"#;
|
||||||
|
///
|
||||||
|
/// let service = OAuthWebService::new(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "my-client-id",
|
||||||
|
/// None,
|
||||||
|
/// "http://localhost:8080/callback",
|
||||||
|
/// None,
|
||||||
|
/// ).with_callback_response(custom_html);
|
||||||
|
/// ```
|
||||||
|
pub fn with_callback_response(mut self, response: impl Into<String>) -> Self {
|
||||||
|
self.callback_response = Some(response.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exchange_authorization_code(
|
||||||
|
&self,
|
||||||
|
code: &str,
|
||||||
|
) -> Result<SalesforceTokenResponse, SalesforceAuthError> {
|
||||||
|
let url = self.token_url()?.url().clone();
|
||||||
|
|
||||||
|
let mut data = vec![
|
||||||
|
("grant_type", "authorization_code"),
|
||||||
|
("code", code),
|
||||||
|
("client_id", self.client_id.as_str()),
|
||||||
|
("redirect_uri", self.redirect_uri.as_str()),
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(client_secret) = self.client_secret.as_deref() {
|
||||||
|
data.push(("client_secret", client_secret));
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("Web Server Flow token exchange request: POST {}", url);
|
||||||
|
|
||||||
|
let response = http_client()?
|
||||||
|
.post(url)
|
||||||
|
.form(&data)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
let body = response.text().await.map_err(SalesforceAuthError::Http)?;
|
||||||
|
|
||||||
|
trace!("Web Server Flow token exchange 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()))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_from_token_response(
|
||||||
|
&self,
|
||||||
|
token_response: SalesforceTokenResponse,
|
||||||
|
) -> Result<SalesforceAuthSession, SalesforceAuthError> {
|
||||||
|
let (org_id, user_id) = parse_salesforce_identity_ids(token_response.id.as_deref());
|
||||||
|
|
||||||
|
let instance_url = token_response
|
||||||
|
.instance_url
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| self.login_url.trim_end_matches('/').to_string());
|
||||||
|
|
||||||
|
let credentials = SalesforceCredentials {
|
||||||
|
flow: SalesforceAuthFlow::AccessToken,
|
||||||
|
login_url: Some(self.login_url.clone()),
|
||||||
|
client_id: Some(self.client_id.clone()),
|
||||||
|
client_secret: self.client_secret.clone(),
|
||||||
|
username: None,
|
||||||
|
private_key_pem: None,
|
||||||
|
access_token: Some(token_response.access_token.clone()),
|
||||||
|
refresh_token: token_response.refresh_token.clone(),
|
||||||
|
instance_url: Some(instance_url.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(SalesforceAuthSession {
|
||||||
|
token: RwLock::new(SalesforceAuthToken {
|
||||||
|
access_token: token_response.access_token,
|
||||||
|
token_type: token_response.token_type,
|
||||||
|
issued_at: token_response.issued_at,
|
||||||
|
signature: token_response.signature,
|
||||||
|
}),
|
||||||
|
credentials,
|
||||||
|
instance_url,
|
||||||
|
org_id,
|
||||||
|
user_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_url(&self) -> Result<TokenUrl, SalesforceAuthError> {
|
||||||
|
let normalized = self.login_url.trim_end_matches('/');
|
||||||
|
let url = format!("{normalized}/services/oauth2/token");
|
||||||
|
|
||||||
|
Ok(TokenUrl::new(url.clone())
|
||||||
|
.map_err(|source| SalesforceAuthError::InvalidUrl { url, source })?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_oauth_callback(
|
||||||
|
listener: TcpListener,
|
||||||
|
callback_response: Option<&str>,
|
||||||
|
) -> Result<HashMap<String, String>, SalesforceAuthError> {
|
||||||
|
let (mut stream, _) = listener.accept().await?;
|
||||||
|
|
||||||
|
let mut buffer = vec![0_u8; 8192];
|
||||||
|
let read = stream.read(&mut buffer).await?;
|
||||||
|
let request = String::from_utf8_lossy(&buffer[..read]);
|
||||||
|
|
||||||
|
let request_line = request
|
||||||
|
.lines()
|
||||||
|
.next()
|
||||||
|
.ok_or(SalesforceAuthError::InvalidOAuthCallback)?;
|
||||||
|
|
||||||
|
let path = request_line
|
||||||
|
.split_whitespace()
|
||||||
|
.nth(1)
|
||||||
|
.ok_or(SalesforceAuthError::InvalidOAuthCallback)?;
|
||||||
|
|
||||||
|
let callback_url = Url::parse(&format!("http://localhost{path}"))
|
||||||
|
.map_err(|_| SalesforceAuthError::InvalidOAuthCallback)?;
|
||||||
|
|
||||||
|
let params = callback_url
|
||||||
|
.query_pairs()
|
||||||
|
.map(|(key, value)| (key.to_string(), value.to_string()))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
let body = callback_response.unwrap_or(DEFAULT_CALLBACK_RESPONSE);
|
||||||
|
let response = format!(
|
||||||
|
concat!(
|
||||||
|
"HTTP/1.1 200 OK\r\n",
|
||||||
|
"Content-Type: text/html; charset=utf-8\r\n",
|
||||||
|
"Content-Length: {}\r\n",
|
||||||
|
"Connection: close\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"{}"
|
||||||
|
),
|
||||||
|
body.len(),
|
||||||
|
body
|
||||||
|
);
|
||||||
|
|
||||||
|
stream.write_all(response.as_bytes()).await?;
|
||||||
|
stream.shutdown().await?;
|
||||||
|
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_state() -> String {
|
||||||
|
let nanos = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_nanos())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
format!("rustsf-auth-{nanos}")
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/// Error type returned by Salesforce authentication operations.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use rustsf_auth::{parse_sfdx_url, SalesforceAuthError};
|
||||||
|
///
|
||||||
|
/// let error = parse_sfdx_url("not-a-valid-sfdx-url").unwrap_err();
|
||||||
|
/// assert!(matches!(error, SalesforceAuthError::InvalidSfdxUrl));
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum SalesforceAuthError {
|
||||||
|
#[error("missing required configuration field: {0}")]
|
||||||
|
MissingRequiredField(&'static str),
|
||||||
|
|
||||||
|
/// The SFDX URL did not match the required format.
|
||||||
|
#[error("invalid SFDX URL")]
|
||||||
|
InvalidSfdxUrl,
|
||||||
|
|
||||||
|
/// The OAuth callback request was invalid.
|
||||||
|
#[error("invalid OAuth callback")]
|
||||||
|
InvalidOAuthCallback,
|
||||||
|
|
||||||
|
/// The OAuth callback state did not match the generated state.
|
||||||
|
#[error("OAuth state mismatch")]
|
||||||
|
OAuthStateMismatch,
|
||||||
|
|
||||||
|
/// A URL failed to parse.
|
||||||
|
#[error("invalid URL `{url}`: {source}")]
|
||||||
|
InvalidUrl {
|
||||||
|
/// The URL string that failed to parse.
|
||||||
|
url: String,
|
||||||
|
|
||||||
|
/// The underlying URL parser error.
|
||||||
|
source: url::ParseError,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Error returned by the `oauth2` crate.
|
||||||
|
#[error("OAuth2 error: {0}")]
|
||||||
|
OAuth2(String),
|
||||||
|
|
||||||
|
/// HTTP error returned by `reqwest`.
|
||||||
|
#[error("HTTP error: {0}")]
|
||||||
|
Http(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
/// I/O error returned while running the local OAuth callback server.
|
||||||
|
#[error("I/O error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Token exchange failure: {0}")]
|
||||||
|
TokenExchange(String),
|
||||||
|
|
||||||
|
/// Error creating a Salesforce JWT assertion.
|
||||||
|
#[error("JWT error: {0}")]
|
||||||
|
Jwt(String),
|
||||||
|
}
|
||||||
+83
-959
File diff suppressed because it is too large
Load Diff
+95
-10
@@ -1,7 +1,10 @@
|
|||||||
use rustsf_auth::{authenticate_sfdx_url, SalesforceAuthConfig};
|
use std::process::Command;
|
||||||
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::OAuthWebService;
|
||||||
|
|
||||||
pub const CONNECT_TIMEOUT: u64 = 15;
|
pub const CONNECT_TIMEOUT: u64 = 15;
|
||||||
pub const REQUEST_TIMEOUT: u64 = 30;
|
pub const REQUEST_TIMEOUT: u64 = 30;
|
||||||
|
|
||||||
@@ -14,13 +17,42 @@ pub fn get_http_client() -> Result<reqwest::Client> {
|
|||||||
.context("Failed to build HTTP client")?)
|
.context("Failed to build HTTP client")?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_browser(url: &str) {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let _ = Command::new("cmd")
|
||||||
|
.args(["/C", "start", "", url])
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let _ = Command::new("open")
|
||||||
|
.arg(url)
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
|
let _ = Command::new("xdg-open")
|
||||||
|
.arg(url)
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[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";
|
||||||
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,32 +67,85 @@ async fn main() {
|
|||||||
/// println!("{}", session.access_token);
|
/// println!("{}", session.access_token);
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
|
|
||||||
let config = SalesforceAuthConfig::sfdx_url(sfdx_auth_url);
|
// SFDX AUTH URL
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
// WEB Flow
|
||||||
|
let web_service = OAuthWebService::new(
|
||||||
|
"https://computing-platform-9537--qa.sandbox.my.salesforce.com",
|
||||||
|
"PlatformCLI",
|
||||||
|
None,
|
||||||
|
"http://localhost:1717/OauthRedirect",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.with_callback_response(
|
||||||
|
r#"<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head><title>Authenticated</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Authentication complete</h1>
|
||||||
|
<p>You can close this tab.</p>
|
||||||
|
</body>
|
||||||
|
</html>"#,
|
||||||
|
);
|
||||||
|
let auth_url = web_service.authorization_url().await.unwrap();
|
||||||
|
|
||||||
let session = config.connect().await.unwrap();
|
// Ask user to authenticate themselves
|
||||||
|
println!("Open this URL in your browser if it did not open automatically:\n{auth_url}");
|
||||||
|
open_browser(&auth_url);
|
||||||
|
let session = web_service.connect().await.unwrap();
|
||||||
|
|
||||||
println!("{}", session.access_token);
|
// println!("Config: {:?}", config);
|
||||||
|
|
||||||
|
// let session = config.connect().await.unwrap();
|
||||||
|
|
||||||
|
println!("Instance URL: {}", session.instance_url);
|
||||||
|
println!("Access 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
|
||||||
headers.insert("Accept", HeaderValue::from_static("application/json"));
|
headers.insert("Accept", HeaderValue::from_static("application/json"));
|
||||||
|
|
||||||
|
// Get API Versions
|
||||||
let response = get_http_client().unwrap()
|
let response = get_http_client().unwrap()
|
||||||
.get(format!("{}/services/data", session.instance_url))
|
.get(format!("{}/services/data", session.instance_url))
|
||||||
.headers(headers)
|
.headers(headers.clone())
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
println!("SUCCESS Response: {:?}", response.text().await.unwrap());
|
println!("SUCCESS Response: {:?}", response.text().await.unwrap());
|
||||||
} else {
|
} else {
|
||||||
println!("ERROR Response: {:?}", response.text().await.unwrap());
|
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);
|
||||||
|
headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value).unwrap());
|
||||||
|
|
||||||
|
// Get LIMITs Versions
|
||||||
|
let response = get_http_client().unwrap()
|
||||||
|
.get(format!("{}/services/data/v67.0/limits", session.instance_url))
|
||||||
|
.headers(headers)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if response.status().is_success() {
|
||||||
|
println!("SUCCESS Response 2: {:?}", response.text().await.unwrap());
|
||||||
|
} else {
|
||||||
|
println!("ERROR Response 2: {:?}", response.text().await.unwrap());
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
use crate::salesforce_token_response::SalesforceTokenResponse;
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SalesforceAuthToken {
|
||||||
|
pub(crate) access_token: String,
|
||||||
|
|
||||||
|
/// Token issue timestamp, when returned by Salesforce.
|
||||||
|
pub(crate) issued_at: Option<String>,
|
||||||
|
|
||||||
|
/// OAuth token type, usually `Bearer`.
|
||||||
|
pub(crate) token_type: Option<String>,
|
||||||
|
|
||||||
|
/// Salesforce response signature, when returned by Salesforce.
|
||||||
|
pub(crate) signature: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SalesforceAuthToken {
|
||||||
|
pub fn from_token_response(token_response: SalesforceTokenResponse) -> Self {
|
||||||
|
Self {
|
||||||
|
access_token: token_response.access_token,
|
||||||
|
issued_at: token_response.issued_at,
|
||||||
|
token_type: token_response.token_type,
|
||||||
|
signature: token_response.signature,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn access_token(&self) -> String {
|
||||||
|
self.access_token.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_expired(&self) -> bool {
|
||||||
|
match &self.issued_at {
|
||||||
|
Some(issued_at) => {
|
||||||
|
let timestamp_ms = match issued_at.parse::<u64>() {
|
||||||
|
Ok(ts) => ts,
|
||||||
|
Err(_) => {
|
||||||
|
// SOAP login returns ISO timestamp; can't compare, attempt refresh
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let seconds = timestamp_ms / 1000;
|
||||||
|
let nanos = (timestamp_ms % 1000) * 1_000_000;
|
||||||
|
|
||||||
|
let given_time = UNIX_EPOCH + Duration::new(seconds, nanos as u32);
|
||||||
|
|
||||||
|
let two_hours = Duration::from_secs(2 * 60 * 60); // 2 hours in seconds
|
||||||
|
let modified_time = given_time + two_hours;
|
||||||
|
|
||||||
|
let current_time = SystemTime::now();
|
||||||
|
|
||||||
|
if current_time > modified_time { true } else { false }
|
||||||
|
}
|
||||||
|
None => false, // Assume token is valid, e.g., with the access_token authentication flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Raw OAuth token response returned by Salesforce.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub(crate) struct SalesforceTokenResponse {
|
||||||
|
pub(crate) access_token: String,
|
||||||
|
pub(crate) instance_url: Option<String>,
|
||||||
|
pub(crate) id: Option<String>,
|
||||||
|
pub(crate) token_type: Option<String>,
|
||||||
|
pub(crate) issued_at: Option<String>,
|
||||||
|
pub(crate) signature: Option<String>,
|
||||||
|
pub(crate) refresh_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user