Connecting an org with an External Client App

Register a Salesforce org in codev with an External Client App and the JWT bearer flow, instead of an SFDX auth URL.

codev registers a Salesforce org with one of two credential shapes:

CredentialWhat it storesHow it expires
SFDX auth URLA refresh token issued to a connected appThe refresh token can be revoked or expire
External Client AppA consumer key plus an RSA private keyNothing expires; each access token is signed on demand

The External Client App option uses the OAuth JWT bearer flow. codev signs a new assertion with the private key every time it needs an access token, so there is no refresh token to rotate and no interactive web login at registration time.

From Spring '26, Salesforce no longer offers classic Connected App creation in new orgs. External Client Apps are the replacement, and they support the JWT bearer flow.

Both credential shapes work everywhere codev uses an org — builds, releases, pools, and environments. The choice affects how the credential is stored and renewed, not what the org can do.

Before you start

You need:

  • An integration user in the Salesforce org. Use a dedicated service account rather than a person's login — the registration is shared across every project on the instance.
  • The owner role in codev, or an application token. Registering an org and reading its credential are both restricted to those.
  • openssl on your machine, to generate the key pair.

Step 1: Generate a certificate and private key

The app authenticates with an X.509 certificate. Salesforce holds the certificate; codev holds the matching private key.

openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout server.key -out server.crt \
  -days 365 -subj "/CN=codev-jwt/O=your-org/C=AU"

This writes two files:

  • server.key — the private key, in PEM form. This goes into codev.
  • server.crt — the certificate. This is uploaded to Salesforce.

Keep server.key out of source control. -days 365 sets the certificate lifetime; when it expires, generate a new pair and follow Rotating a credential.

Step 2: Create the External Client App

In Salesforce Setup, go to Apps → External Client Apps → External Client App Manager and click New External Client App.

Under Basic Information:

FieldValue
External Client App NameA name of your choosing, for example codev JWT
API NameFills in automatically from the name
Contact EmailAn address your team monitors
Distribution StateLocal — the default
Salesforce New External Client App form showing the Basic Information section
Basic Information on a new external client app. Distribution State defaults to Local.

Expand API (Enable OAuth Settings) and tick Enable OAuth. That reveals the rest of the OAuth configuration:

  • Callback URL — required by Salesforce even though the JWT bearer flow never redirects. http://localhost:1717/OauthRedirect is a workable value.
  • OAuth Scopes — move these from Available to Selected:
    • Manage user data via APIs (api)
    • Manage user data via Web browsers (web)
    • Perform requests at any time (refresh_token, offline_access)

Under Flow Enablement, tick Enable JWT Bearer Flow. A Certificate Upload control appears directly beneath it — upload the server.crt from Step 1.

Flow Enablement section with Enable JWT Bearer Flow ticked and the Certificate Upload control beneath it
Ticking Enable JWT Bearer Flow reveals Certificate Upload. The certificate is required before the app can be created.

Leave Issue JSON Web Token (JWT)-based access tokens for named users unticked. It is off by default.

Do not tick Issue JSON Web Token (JWT)-based access tokens for named users.

With it on, Salesforce returns a JWT-format access token instead of a session id. The org authenticates and queries normally, so registration appears to succeed — but the SOAP Metadata API rejects that token shape, and every deployment to the org then fails.

codev checks for this at registration and refuses the org with an explanatory message rather than letting it fail later during a deploy.

Click Create.

Step 3: Pre-authorize the integration user

A new app is created with App Authorization: All users can self-authorize. The JWT bearer flow does not self-authorize, so this has to change before the credential will work.

On the app, open the Policies tab and click Edit. Under OAuth Policies, set Permitted Users to Admin approved users are pre-authorized. Salesforce asks you to confirm, because the change denies access to any user currently using the app. Click Save.

Then assign the integration user's profile or a permission set to the app. Setting Permitted Users alone is not enough — with the policy set but no profile assigned, authentication fails with:

invalid_app_access: user is not admin approved to access this app

Step 4: Copy the consumer key

Open the app's OAuth settings and copy the Consumer Key. It starts with 3MVG9.

Each org issues its own consumer key, even for the same app definition and the same certificate.

A key from production does not authenticate against that production org's sandbox. When you connect a sandbox, create the app there too and use the key that sandbox issued. The certificate and private key can be reused across orgs; the consumer key cannot.

Step 5: Register the org in codev

Go to Settings → Orgs and click Add Org, then choose the External Client App tab.

Register Org dialog with the External Client App tab selected, showing Username and Instance URL fields
The External Client App tab on the Register Org dialog. The values shown are placeholders.

Fill in:

FieldValue
UsernameThe integration user the JWT authenticates as — the user pre-authorized in Step 3
Instance URLThe org's My Domain URL, for example https://your-instance.my.salesforce.com
Consumer KeyThe key copied in Step 4
Private Key (PEM)The full contents of server.key, including the -----BEGIN and -----END lines

Check This is a DevHub org if the org provisions scratch orgs, and Set as default to make it the default Dev Hub.

Click Register Org. codev authenticates with the credential before saving it, so a rejected registration means the credential does not work — not that it was stored and will fail later.

The private key is encrypted at rest and is never returned to member users.

Sandboxes

For a sandbox, use the sandbox's own login host as the instance URL — https://your-instance--sandboxname.sandbox.my.salesforce.com. The JWT audience resolves to https://test.salesforce.com for sandboxes and https://login.salesforce.com for production.

An External Client App created in a production org does not exist in its sandboxes, including sandboxes created after the app. Each sandbox needs its own app, and issues its own consumer key.

Registering a sandbox by name

The By Name tab registers a sandbox through an already-registered production org, without a separate credential for the sandbox. It works the same way when the production org uses an External Client App — codev authenticates to the production org with the JWT credential and asks Salesforce for a sandbox session from there.

The resulting registration is a just-in-time one: it stores no credential of its own and its session is derived from the parent whenever it is needed. Its authType reads refresh_token rather than jwt, because what Salesforce returns for the sandbox is an auth code exchanged for a refresh token — the External Client App applies to the parent, not to the sandbox.

Salesforce applies two rules to that request, and both are about the production user, not the credential:

  • The user must have created the sandbox. Otherwise: User must be sandbox creator to be authorized.
  • The user must exist inside the sandbox copy. Otherwise: Unable to lookup authUserId ... for SandboxOrgId.

If you adopt an External Client App and switch to a new integration user at the same time, By Name stops working for every sandbox that predates that user — it did not create them, and it does not exist inside them.

Those sandboxes still register normally through the External Client App or SFDX Auth URL tabs, which authenticate to the sandbox directly. Sandboxes created by the new user, after that user exists in the source org, work with By Name as usual.

Rotating a credential

To move an already-registered org onto an External Client App, or to replace an expiring certificate, open the org's row menu and choose Edit Org. The Credentials section has the same two options, and opens on whichever shape the org uses now.

Edit Organization dialog showing the Credentials tabs with External Client App selected and Consumer Key and Private Key fields
Rotating an org onto an External Client App. The values shown are placeholders.

Paste the new consumer key and private key, then Save Changes. Points worth knowing:

  • The new credential is validated before it is stored, and must resolve to the same username. A credential that opens a different org is rejected, so a rotation cannot silently repoint the registration.
  • Saving one credential shape clears the other. An org is on one shape or the other, never both.
  • The stored private key is never sent back to the browser, so re-paste it on every rotation.
  • Leave both fields blank to change only the flags or metadata.

Scratch org creation from a JWT Dev Hub

When a Dev Hub is registered this way, codev pins the scratch org signup app while it creates orgs. Without that pin, Salesforce tries to replicate the calling app into the new scratch org and the creation fails with C-1016 / RemoteOrgSignupFailed.

This is automatic — there is nothing to configure — but it explains the pinning scratch org signup app line in pool provisioning logs.

Troubleshooting

MessageCause
client identifier invalidThe consumer key is not from this org. Copy the key that this org issued (see Step 4)
invalid client credentialsThe private key does not match the certificate uploaded on the app
user is not admin approved to access this appPermitted Users is set to pre-authorized but the user's profile or permission set is not assigned to the app (Step 3)
This External Client App issues JWT-based access tokensIssue JSON Web Token (JWT)-based access tokens for named users is ticked. Untick it and register again
no login URL is storedThe registration has no instance URL to resolve the JWT audience against. Re-register with the instance URL filled in

To confirm a stored credential still works, use Test Connection on the org's row menu.

On this page