diff --git a/Cargo.toml b/Cargo.toml index fb412a6..fe9a1fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "rustsf_auth" version = "0.1.0" authors = ["Wim Velzeboer "] 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" homepage = "https://github.com/wimvelzeboer/rustsf" repository = "https://github.com/wimvelzeboer/rustsf" diff --git a/README.md b/README.md index e69de29..ae7a401 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,127 @@ +[![crate-name at crates.io](https://img.shields.io/crates/v/rustsf_auth.svg)](https://crates.io/crates/rustsf) +[![crate-name at docs.rs](https://docs.rs/rustsf_auth/badge.svg)](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(); +``` diff --git a/examples/client_credentials.rs b/examples/client_credentials.rs index e69de29..4dd8570 100644 --- a/examples/client_credentials.rs +++ b/examples/client_credentials.rs @@ -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()); + } +} \ No newline at end of file diff --git a/examples/sfdx_url.rs b/examples/sfdx_url.rs index 37df660..d8133ef 100644 --- a/examples/sfdx_url.rs +++ b/examples/sfdx_url.rs @@ -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 //! with Salesforce using an SFDX auth URL. diff --git a/examples/sfdx_url_json.rs b/examples/sfdx_url_json.rs index 3f06e7c..e9e9e52 100644 --- a/examples/sfdx_url_json.rs +++ b/examples/sfdx_url_json.rs @@ -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 //! with Salesforce using an SFDX auth URL. @@ -20,7 +20,6 @@ use oauth2::http::header::AUTHORIZATION; use oauth2::http::{HeaderMap, HeaderValue}; use rustsf_auth::{SalesforceCredentials, SfdxAuthJson}; -use serde::Deserialize; use std::fs; pub const CONNECT_TIMEOUT: u64 = 15; diff --git a/examples/web_server.rs b/examples/web_server.rs index e69de29..a7fa91f 100644 --- a/examples/web_server.rs +++ b/examples/web_server.rs @@ -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()); + } +} \ No newline at end of file diff --git a/sfdx_auth_url.json b/sfdx_auth_url.json new file mode 100644 index 0000000..aba0422 --- /dev/null +++ b/sfdx_auth_url.json @@ -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." + ] +} diff --git a/src/credentials/access_token.rs b/src/credentials/access_token.rs index 0c3c5d9..894d722 100644 --- a/src/credentials/access_token.rs +++ b/src/credentials/access_token.rs @@ -44,6 +44,8 @@ impl SalesforceCredentials { access_token: Some(access_token.into()), refresh_token, instance_url: Some(instance_url.into()), + redirect_uri: None, + scopes: vec![], } } diff --git a/src/credentials/client_credentials.rs b/src/credentials/client_credentials.rs index 6bfe676..f1e58ab 100644 --- a/src/credentials/client_credentials.rs +++ b/src/credentials/client_credentials.rs @@ -35,6 +35,8 @@ impl SalesforceCredentials { access_token: None, refresh_token: None, instance_url: None, + redirect_uri: None, + scopes: vec![], } } diff --git a/src/credentials/jwt_bearer.rs b/src/credentials/jwt_bearer.rs index e48a585..c9ebb50 100644 --- a/src/credentials/jwt_bearer.rs +++ b/src/credentials/jwt_bearer.rs @@ -66,6 +66,8 @@ impl SalesforceCredentials { access_token: None, refresh_token: None, instance_url: None, + redirect_uri: None, + scopes: vec![], } } diff --git a/src/credentials/mod.rs b/src/credentials/mod.rs index acbfcd2..144c82d 100644 --- a/src/credentials/mod.rs +++ b/src/credentials/mod.rs @@ -10,6 +10,7 @@ mod access_token; mod client_credentials; mod jwt_bearer; pub(crate) mod sfdx_auth_url; +mod web_server; /// Supported Salesforce OAuth authentication flows. /// @@ -35,6 +36,9 @@ pub enum SalesforceAuthFlow { /// Authenticate from an SFDX auth URL. SfdxUrl, + /// Authenticate using the OAuth 2.0 Web Server Flow. + WebServer, + /// Use an already available Salesforce access token. AccessToken, } @@ -91,6 +95,12 @@ pub struct SalesforceCredentials { /// Salesforce instance URL, for example `https://example.my.salesforce.com`. pub instance_url: Option, + + /// OAuth redirect URI used by the Web Server Flow. + pub redirect_uri: Option, + + /// OAuth scopes requested by the Web Server Flow. + pub scopes: Vec, } impl SalesforceCredentials { @@ -140,6 +150,11 @@ impl SalesforceCredentials { (_, _, _) => "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, @@ -147,7 +162,7 @@ impl SalesforceCredentials { issued_at: token_response.issued_at, signature: token_response.signature, }), - credentials: self, + credentials, instance_url, org_id, user_id, @@ -165,6 +180,7 @@ impl SalesforceCredentials { SalesforceAuthFlow::ClientCredentials => self.connect_client_credentials().await, SalesforceAuthFlow::JwtBearer => self.connect_jwt().await, SalesforceAuthFlow::SfdxUrl => self.connect_sfdx_url().await, + SalesforceAuthFlow::WebServer => self.connect_web_server().await, } } diff --git a/src/credentials/sfdx_auth_url.rs b/src/credentials/sfdx_auth_url.rs index 1032fa2..0e14926 100644 --- a/src/credentials/sfdx_auth_url.rs +++ b/src/credentials/sfdx_auth_url.rs @@ -91,6 +91,8 @@ impl SalesforceCredentials { access_token: None, refresh_token: Some(refresh_token), instance_url: None, + redirect_uri: None, + scopes: vec![], }) } diff --git a/src/credentials/web_server.rs b/src/credentials/web_server.rs index e69de29..332e2b1 100644 --- a/src/credentials/web_server.rs +++ b/src/credentials/web_server.rs @@ -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, + client_id: impl Into, + client_secret: Option, + redirect_uri: impl Into, + scopes: Option>, + ) -> 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 { + 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 { + 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 { + 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::(&body) + .map_err(|e| SalesforceAuthError::TokenExchange(e.to_string()))?) + } +} + +async fn receive_oauth_callback( + listener: TcpListener, +) -> Result, 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::>(); + + 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", + "", + "", + "Salesforce Login Complete", + "", + "

Salesforce login complete

", + "

You can close this browser window and return to your application.

", + "", + "" + ); + + 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(); + } +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index e61b143..de2e121 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,6 +18,14 @@ pub enum SalesforceAuthError { #[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 { @@ -36,6 +44,10 @@ pub enum SalesforceAuthError { #[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), diff --git a/src/lib.rs b/src/lib.rs index 36a11f8..77386df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub mod salesforce_token_response; use self::error::SalesforceAuthError; 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; /// The default Salesforce production login URL. diff --git a/src/main.rs b/src/main.rs index a75eb75..cdac932 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,19 +44,30 @@ async fn main() { // 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 config = SalesforceCredentials::web_server( + "https://computing-platform-9537--qa.sandbox.my.salesforce.com", + "PlatformCLI", + None, + "http://localhost:1717/OauthRedirect", + None, + ); + + println!("Config: {:?}", config); 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();