Step 1 — Salesforce App Setup
Create the Salesforce External Client App, configure the callback URL and OAuth scopes, and point the platform at your org's My Domain so users can sign in with Salesforce.
On this page
TL;DR: Create one Salesforce app, copy its consumer key into
services/web/config/salesforce.yaml, put the consumer secret in
SALESFORCE_CLIENT_SECRET, and set the callback URL to exactly
https://<your-host>/admin/auth/salesforce/callback. This step delivers
"Sign in with Salesforce" on the login page.
Before You Start
You need Salesforce Setup access sufficient to create an app and read My Domain, and you need to know the public HTTPS hostname of your deployed platform instance. The callback URL must match character-for-character on both sides, so settle the hostname first.
A Note on App Types
Salesforce is mid-migration from Connected Apps to External Client Apps. Both work here — the OAuth flow is identical and the platform only ever sees a consumer key and secret. New orgs should prefer External Client Apps, since Salesforce has stated Connected Apps are the legacy path. The repository's comments use both terms interchangeably; wherever you read "Connected App", "External Client App" is equally valid.
1. Find Your My Domain
In Salesforce Setup, search for My Domain and copy the Current My Domain
URL. It looks like https://yourcompany.my.salesforce.com.
This one value does triple duty in the platform: it is the OAuth host, the
federated-identity issuer key recorded against every linked account, and the
aud of the JWT-bearer assertion in step 2. Changing it later re-keys every
existing federated identity, so use the production org's My Domain from the
start rather than migrating from a sandbox value.
2. Create the App
In Setup, create a new External Client App (or Connected App) and enable OAuth.
Callback URL — set it to exactly:
https://<your-host>/admin/auth/salesforce/callback
OAuth scopes — select the four that map to what the platform requests:
| Scope | Why it is needed |
|---|---|
openid |
Issues the ID token that identifies the user |
email |
Supplies the email and email_verified claims the gate depends on |
profile |
Supplies name and, critically, preferred_username — the Salesforce Username step 2 signs assertions with |
api |
Covers direct REST calls against the org |
Do not add refresh_token. The platform deliberately does not request it:
Salesforce tokens for tool calls are minted fresh on demand via the JWT-bearer
grant rather than banked at login, so there is no long-lived token to refresh
and nothing to leak.
Save the app, then copy the Consumer Key and Consumer Secret. Salesforce may take several minutes to propagate a new app before the first login succeeds.
3. Configure the Platform
Edit services/web/config/salesforce.yaml:
enabled: true
# Setup → My Domain → Current My Domain URL
my_domain: "https://yourcompany.my.salesforce.com"
# The app's Consumer Key. Not a secret — safe to commit.
client_id: "3MVG9..."
# Must match the app's callback URL exactly.
redirect_uri: "https://your-host/admin/auth/salesforce/callback"
scopes: "openid email profile api"
# Only these verified email domains may sign in.
allowed_email_domains:
- yourcompany.com
auto_provision: true
Every key is validated strictly — an unknown key fails the load loudly rather than being silently ignored.
Two fields deserve thought:
allowed_email_domains is a hard gate applied after Salesforce
authenticates the user and before any account is created. A user with a
verified email outside these domains is rejected with ?sso=forbidden and no
local account is minted. The shipped file includes systemprompt.io for
platform testing — remove it.
auto_provision decides your provisioning posture. Left true, the first
verified allow-listed login creates the account with zero admin steps — the
frictionless path. Set to false, SSO will only log in or link users an admin
has already created, and an unknown identity is rejected with
?sso=not_provisioned. Step 4 covers the trade-off.
4. Provision the Secret
The consumer secret is never stored in salesforce.yaml. The platform resolves
it from the SALESFORCE_CLIENT_SECRET environment variable first, then falls
back to the salesforce_client_secret key in the active profile's gitignored
secrets.json.
export SALESFORCE_CLIENT_SECRET='<your consumer secret>'
Use your normal secret-management path for the deployed instance. If the secret
is missing, login fails with ?sso=unavailable and an error is logged — the
routes still mount, so the failure is clean rather than a crash.
What the Login Flow Does
Understanding the sequence makes the failure reasons below self-explanatory.
- The user clicks Sign in with Salesforce, hitting
/admin/auth/salesforce/start. Any?redirect=target is carried through. - The platform generates a PKCE verifier and S256 challenge, and stores the
state, verifier, and redirect target in a short-lived
sf_oauth_statecookie —HttpOnly,SameSite=Lax, scoped to the callback path, 10-minute lifetime. - The browser is redirected to
{my_domain}/services/oauth2/authorize. - Salesforce authenticates the user, applying your org's MFA and login policies.
- Salesforce redirects to
/admin/auth/salesforce/callback. The platform validates the state, exchanges the code for tokens, and fetches{my_domain}/services/oauth2/userinfo. - The claims are gated, the identity is resolved to a local user, and the Salesforce Username is recorded.
- A platform session JWT is minted and set as an
access_tokencookie.
The Salesforce access token from this exchange is used only to fetch userinfo. It is never retained.
Verify
Restart the server, then:
# The login page should render the Salesforce button
curl -s https://<your-host>/admin/login | grep -o 'salesforce-login'
# Watch the flow live while you sign in from a browser
systemprompt infra logs view --follow --since 30s
Sign in as a test user whose email is on an allow-listed domain. Success lands
you on /admin with an access_token cookie set. A failure returns you to
/admin/login?sso=<reason> with a message in the error banner.
Troubleshooting
Every failure collapses to one ?sso= reason on the login URL:
| Reason | Meaning | Fix |
|---|---|---|
unavailable |
SALESFORCE_CLIENT_SECRET is unset, or the config is incomplete or disabled |
Provision the secret; confirm enabled: true and that my_domain, client_id, and redirect_uri are all non-empty |
denied |
The user cancelled at the Salesforce consent screen | None — user action |
forbidden |
The email's domain is not in allowed_email_domains |
Add the domain, or have the user sign in with their work account |
unverified |
Salesforce reported email_verified: false |
The user verifies their email in Salesforce. This gate is deliberate: linking an unverified address would let a hostile identity provider claim an existing account through the email-merge path |
no_email |
Salesforce returned no email claim | Confirm the email scope is on the app and the user record has an email |
not_provisioned |
auto_provision: false and no existing account matches |
An admin creates the account first — see step 4 |
seat_limit |
The user's organization has used every seat | Free a seat or raise the plan limit — see step 4 |
error |
Token exchange or userinfo fetch failed | Check the server logs. Usually a redirect_uri mismatch or a stale consumer secret |
A redirect_uri mismatch is by far the most common first-run failure, and
Salesforce's own error text for it is unhelpfully generic. Compare the two
values byte for byte, including the scheme and any trailing slash.