Authentication¶
lht authenticates to Salesforce with OAuth 2.0 and to Snowflake with key-pair (JWT) authentication. It never asks for, or stores, a Salesforce or Snowflake password.
Salesforce¶
lht supports two server-to-server OAuth flows. Neither needs a browser or a user login.
| Flow | Best for | What lht needs |
|---|---|---|
| Client Credentials (default) | Most setups; used by lht create-connection --salesforce |
Consumer key, consumer secret, My Domain |
| JWT Bearer | Unattended jobs where you'd rather manage a certificate than a secret | Consumer key, username, private key |
Client Credentials flow¶
- In Setup, create an External Client App (or a Connected App) and enable OAuth.
- Enable Client Credentials Flow. Add the scopes lht needs:
apiis enough for sync and reverse ETL. - Set a Run As user. lht acts with that user's permissions, so give it read access to the objects you sync, and write access to the objects you push to.
- Copy the Consumer Key and Consumer Secret.
- Find your My Domain under Setup → My Domain. It's everything before
.my.salesforce.com, for exampleacmeoracme--dev.sandbox.
Salesforce reference: OAuth 2.0 Client Credentials Flow.
Then run:
lht create-connection --salesforce
lht sends the key and secret in a form-encoded POST body to https://<my-domain>.my.salesforce.com/services/oauth2/token, never in the URL.
JWT Bearer flow¶
- Generate a key pair and certificate:
openssl req -x509 -sha256 -nodes -days 730 -newkey rsa:2048 \ -keyout sf_private.key -out sf_cert.crt -subj "/CN=lht" - In your app, enable Use digital signatures and upload
sf_cert.crt. - Pre-authorize the integration user: set the app to "Admin approved users are pre-authorized" and add the user's profile or permission set.
- Pass the credentials to lht:
from lht.user.salesforce_auth import get_salesforce_access_info_from_credentials access_info = get_salesforce_access_info_from_credentials({ "auth_flow": "jwt_bearer", "client_id": "<consumer key>", "username": "integration@example.com", "private_key_pem": open("sf_private.key", "rb").read(), "sandbox": False, # True uses https://test.salesforce.com # "login_url": "https://acme.my.salesforce.com", # optional override })
Snowflake¶
lht connects through Snowpark using key-pair authentication.
- Generate a key pair. Encrypting the private key is optional but recommended:
openssl genrsa 2048 | openssl pkcs8 -topk8 -v2 des3 -inform PEM -out rsa_key.p8 openssl rsa -in rsa_key.p8 -pubout -out rsa_key.pub - Create a role and user, and attach the public key:
CREATE ROLE IF NOT EXISTS LHT_ROLE; GRANT USAGE ON WAREHOUSE COMPUTE_WH TO ROLE LHT_ROLE; GRANT USAGE ON DATABASE SALESFORCE TO ROLE LHT_ROLE; GRANT USAGE, CREATE TABLE ON SCHEMA SALESFORCE.RAW TO ROLE LHT_ROLE; -- optional: allow lht to create schemas it's told to sync into GRANT CREATE SCHEMA ON DATABASE SALESFORCE TO ROLE LHT_ROLE; CREATE USER IF NOT EXISTS LHT_USER DEFAULT_ROLE = LHT_ROLE TYPE = SERVICE; GRANT ROLE LHT_ROLE TO USER LHT_USER; ALTER USER LHT_USER SET RSA_PUBLIC_KEY = '<contents of rsa_key.pub without the header/footer lines>'; - Save the connection:
lht copies the private key into
lht create-connection --snowflake~/.solomo/with0600permissions. It stores the passphrase, if you entered one, inconnections.toml, which is also0600.
For reverse ETL with --log-results, the role also needs CREATE SCHEMA (for LOGS) or ownership of an existing LOGS schema.
Where credentials live¶
| Item | Location | Permissions |
|---|---|---|
| Connection settings and secrets | ~/.solomo/connections.toml |
0600 |
| Snowflake private keys | ~/.solomo/<key file> |
0600 |
| Directory | ~/.solomo/ |
0700 |
To avoid a local file entirely (CI, containers, orchestrators), pass credentials straight to create_session() and get_salesforce_access_info_from_credentials() from your secret manager. See Python API.