initial working version of web server flow
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@ name = "rustsf_auth"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Wim Velzeboer <wim@velzeboer.nl>"]
|
authors = ["Wim Velzeboer <wim@velzeboer.nl>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Salesforce OAuth2 authentication SDK for Rust, supporting JWT, sfdxAuthUrl, ClientCredentials and Web"
|
description = "Salesforce OAuth2 authentication SDK for Rust, supporting; JWT, sfdxAuthUrl, ClientCredentials and Web Server flow"
|
||||||
documentation = "https://docs.rs/rustsf"
|
documentation = "https://docs.rs/rustsf"
|
||||||
homepage = "https://github.com/wimvelzeboer/rustsf"
|
homepage = "https://github.com/wimvelzeboer/rustsf"
|
||||||
repository = "https://github.com/wimvelzeboer/rustsf"
|
repository = "https://github.com/wimvelzeboer/rustsf"
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
[](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,
|
||||||
|
(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();
|
||||||
|
```
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//! RustSF Authentication library - SFDX Auth URL Example
|
//! RustSF Authentication library - OAuth2.0 SFDX Auth URL Example
|
||||||
//!
|
//!
|
||||||
//! In this example, we demonstrate how to use the RustSF Authentication library to authenticate
|
//! In this example, we demonstrate how to use the RustSF Authentication library to authenticate
|
||||||
//! with Salesforce using an SFDX auth URL.
|
//! with Salesforce using an SFDX auth URL.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! RustSF Authentication library - SFDX Auth URL Example
|
//! RustSF Authentication library - OAuth2.0 SFDX Auth URL Example
|
||||||
//!
|
//!
|
||||||
//! In this example, we demonstrate how to use the RustSF Authentication library to authenticate
|
//! In this example, we demonstrate how to use the RustSF Authentication library to authenticate
|
||||||
//! with Salesforce using an SFDX auth URL.
|
//! with Salesforce using an SFDX auth URL.
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
use oauth2::http::header::AUTHORIZATION;
|
use oauth2::http::header::AUTHORIZATION;
|
||||||
use oauth2::http::{HeaderMap, HeaderValue};
|
use oauth2::http::{HeaderMap, HeaderValue};
|
||||||
use rustsf_auth::{SalesforceCredentials, SfdxAuthJson};
|
use rustsf_auth::{SalesforceCredentials, SfdxAuthJson};
|
||||||
use serde::Deserialize;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
pub const CONNECT_TIMEOUT: u64 = 15;
|
pub const CONNECT_TIMEOUT: u64 = 15;
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
//! 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::{SalesforceCredentials};
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Constructing the authentication session and connecting to Salesforce
|
||||||
|
// This will open the default browser and wait for the user to complete the authentication process.
|
||||||
|
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,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."
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -44,6 +44,8 @@ impl SalesforceCredentials {
|
|||||||
access_token: Some(access_token.into()),
|
access_token: Some(access_token.into()),
|
||||||
refresh_token,
|
refresh_token,
|
||||||
instance_url: Some(instance_url.into()),
|
instance_url: Some(instance_url.into()),
|
||||||
|
redirect_uri: None,
|
||||||
|
scopes: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ impl SalesforceCredentials {
|
|||||||
access_token: None,
|
access_token: None,
|
||||||
refresh_token: None,
|
refresh_token: None,
|
||||||
instance_url: None,
|
instance_url: None,
|
||||||
|
redirect_uri: None,
|
||||||
|
scopes: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ impl SalesforceCredentials {
|
|||||||
access_token: None,
|
access_token: None,
|
||||||
refresh_token: None,
|
refresh_token: None,
|
||||||
instance_url: None,
|
instance_url: None,
|
||||||
|
redirect_uri: None,
|
||||||
|
scopes: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -10,6 +10,7 @@ mod access_token;
|
|||||||
mod client_credentials;
|
mod client_credentials;
|
||||||
mod jwt_bearer;
|
mod jwt_bearer;
|
||||||
pub(crate) mod sfdx_auth_url;
|
pub(crate) mod sfdx_auth_url;
|
||||||
|
mod web_server;
|
||||||
|
|
||||||
/// Supported Salesforce OAuth authentication flows.
|
/// Supported Salesforce OAuth authentication flows.
|
||||||
///
|
///
|
||||||
@@ -35,6 +36,9 @@ pub enum SalesforceAuthFlow {
|
|||||||
/// Authenticate from an SFDX auth URL.
|
/// Authenticate from an SFDX auth URL.
|
||||||
SfdxUrl,
|
SfdxUrl,
|
||||||
|
|
||||||
|
/// Authenticate using the OAuth 2.0 Web Server Flow.
|
||||||
|
WebServer,
|
||||||
|
|
||||||
/// Use an already available Salesforce access token.
|
/// Use an already available Salesforce access token.
|
||||||
AccessToken,
|
AccessToken,
|
||||||
}
|
}
|
||||||
@@ -91,6 +95,12 @@ pub struct SalesforceCredentials {
|
|||||||
|
|
||||||
/// Salesforce instance URL, for example `https://example.my.salesforce.com`.
|
/// Salesforce instance URL, for example `https://example.my.salesforce.com`.
|
||||||
pub instance_url: Option<String>,
|
pub instance_url: Option<String>,
|
||||||
|
|
||||||
|
/// OAuth redirect URI used by the Web Server Flow.
|
||||||
|
pub redirect_uri: Option<String>,
|
||||||
|
|
||||||
|
/// OAuth scopes requested by the Web Server Flow.
|
||||||
|
pub scopes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SalesforceCredentials {
|
impl SalesforceCredentials {
|
||||||
@@ -140,6 +150,11 @@ impl SalesforceCredentials {
|
|||||||
(_, _, _) => "http://login.salesforce.com",
|
(_, _, _) => "http://login.salesforce.com",
|
||||||
}.to_string();
|
}.to_string();
|
||||||
|
|
||||||
|
let mut credentials = self;
|
||||||
|
if credentials.refresh_token.is_none() {
|
||||||
|
credentials.refresh_token = token_response.refresh_token.clone();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(SalesforceAuthSession {
|
Ok(SalesforceAuthSession {
|
||||||
token: RwLock::new(SalesforceAuthToken {
|
token: RwLock::new(SalesforceAuthToken {
|
||||||
access_token: token_response.access_token,
|
access_token: token_response.access_token,
|
||||||
@@ -147,7 +162,7 @@ impl SalesforceCredentials {
|
|||||||
issued_at: token_response.issued_at,
|
issued_at: token_response.issued_at,
|
||||||
signature: token_response.signature,
|
signature: token_response.signature,
|
||||||
}),
|
}),
|
||||||
credentials: self,
|
credentials,
|
||||||
instance_url,
|
instance_url,
|
||||||
org_id,
|
org_id,
|
||||||
user_id,
|
user_id,
|
||||||
@@ -165,6 +180,7 @@ impl SalesforceCredentials {
|
|||||||
SalesforceAuthFlow::ClientCredentials => self.connect_client_credentials().await,
|
SalesforceAuthFlow::ClientCredentials => self.connect_client_credentials().await,
|
||||||
SalesforceAuthFlow::JwtBearer => self.connect_jwt().await,
|
SalesforceAuthFlow::JwtBearer => self.connect_jwt().await,
|
||||||
SalesforceAuthFlow::SfdxUrl => self.connect_sfdx_url().await,
|
SalesforceAuthFlow::SfdxUrl => self.connect_sfdx_url().await,
|
||||||
|
SalesforceAuthFlow::WebServer => self.connect_web_server().await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ impl SalesforceCredentials {
|
|||||||
access_token: None,
|
access_token: None,
|
||||||
refresh_token: Some(refresh_token),
|
refresh_token: Some(refresh_token),
|
||||||
instance_url: None,
|
instance_url: None,
|
||||||
|
redirect_uri: None,
|
||||||
|
scopes: vec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use log::trace;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials};
|
||||||
|
use crate::salesforce_token_response::SalesforceTokenResponse;
|
||||||
|
use crate::SalesforceAuthError;
|
||||||
|
|
||||||
|
const DEFAULT_WEB_SERVER_SCOPES: &[&str] = &["api", "refresh_token", "offline_access"];
|
||||||
|
|
||||||
|
impl SalesforceCredentials {
|
||||||
|
/// Creates a configuration for the OAuth 2.0 Web Server Flow.
|
||||||
|
///
|
||||||
|
/// This flow opens the Salesforce authorization URL in the user's browser,
|
||||||
|
/// starts a temporary local callback server, receives the authorization code,
|
||||||
|
/// and exchanges it for an access token and refresh token.
|
||||||
|
///
|
||||||
|
/// The connected app must have a callback URL matching `redirect_uri`, for example:
|
||||||
|
///
|
||||||
|
/// `http://localhost:1717/OauthRedirect`
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rustsf_auth::{SalesforceAuthFlow, SalesforceCredentials};
|
||||||
|
///
|
||||||
|
/// # async fn example() -> Result<(), rustsf_auth::SalesforceAuthError> {
|
||||||
|
/// let config = SalesforceCredentials::web_server(
|
||||||
|
/// "https://login.salesforce.com",
|
||||||
|
/// "client-id",
|
||||||
|
/// Some("client-secret".to_string()),
|
||||||
|
/// "http://localhost:1717/OauthRedirect",
|
||||||
|
/// None,
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(config.flow, SalesforceAuthFlow::WebServer);
|
||||||
|
///
|
||||||
|
/// let session = config.connect().await?;
|
||||||
|
/// println!("{}", session.access_token().await?);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn web_server(
|
||||||
|
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 {
|
||||||
|
flow: SalesforceAuthFlow::WebServer,
|
||||||
|
login_url: Some(login_url.into()),
|
||||||
|
client_id: Some(client_id.into()),
|
||||||
|
client_secret,
|
||||||
|
username: None,
|
||||||
|
private_key_pem: None,
|
||||||
|
access_token: None,
|
||||||
|
refresh_token: None,
|
||||||
|
instance_url: None,
|
||||||
|
redirect_uri: Some(redirect_uri.into()),
|
||||||
|
scopes: scopes.unwrap_or_else(|| {
|
||||||
|
DEFAULT_WEB_SERVER_SCOPES
|
||||||
|
.iter()
|
||||||
|
.map(|scope| (*scope).to_string())
|
||||||
|
.collect()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the Salesforce authorization URL for the Web Server Flow.
|
||||||
|
///
|
||||||
|
/// This is useful if callers want to present or open the URL themselves.
|
||||||
|
pub fn web_server_authorization_url(&self, state: &str) -> Result<String, SalesforceAuthError> {
|
||||||
|
let login_url = required(self.login_url.as_deref(), "login_url")?;
|
||||||
|
let client_id = required(self.client_id.as_deref(), "client_id")?;
|
||||||
|
let redirect_uri = required(self.redirect_uri.as_deref(), "redirect_uri")?;
|
||||||
|
|
||||||
|
let normalized = login_url.trim_end_matches('/');
|
||||||
|
let mut url = Url::parse(&format!("{normalized}/services/oauth2/authorize"))
|
||||||
|
.map_err(|source| SalesforceAuthError::InvalidUrl {
|
||||||
|
url: format!("{normalized}/services/oauth2/authorize"),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair("response_type", "code")
|
||||||
|
.append_pair("client_id", client_id)
|
||||||
|
.append_pair("redirect_uri", redirect_uri)
|
||||||
|
.append_pair("scope", &self.scopes.join(" "))
|
||||||
|
.append_pair("state", state)
|
||||||
|
.append_pair("prompt", "login");
|
||||||
|
|
||||||
|
Ok(url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn connect_web_server(&self) -> Result<SalesforceTokenResponse, SalesforceAuthError> {
|
||||||
|
let state = create_state();
|
||||||
|
let auth_url = self.web_server_authorization_url(&state)?;
|
||||||
|
let redirect_uri = required(self.redirect_uri.as_deref(), "redirect_uri")?;
|
||||||
|
let callback_url = Url::parse(redirect_uri).map_err(|source| SalesforceAuthError::InvalidUrl {
|
||||||
|
url: redirect_uri.to_string(),
|
||||||
|
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?;
|
||||||
|
|
||||||
|
trace!("Web Server Flow authorization URL: {}", auth_url);
|
||||||
|
open_browser(&auth_url);
|
||||||
|
println!("Open this URL in your browser if it did not open automatically:\n{auth_url}");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let callback = receive_oauth_callback(listener).await?;
|
||||||
|
let callback_state = callback
|
||||||
|
.get("state")
|
||||||
|
.ok_or(SalesforceAuthError::InvalidOAuthCallback)?;
|
||||||
|
|
||||||
|
if callback_state != &state {
|
||||||
|
return Err(SalesforceAuthError::OAuthStateMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = callback
|
||||||
|
.get("code")
|
||||||
|
.ok_or(SalesforceAuthError::InvalidOAuthCallback)?;
|
||||||
|
|
||||||
|
self.exchange_authorization_code(code).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exchange_authorization_code(
|
||||||
|
&self,
|
||||||
|
code: &str,
|
||||||
|
) -> Result<SalesforceTokenResponse, SalesforceAuthError> {
|
||||||
|
let client_id = required(self.client_id.as_deref(), "client_id")?;
|
||||||
|
let redirect_uri = required(self.redirect_uri.as_deref(), "redirect_uri")?;
|
||||||
|
|
||||||
|
let url = self.token_url()?.url().clone();
|
||||||
|
|
||||||
|
let mut data = vec![
|
||||||
|
("grant_type", "authorization_code"),
|
||||||
|
("code", code),
|
||||||
|
("client_id", client_id),
|
||||||
|
("redirect_uri", redirect_uri),
|
||||||
|
];
|
||||||
|
|
||||||
|
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()))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_oauth_callback(
|
||||||
|
listener: TcpListener,
|
||||||
|
) -> 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 response = concat!(
|
||||||
|
"HTTP/1.1 200 OK\r\n",
|
||||||
|
"Content-Type: text/html; charset=utf-8\r\n",
|
||||||
|
"Connection: close\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"<!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>"
|
||||||
|
);
|
||||||
|
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,14 @@ pub enum SalesforceAuthError {
|
|||||||
#[error("invalid SFDX URL")]
|
#[error("invalid SFDX URL")]
|
||||||
InvalidSfdxUrl,
|
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.
|
/// A URL failed to parse.
|
||||||
#[error("invalid URL `{url}`: {source}")]
|
#[error("invalid URL `{url}`: {source}")]
|
||||||
InvalidUrl {
|
InvalidUrl {
|
||||||
@@ -36,6 +44,10 @@ pub enum SalesforceAuthError {
|
|||||||
#[error("HTTP error: {0}")]
|
#[error("HTTP error: {0}")]
|
||||||
Http(#[from] reqwest::Error),
|
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}")]
|
#[error("Token exchange failure: {0}")]
|
||||||
TokenExchange(String),
|
TokenExchange(String),
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ pub mod salesforce_token_response;
|
|||||||
use self::error::SalesforceAuthError;
|
use self::error::SalesforceAuthError;
|
||||||
use self::salesforce_auth_token::SalesforceAuthToken;
|
use self::salesforce_auth_token::SalesforceAuthToken;
|
||||||
|
|
||||||
pub use self::credentials::SalesforceCredentials;
|
pub use self::credentials::{SalesforceAuthFlow, SalesforceCredentials};
|
||||||
pub use self::credentials::sfdx_auth_url::SfdxAuthJson;
|
pub use self::credentials::sfdx_auth_url::SfdxAuthJson;
|
||||||
|
|
||||||
/// The default Salesforce production login URL.
|
/// The default Salesforce production login URL.
|
||||||
|
|||||||
+14
-3
@@ -44,19 +44,30 @@ async fn main() {
|
|||||||
|
|
||||||
// SFDX AUTH URL
|
// SFDX AUTH URL
|
||||||
// let config = SalesforceCredentials::sfdx_url(sfdx_auth_url).unwrap();
|
// let config = SalesforceCredentials::sfdx_url(sfdx_auth_url).unwrap();
|
||||||
// /*
|
/*
|
||||||
// CLIENT CREDENTIALS
|
// CLIENT CREDENTIALS
|
||||||
let config = SalesforceCredentials::client_credentials(
|
let config = SalesforceCredentials::client_credentials(
|
||||||
"https://computing-platform-9537--qa.sandbox.my.salesforce.com",
|
"https://computing-platform-9537--qa.sandbox.my.salesforce.com",
|
||||||
customer_id,
|
customer_id,
|
||||||
customer_secret,
|
customer_secret,
|
||||||
);
|
);
|
||||||
// */
|
*/
|
||||||
|
// WEB Flow
|
||||||
|
let config = SalesforceCredentials::web_server(
|
||||||
|
"https://computing-platform-9537--qa.sandbox.my.salesforce.com",
|
||||||
|
"PlatformCLI",
|
||||||
|
None,
|
||||||
|
"http://localhost:1717/OauthRedirect",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
println!("Config: {:?}", config);
|
println!("Config: {:?}", config);
|
||||||
|
|
||||||
let session = config.connect().await.unwrap();
|
let session = config.connect().await.unwrap();
|
||||||
|
|
||||||
println!("token: {}", session.access_token().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();
|
||||||
|
|||||||
Reference in New Issue
Block a user