Working SFDX Auth URL and Client Credentials

This commit is contained in:
Wim Velzeboer
2026-09-21 21:09:09 +01:00
parent c1c69168d7
commit 512b60d9f4
10 changed files with 911 additions and 998 deletions
+87
View File
@@ -0,0 +1,87 @@
use crate::credentials::{required, SalesforceAuthFlow, SalesforceCredentials};
use crate::error::SalesforceAuthError;
use crate::salesforce_token_response::SalesforceTokenResponse;
use crate::SalesforceAuthSession;
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`].
///
/// # 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"));
/// ```
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`].
///
/// # 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,
})
}
}
+105
View File
@@ -0,0 +1,105 @@
use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials};
use crate::salesforce_token_response::SalesforceTokenResponse;
use crate::SalesforceAuthError;
impl SalesforceCredentials {
/// Creates a configuration for the OAuth 2.0 client mod flow.
///
/// # Examples
///
/// ```rust
/// use rustsf_auth::{SalesforceCredentials, SalesforceAuthFlow};
///
/// let config = SalesforceCredentials::client_credentials(
/// "https://login.salesforce.com",
/// "client-id",
/// "client-secret",
/// );
///
/// assert_eq!(config.flow, SalesforceAuthFlow::ClientCredentials);
/// assert_eq!(config.client_id.as_deref(), Some("client-id"));
/// ```
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 mod flow.
///
/// This method sends a client mod token request to:
///
/// `{login_url}/services/oauth2/token`
///
/// # Errors
///
/// Returns [`SalesforceAuthError`] if the OAuth client cannot be built, the HTTP
/// request fails, or Salesforce rejects the token request.
///
/// # Examples
///
/// ```rust,no_run
///
/// use rustsf_auth::authenticate_client_credentials;
/// # async fn example() -> Result<(), rustsf_auth::SalesforceAuthError> {
/// let session = authenticate_client_credentials(
/// "https://login.salesforce.com",
/// "client-id",
/// "client-secret",
/// ).await?;
///
/// println!("{}", session.access_token);
/// # Ok(())
/// # }
/// ```
///
/// Configure Salesforce
///
/// 1. Create the External Client App
/// - Navigate to Setup > External Client App Manager.
/// - Click New External Client App and enter the app name and contact email.
/// - Expand the API (Enable OAuth Settings) section:
/// - Check Enable OAuth.
/// - Check Enable Client Credentials Flow.
/// - Add the Manage user data via APIs (api) scope. Do not add refresh_token or offline_access as these are invalid for this flow.
/// - Click Create and note the Consumer Key (Client ID) and Consumer Secret.
///
/// 2. Configure Policies and Run As User
/// - In the External Client App Manager, find your new app and click Edit.
/// - Go to the Policies tab.
/// - Under OAuth Flows and External Client App Enhancements:
/// - Ensure Enable Client Credentials Flow is checked.
/// - In the Run As field, select the integration user (a dedicated service account with necessary API permissions).
/// - Save the changes.
///
pub(crate) 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")?;
Ok(http_client()?
.post(self.token_url()?.url().clone())
.form(&[
("grant_type", "client_credentials"),
("client_id", client_id),
("client_secret", client_secret),
])
.send()
.await?
.error_for_status()?
.json::<SalesforceTokenResponse>()
.await?)
}
}
+192
View File
@@ -0,0 +1,192 @@
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::{Deserialize, Serialize};
use sha2::Sha256;
use std::time::Duration;
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))
}
+251
View File
@@ -0,0 +1,251 @@
use std::sync::RwLock;
use std::time::Duration;
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;
mod sfdx_auth_url;
/// 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,
}
/// Configuration for authenticating to Salesforce.
///
/// This struct provides a single, convenient way to pass mod and select
/// the authentication flow. Prefer using the constructor methods such as
/// [`SalesforceCredentials::client_credentials`], [`SalesforceCredentials::jwt_bearer`],
/// [`SalesforceCredentials::sfdx_url`], and [`SalesforceCredentials::access_token`]
/// instead of manually constructing the struct.
///
/// # Examples
///
/// ```rust
/// use rustsf_auth::{SalesforceCredentials, SalesforceAuthFlow};
///
/// let config = SalesforceCredentials::access_token(
/// "access-token",
/// "https://example.my.salesforce.com",
/// None,
/// None,
/// None,
/// None,
/// );
///
/// assert_eq!(config.flow, SalesforceAuthFlow::AccessToken);
/// ```
#[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;
///
/// # async fn example() -> Result<(), rustsf_auth::SalesforceAuthError> {
/// let config = SalesforceCredentials::client_credentials(
/// "https://login.salesforce.com",
/// "client-id",
/// "client-secret",
/// );
///
/// let session = config.connect().await?;
/// println!("{}", session.access_token);
/// # Ok(())
/// # }
/// ```
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 (&self.instance_url, &self.login_url) {
(Some(url), None)
| (None, Some(url))
| (Some(url), Some(_)) => { url },
(None, None) => "http://login.salesforce.com",
}.to_string();
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: self,
instance_url,
org_id,
user_id,
})
}
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))
}
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,
}
}
pub(crate) async fn refresh(&self) -> Result<SalesforceAuthToken, SalesforceAuthError> {
match &self.refresh_token {
Some(refresh_token) => {
let client_id = required(self.client_id.as_deref(), "client_id")?;
let response = Client::new()
.post(self.token_url()?.url().clone())
.form(&[
("grant_type", "refresh_token"),
("client_id", client_id),
("refresh_token", refresh_token),
// ("client_secret", client_secret.unwrap_or_default()),
])
.send()
.await?
.error_for_status()?
.json::<SalesforceTokenResponse>()
.await?;
Ok(SalesforceAuthToken::from_token_response(response))
},
None => {
self.reconnect().await
}
}
}
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")),
}
}
}
pub(crate) fn http_client() -> Result<reqwest::Client, SalesforceAuthError> {
Ok(Client::builder()
.redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(30))
.build()?)
}
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))
}
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)
}
+131
View File
@@ -0,0 +1,131 @@
use regex::Regex;
use crate::credentials::{http_client, required, SalesforceAuthFlow, SalesforceCredentials};
use crate::SalesforceAuthError;
use crate::salesforce_token_response::SalesforceTokenResponse;
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,
})
}
/// 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")?;
Ok(http_client()?
.post(self.token_url()?.url().clone())
.form(&[
("grant_type", "refresh_token"),
("client_id", client_id),
("refresh_token", refresh_token),
// ("client_secret", client_secret.unwrap_or_default()),
])
.send()
.await?
.error_for_status()?
.json::<SalesforceTokenResponse>()
.await?)
}
}
+42
View File
@@ -0,0 +1,42 @@
/// 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,
/// 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),
/// Error creating a Salesforce JWT assertion.
#[error("JWT error: {0}")]
Jwt(String),
}
+21 -994
View File
File diff suppressed because it is too large Load Diff
+11 -4
View File
@@ -1,7 +1,8 @@
use rustsf_auth::{authenticate_sfdx_url, SalesforceAuthConfig};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue}; use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
use rustsf_auth::credentials::SalesforceCredentials;
pub const CONNECT_TIMEOUT: u64 = 15; pub const CONNECT_TIMEOUT: u64 = 15;
pub const REQUEST_TIMEOUT: u64 = 30; pub const REQUEST_TIMEOUT: u64 = 30;
@@ -37,13 +38,16 @@ 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 = SalesforceAuthConfig::client_credentials( 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", "https://computing-platform-9537--qa.sandbox.my.salesforce.com",
customer_id, customer_id,
customer_secret, customer_secret,
); );
*/
println!("Config: {:?}", config); println!("Config: {:?}", config);
let session = config.connect().await.unwrap(); let session = config.connect().await.unwrap();
@@ -72,6 +76,9 @@ async fn main() {
} }
session.refresh_access_token().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 // Get LIMITs Versions
let response = get_http_client().unwrap() let response = get_http_client().unwrap()
+57
View File
@@ -0,0 +1,57 @@
use crate::salesforce_token_response::SalesforceTokenResponse;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, 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
}
}
}
+14
View File
@@ -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>,
}