# hyperdb-api-salesforce
Salesforce Data Cloud OAuth authentication for Hyper database.
This crate implements the two-stage OAuth token flow for connecting to Salesforce
Data Cloud via gRPC. It supports the JWT Bearer Token Flow, username-password
flow, and refresh token flow, with automatic token caching and refresh.
---
## Overview
`hyperdb-api-salesforce` is a companion crate that provides Salesforce-specific
authentication, separate from the core `hyperdb-api` and `hyperdb-api-core::client` crates.
- **Three OAuth flows** -- JWT Bearer Token (recommended), username-password, refresh token
- **Automatic token refresh** -- Proactive refresh before expiration, reactive on auth errors
- **Thread-safe** -- `SharedTokenProvider` for concurrent access via `Arc<Mutex<>>`
- **Secure memory** -- Private keys and secrets stored with `zeroize`
- **gRPC integration** -- Works with `AuthenticatedGrpcClient` from `hyperdb-api-core::client`
---
## Quick Start (JWT Bearer Token)
```rust
use hyperdb_api_salesforce::{SalesforceAuthConfig, AuthMode, DataCloudTokenProvider};
let private_key_pem = std::fs::read_to_string("private.key")?;
let config = SalesforceAuthConfig::new(
"https://login.salesforce.com",
"your-connected-app-consumer-key",
)?
.auth_mode(AuthMode::private_key("user@example.com", &private_key_pem)?);
let mut provider = DataCloudTokenProvider::new(config)?;
let token = provider.get_token().await?;
println!("Bearer token: {}", token.bearer_token());
println!("Tenant URL: {}", token.tenant_url_str());
```
---
## Authentication Flows
### JWT Bearer Token (Recommended)
Best for server-to-server authentication. Does not require a client secret.
```rust
let config = SalesforceAuthConfig::new(login_url, client_id)?
.auth_mode(AuthMode::private_key("user@example.com", &private_key_pem)?);
```
| `login_url` | Salesforce login URL (e.g., `https://login.salesforce.com`) |
| `client_id` | External Client App Consumer Key |
| `username` | Salesforce username that authorized the app |
| `private_key_pem` | RSA private key in PKCS#8 PEM format |
### Username-Password
Requires `client_secret`. The password may need a security token suffix.
```rust
let config = SalesforceAuthConfig::new(login_url, client_id)?
.client_secret("your-consumer-secret")
.auth_mode(AuthMode::password("user@example.com", "password+securitytoken"));
```
### Refresh Token
Uses a long-lived OAuth refresh token. Requires `client_secret`.
```rust
let config = SalesforceAuthConfig::new(login_url, client_id)?
.client_secret("your-consumer-secret")
.auth_mode(AuthMode::refresh_token("your-refresh-token"));
```
---
## Token Management
`DataCloudTokenProvider` caches both the OAuth Access Token and the DC JWT
independently. The OAuth Access Token (~2-hour lifetime) is only refreshed when
genuinely expired, avoiding unnecessary refresh token rotation. The DC JWT is
refreshed proactively based on both expiry and age. `get_token()` returns a
cached token when valid, or transparently performs the full flow when needed.
### SharedTokenProvider
For concurrent applications, wrap the provider in `SharedTokenProvider`:
```rust
use hyperdb_api_salesforce::SharedTokenProvider;
let provider = SharedTokenProvider::new(config)?;
// Clone and share across tasks
let provider_clone = provider.clone();
tokio::spawn(async move {
let token = provider_clone.get_token().await?;
// use token.bearer_token() as the Authorization header
});
```
`SharedTokenProvider` uses `Arc<Mutex<DataCloudTokenProvider>>` internally.
All token operations are serialized to prevent concurrent refresh races.
---
## Usage with AuthenticatedGrpcClient
Enable the `salesforce-auth` feature on `hyperdb-api-core::client`:
```toml
[dependencies]
hyperdb-api-core = { version = "0.1", features = ["salesforce-auth"] }
hyperdb-api-salesforce = "0.1"
```
`AuthenticatedGrpcClient` handles token refresh automatically, including
reconnection with new tokens:
```rust
use hyperdb_api_salesforce::{SalesforceAuthConfig, AuthMode, SharedTokenProvider};
use hyperdb_api_core::client::grpc::AuthenticatedGrpcClient;
let private_key_pem = std::fs::read_to_string("private.key")?;
let auth_config = SalesforceAuthConfig::new(login_url, client_id)?
.auth_mode(AuthMode::private_key("user@example.com", &private_key_pem)?);
let token_provider = SharedTokenProvider::new(auth_config)?;
let mut client = AuthenticatedGrpcClient::connect(token_provider, None).await?;
// Token refresh is handled automatically, even for long-running queries
let result = client.execute_query("SELECT * FROM my_table").await?;
// Built-in catalog operations
let tables = client.list_tables().await?;
```
**Refresh strategy:**
1. **Proactive** -- Refreshes the DC JWT before expiration (5-minute buffer) and
when it exceeds a maximum age (15 minutes by default)
2. **Reactive** -- On gRPC auth errors (UNAUTHENTICATED), refreshes and retries once
---
## Setup Guide
### Step 1: Generate RSA Key Pair
```bash
# Generate a 2048-bit RSA private key
openssl genrsa -out keypair.key 2048
# Create a self-signed certificate (valid 1 year)
openssl req -new -x509 -nodes -sha256 -days 365 -key keypair.key -out certificate.crt
# Convert to PKCS#8 format (required by this crate)
openssl pkcs8 -topk8 -nocrypt -in keypair.key -out private.key
```
- `certificate.crt` -- Upload to Salesforce External Client App
- `private.key` -- Use in your application (keep secure)
### Step 2: Create External Client App in Salesforce
1. **Setup > External Client App Manager > New External Client App**
2. Flow Type: **App acts on behalf of a user**
3. Add OAuth scopes:
- Access the identity URL service (`id`, `profile`, `email`, `address`, `phone`)
- Manage user data via APIs (`api`)
- Perform requests at any time (`refresh_token`, `offline_access`)
- **Perform ANSI SQL queries on Data Cloud data (`cdp_query_api`)** -- required
4. Callback URL: `https://localhost:1717/OauthRedirect`
5. Enable JWT Bearer Flow and upload `certificate.crt`
### Step 3: Pre-authorize Users
1. External Client App Manager > Your app > **Policies** tab
2. Set Permitted Users to **Admin approved users are pre-authorized**
3. **Manage Profiles** > Add profiles that should have access
### Step 4: Get Consumer Key
1. External Client App Manager > Your app > **OAuth Settings**
2. Click **Manage Consumer Details** (verify identity)
3. Copy the **Consumer Key** (this is the `client_id`)
---
## Environment Variables
The included example reads configuration from environment variables:
```bash
export SF_LOGIN_URL="https://login.salesforce.com"
export SF_CLIENT_ID="3MVG9..."
export SF_USERNAME="your-email@company.com"
export SF_PRIVATE_KEY_PATH="/path/to/private.key"
export SF_DATASPACE="default" # optional
```
```bash
cargo run -p hyperdb-api-salesforce --example salesforce_auth_example
```
---
## Configuration Options
| `.client_secret(s)` | `None` | Required for Password and RefreshToken modes |
| `.dataspace(s)` | `None` | Data Cloud dataspace name |
| `.timeout_secs(n)` | `30` | HTTP request timeout |
| `.max_retries(n)` | `3` | Max retries for transient (5xx) failures |
**Login URLs:** `https://login.salesforce.com` (production),
`https://test.salesforce.com` (sandbox), or
`https://mydomain.my.salesforce.com` (custom domain).
---
## Troubleshooting
| `invalid_grant: user hasn't approved this consumer` | Pre-authorize the user (Step 3 above) |
| `invalid_client_id` | Verify Consumer Key matches exactly |
| `Private key error: failed to parse private key` | Convert to PKCS#8: `openssl pkcs8 -topk8 -nocrypt -in keypair.key -out private.key` |
| `invalid_grant: authentication failure` | Check username, login URL, and certificate match |
| `DC JWT exchange failed` | Verify Data Cloud license and `cdp_query_api` scope |
| `client_secret is required for Password and RefreshToken auth modes` | Add `.client_secret(...)` to your config |
---
## License
Apache-2.0 — see [LICENSE-APACHE](../LICENSE-APACHE).