OAuth Web server flow
This commit is contained in:
+13
-6
@@ -9,11 +9,11 @@
|
|||||||
//!
|
//!
|
||||||
//! The user must have API access enabled to be able to make API calls to Salesforce.
|
//! The user must have API access enabled to be able to make API calls to Salesforce.
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use oauth2::http::header::AUTHORIZATION;
|
use oauth2::http::header::AUTHORIZATION;
|
||||||
use oauth2::http::{HeaderMap, HeaderValue};
|
use oauth2::http::{HeaderMap, HeaderValue};
|
||||||
use rustsf_auth::{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;
|
||||||
@@ -57,6 +57,17 @@ async fn main(){
|
|||||||
client_secret,
|
client_secret,
|
||||||
redirect_url,
|
redirect_url,
|
||||||
scopes,
|
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();
|
let auth_url = web_service.authorization_url().await.unwrap();
|
||||||
|
|
||||||
@@ -65,10 +76,6 @@ async fn main(){
|
|||||||
open_browser(&auth_url);
|
open_browser(&auth_url);
|
||||||
let session = web_service.connect().await.unwrap();
|
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
|
// Build the headers to include the access token
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
let auth_value = format!("Bearer {}", session.access_token().await.unwrap());
|
let auth_value = format!("Bearer {}", session.access_token().await.unwrap());
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ 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,8 +35,6 @@ 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,8 +66,6 @@ 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![],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-11
@@ -10,7 +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;
|
pub mod web_server;
|
||||||
|
|
||||||
/// Supported Salesforce OAuth authentication flows.
|
/// Supported Salesforce OAuth authentication flows.
|
||||||
///
|
///
|
||||||
@@ -36,9 +36,6 @@ 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,
|
||||||
}
|
}
|
||||||
@@ -95,12 +92,6 @@ 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 {
|
||||||
@@ -180,7 +171,6 @@ 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,8 +91,6 @@ 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![],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+137
-106
@@ -1,51 +1,55 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::process::Command;
|
use std::sync::RwLock;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use log::trace;
|
use log::trace;
|
||||||
|
use oauth2::TokenUrl;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials};
|
use crate::credentials::{
|
||||||
|
http_client, parse_salesforce_identity_ids, required, SalesforceAuthFlow, SalesforceCredentials,
|
||||||
|
};
|
||||||
use crate::salesforce_token_response::SalesforceTokenResponse;
|
use crate::salesforce_token_response::SalesforceTokenResponse;
|
||||||
use crate::SalesforceAuthError;
|
use crate::{SalesforceAuthError, SalesforceAuthSession, SalesforceAuthToken};
|
||||||
|
|
||||||
const DEFAULT_WEB_SERVER_SCOPES: &[&str] = &["api", "refresh_token", "offline_access"];
|
const DEFAULT_WEB_SERVER_SCOPES: &[&str] = &["api", "refresh_token", "offline_access"];
|
||||||
|
|
||||||
impl SalesforceCredentials {
|
const DEFAULT_CALLBACK_RESPONSE: &str = concat!(
|
||||||
/// Creates a configuration for the OAuth 2.0 Web Server Flow.
|
"<!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 flow opens the Salesforce authorization URL in the user's browser,
|
/// This type prepares a Salesforce authorization URL, lets the caller decide how
|
||||||
/// starts a temporary local callback server, receives the authorization code,
|
/// to open it, then listens for the OAuth callback and exchanges the received
|
||||||
/// and exchanges it for an access token and refresh token.
|
/// 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 connected app must have a callback URL matching `redirect_uri`, for example:
|
/// The caller should call [`OAuthWebService::authorization_url`] first, open
|
||||||
///
|
/// the returned URL in a browser, then call [`OAuthWebService::connect`] to
|
||||||
/// `http://localhost:1717/OauthRedirect`
|
/// wait for the callback and receive a [`SalesforceAuthSession`].
|
||||||
///
|
pub fn new(
|
||||||
/// # 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>,
|
login_url: impl Into<String>,
|
||||||
client_id: impl Into<String>,
|
client_id: impl Into<String>,
|
||||||
client_secret: Option<String>,
|
client_secret: Option<String>,
|
||||||
@@ -53,57 +57,55 @@ impl SalesforceCredentials {
|
|||||||
scopes: Option<Vec<String>>,
|
scopes: Option<Vec<String>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
flow: SalesforceAuthFlow::WebServer,
|
login_url: login_url.into(),
|
||||||
login_url: Some(login_url.into()),
|
client_id: client_id.into(),
|
||||||
client_id: Some(client_id.into()),
|
|
||||||
client_secret,
|
client_secret,
|
||||||
username: None,
|
redirect_uri: redirect_uri.into(),
|
||||||
private_key_pem: None,
|
|
||||||
access_token: None,
|
|
||||||
refresh_token: None,
|
|
||||||
instance_url: None,
|
|
||||||
redirect_uri: Some(redirect_uri.into()),
|
|
||||||
scopes: scopes.unwrap_or_else(|| {
|
scopes: scopes.unwrap_or_else(|| {
|
||||||
DEFAULT_WEB_SERVER_SCOPES
|
DEFAULT_WEB_SERVER_SCOPES
|
||||||
.iter()
|
.iter()
|
||||||
.map(|scope| (*scope).to_string())
|
.map(|scope| (*scope).to_string())
|
||||||
.collect()
|
.collect()
|
||||||
}),
|
}),
|
||||||
|
state: create_state(),
|
||||||
|
callback_response: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the Salesforce authorization URL for the Web Server Flow.
|
/// Builds the Salesforce authorization URL.
|
||||||
///
|
///
|
||||||
/// This is useful if callers want to present or open the URL themselves.
|
/// This method does not open a browser. The caller is responsible for opening
|
||||||
pub fn web_server_authorization_url(&self, state: &str) -> Result<String, SalesforceAuthError> {
|
/// the returned URL or presenting it to the user.
|
||||||
let login_url = required(self.login_url.as_deref(), "login_url")?;
|
pub async fn authorization_url(&self) -> Result<String, SalesforceAuthError> {
|
||||||
let client_id = required(self.client_id.as_deref(), "client_id")?;
|
let normalized = self.login_url.trim_end_matches('/');
|
||||||
let redirect_uri = required(self.redirect_uri.as_deref(), "redirect_uri")?;
|
let authorize_url = format!("{normalized}/services/oauth2/authorize");
|
||||||
|
|
||||||
let normalized = login_url.trim_end_matches('/');
|
let mut url = Url::parse(&authorize_url)
|
||||||
let mut url = Url::parse(&format!("{normalized}/services/oauth2/authorize"))
|
|
||||||
.map_err(|source| SalesforceAuthError::InvalidUrl {
|
.map_err(|source| SalesforceAuthError::InvalidUrl {
|
||||||
url: format!("{normalized}/services/oauth2/authorize"),
|
url: authorize_url,
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
url.query_pairs_mut()
|
url.query_pairs_mut()
|
||||||
.append_pair("response_type", "code")
|
.append_pair("response_type", "code")
|
||||||
.append_pair("client_id", client_id)
|
.append_pair("client_id", &self.client_id)
|
||||||
.append_pair("redirect_uri", redirect_uri)
|
.append_pair("redirect_uri", &self.redirect_uri)
|
||||||
.append_pair("scope", &self.scopes.join(" "))
|
.append_pair("scope", &self.scopes.join(" "))
|
||||||
.append_pair("state", state)
|
.append_pair("state", &self.state)
|
||||||
.append_pair("prompt", "login");
|
.append_pair("prompt", "login");
|
||||||
|
|
||||||
Ok(url.to_string())
|
Ok(url.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn connect_web_server(&self) -> Result<SalesforceTokenResponse, SalesforceAuthError> {
|
/// Starts listening for the OAuth callback and exchanges the authorization
|
||||||
let state = create_state();
|
/// code for a Salesforce authentication session.
|
||||||
let auth_url = self.web_server_authorization_url(&state)?;
|
///
|
||||||
let redirect_uri = required(self.redirect_uri.as_deref(), "redirect_uri")?;
|
/// Call [`OAuthWebService::authorization_url`] first and open that URL in a
|
||||||
let callback_url = Url::parse(redirect_uri).map_err(|source| SalesforceAuthError::InvalidUrl {
|
/// browser before awaiting this method.
|
||||||
url: redirect_uri.to_string(),
|
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,
|
source,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -115,18 +117,16 @@ impl SalesforceCredentials {
|
|||||||
let bind_host = if host == "localhost" { "127.0.0.1" } else { host };
|
let bind_host = if host == "localhost" { "127.0.0.1" } else { host };
|
||||||
let listener = TcpListener::bind((bind_host, port)).await?;
|
let listener = TcpListener::bind((bind_host, port)).await?;
|
||||||
|
|
||||||
trace!("Web Server Flow authorization URL: {}", auth_url);
|
let callback = receive_oauth_callback(
|
||||||
open_browser(&auth_url);
|
listener,
|
||||||
println!("Open this URL in your browser if it did not open automatically:\n{auth_url}");
|
self.callback_response.as_deref(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let callback = receive_oauth_callback(listener).await?;
|
|
||||||
let callback_state = callback
|
let callback_state = callback
|
||||||
.get("state")
|
.get("state")
|
||||||
.ok_or(SalesforceAuthError::InvalidOAuthCallback)?;
|
.ok_or(SalesforceAuthError::InvalidOAuthCallback)?;
|
||||||
|
|
||||||
if callback_state != &state {
|
if callback_state != &self.state {
|
||||||
return Err(SalesforceAuthError::OAuthStateMismatch);
|
return Err(SalesforceAuthError::OAuthStateMismatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,23 +134,32 @@ impl SalesforceCredentials {
|
|||||||
.get("code")
|
.get("code")
|
||||||
.ok_or(SalesforceAuthError::InvalidOAuthCallback)?;
|
.ok_or(SalesforceAuthError::InvalidOAuthCallback)?;
|
||||||
|
|
||||||
self.exchange_authorization_code(code).await
|
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.
|
||||||
|
pub fn with_callback_response(mut self, response: impl Into<String>) -> Self {
|
||||||
|
self.callback_response = Some(response.into());
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn exchange_authorization_code(
|
async fn exchange_authorization_code(
|
||||||
&self,
|
&self,
|
||||||
code: &str,
|
code: &str,
|
||||||
) -> Result<SalesforceTokenResponse, SalesforceAuthError> {
|
) -> 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 url = self.token_url()?.url().clone();
|
||||||
|
|
||||||
let mut data = vec![
|
let mut data = vec![
|
||||||
("grant_type", "authorization_code"),
|
("grant_type", "authorization_code"),
|
||||||
("code", code),
|
("code", code),
|
||||||
("client_id", client_id),
|
("client_id", self.client_id.as_str()),
|
||||||
("redirect_uri", redirect_uri),
|
("redirect_uri", self.redirect_uri.as_str()),
|
||||||
];
|
];
|
||||||
|
|
||||||
if let Some(client_secret) = self.client_secret.as_deref() {
|
if let Some(client_secret) = self.client_secret.as_deref() {
|
||||||
@@ -177,10 +186,56 @@ impl SalesforceCredentials {
|
|||||||
Ok(serde_json::from_str::<SalesforceTokenResponse>(&body)
|
Ok(serde_json::from_str::<SalesforceTokenResponse>(&body)
|
||||||
.map_err(|e| SalesforceAuthError::TokenExchange(e.to_string()))?)
|
.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(
|
async fn receive_oauth_callback(
|
||||||
listener: TcpListener,
|
listener: TcpListener,
|
||||||
|
callback_response: Option<&str>,
|
||||||
) -> Result<HashMap<String, String>, SalesforceAuthError> {
|
) -> Result<HashMap<String, String>, SalesforceAuthError> {
|
||||||
let (mut stream, _) = listener.accept().await?;
|
let (mut stream, _) = listener.accept().await?;
|
||||||
|
|
||||||
@@ -206,19 +261,18 @@ async fn receive_oauth_callback(
|
|||||||
.map(|(key, value)| (key.to_string(), value.to_string()))
|
.map(|(key, value)| (key.to_string(), value.to_string()))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
let response = concat!(
|
let body = callback_response.unwrap_or(DEFAULT_CALLBACK_RESPONSE);
|
||||||
|
let response = format!(
|
||||||
|
concat!(
|
||||||
"HTTP/1.1 200 OK\r\n",
|
"HTTP/1.1 200 OK\r\n",
|
||||||
"Content-Type: text/html; charset=utf-8\r\n",
|
"Content-Type: text/html; charset=utf-8\r\n",
|
||||||
|
"Content-Length: {}\r\n",
|
||||||
"Connection: close\r\n",
|
"Connection: close\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"<!doctype html>",
|
"{}"
|
||||||
"<html>",
|
),
|
||||||
"<head><title>Salesforce Login Complete</title></head>",
|
body.len(),
|
||||||
"<body>",
|
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.write_all(response.as_bytes()).await?;
|
||||||
@@ -235,26 +289,3 @@ fn create_state() -> String {
|
|||||||
|
|
||||||
format!("rustsf-auth-{nanos}")
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@ use self::salesforce_auth_token::SalesforceAuthToken;
|
|||||||
|
|
||||||
pub use self::credentials::{SalesforceAuthFlow, SalesforceCredentials};
|
pub use self::credentials::{SalesforceAuthFlow, SalesforceCredentials};
|
||||||
pub use self::credentials::sfdx_auth_url::SfdxAuthJson;
|
pub use self::credentials::sfdx_auth_url::SfdxAuthJson;
|
||||||
|
pub use self::credentials::web_server::OAuthWebService;
|
||||||
|
|
||||||
/// The default Salesforce production login URL.
|
/// The default Salesforce production login URL.
|
||||||
///
|
///
|
||||||
|
|||||||
+43
-3
@@ -1,7 +1,9 @@
|
|||||||
|
use std::process::Command;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
|
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
|
||||||
use rustsf_auth::credentials::SalesforceCredentials;
|
use rustsf_auth::credentials::SalesforceCredentials;
|
||||||
|
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;
|
||||||
@@ -15,6 +17,29 @@ 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!");
|
||||||
@@ -53,18 +78,33 @@ async fn main() {
|
|||||||
);
|
);
|
||||||
*/
|
*/
|
||||||
// WEB Flow
|
// WEB Flow
|
||||||
let config = SalesforceCredentials::web_server(
|
let web_service = OAuthWebService::new(
|
||||||
"https://computing-platform-9537--qa.sandbox.my.salesforce.com",
|
"https://computing-platform-9537--qa.sandbox.my.salesforce.com",
|
||||||
"PlatformCLI",
|
"PlatformCLI",
|
||||||
None,
|
None,
|
||||||
"http://localhost:1717/OauthRedirect",
|
"http://localhost:1717/OauthRedirect",
|
||||||
None,
|
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();
|
||||||
|
|
||||||
|
// 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!("Config: {:?}", config);
|
// println!("Config: {:?}", config);
|
||||||
|
|
||||||
let session = config.connect().await.unwrap();
|
// let session = config.connect().await.unwrap();
|
||||||
|
|
||||||
println!("Instance URL: {}", session.instance_url);
|
println!("Instance URL: {}", session.instance_url);
|
||||||
println!("Access token: {}", session.access_token().await.unwrap());
|
println!("Access token: {}", session.access_token().await.unwrap());
|
||||||
|
|||||||
Reference in New Issue
Block a user