
Configuring Connected Apps
- 2.3k installs
- 763 repo stars
- Updated July 24, 2026
- forcedotcom/sf-skills
configuring-connected-apps sets up Salesforce Connected Apps and ECAs with OAuth flows, templates, and security hardening.
About
The configuring-connected-apps skill guides OAuth app setup in Salesforce via Connected App or External Client App metadata including auth code, PKCE, JWT bearer, device, and client credentials flows. First decision table prefers ECA for new regulated or packageable solutions and Connected Apps for simple single-org legacy compatibility, noting Spring 26 disables new Connected App creation by default. Workflow chooses app model, OAuth flow by client type, loads XML templates from assets rather than scratch builds, applies a 120-point security checklist, and validates deployment readiness. Rules forbid committing consumer secrets, default Full scope, wildcard callbacks, or creating ECA OAuth security settings without org retrieve first. Metadata paths span connectedApps, externalClientApps, and multiple extlClntApp directories with abbreviated suffixes like ecaGlblOauth. Error handling covers DUPLICATE_VALUE, INVALID_CROSS_REFERENCE_KEY, and INSUFFICIENT_ACCESS_OR_READONLY with stop-on-failure guidance. Delegates Named Credential callouts, metadata deploy-only tasks, and Apex token code to sibling skills.
- Connected App vs External Client App decision with Spring 26 defaults.
- OAuth flow selection for web, SPA, server-to-server, device, and service accounts.
- Template-driven metadata from assets; retrieve ECA security settings from org first.
- 120-point security checklist: PKCE, least privilege scopes, no secrets in source.
- Gotchas for ecaGlblOauth suffixes and consumer key post-deploy retrieve.
Configuring Connected Apps by the numbers
- 2,310 all-time installs (skills.sh)
- +8 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #205 of 2,209 Security skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
configuring-connected-apps capabilities & compatibility
- Capabilities
- connected app vs eca architecture decision guida · oauth flow selection by client type · template based metadata generation paths · security checklist and deployment validation · metadata suffix and retrieve first gotchas
- Use cases
- security audit · api development
What configuring-connected-apps says it does
Never commit consumer secrets to source control
Spring '26: new Connected Apps disabled by default
ECA OAuth security settings must be retrieved from org before editing
npx skills add https://github.com/forcedotcom/sf-skills --skill configuring-connected-appsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.3k |
|---|---|
| repo stars | ★ 763 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 24, 2026 |
| Repository | forcedotcom/sf-skills ↗ |
How do I configure Salesforce OAuth apps with the right flow, metadata files, and security controls?
Configure Salesforce Connected Apps and External Client Apps OAuth flows, JWT bearer, scopes, and metadata with security hardening.
Who is it for?
Salesforce integrators authoring OAuth for web, mobile, or server-to-server clients.
Skip if: Skip for Named Credential callout setup or Apex-only token handling without app metadata.
When should I use this skill?
User touches connectedApp-meta.xml, eca-meta.xml, OAuth flows, JWT bearer, or PKCE for Salesforce.
What you get
Deployment-ready connectedApp or ECA metadata with validated scopes, callbacks, and no embedded secrets.
- ConnectedApp metadata XML
- OAuth configuration block
Files
configuring-connected-apps: Salesforce Connected Apps & External Client Apps
Use this skill when the user needs OAuth app configuration in Salesforce: Connected Apps, External Client Apps (ECAs), JWT bearer setup, PKCE decisions, scope design, or migration from older Connected App patterns to newer ECA patterns.
Scope
In scope:
.connectedApp-meta.xmlor.eca-meta.xmlfiles- OAuth flow selection and callback / scope setup
- JWT bearer auth, device flow, client credentials, or auth-code decisions
- Connected App vs External Client App architecture choices
- Consumer key / secret / certificate handling strategy
Out of scope — delegate elsewhere:
- Configuring Named Credentials or runtime callouts → building-sf-integrations
- Deploying metadata to orgs → deploying-metadata
- Writing Apex token-handling code → generating-apex
---
First Decision: Connected App or External Client App
| If the need is... | Prefer |
|---|---|
| simple single-org OAuth app | Connected App |
| new development with better secret handling | External Client App |
| multi-org / packaging / stronger operational controls | External Client App |
| straightforward legacy compatibility | Connected App |
Default guidance:
- Choose ECA for new regulated, packageable, or automation-heavy solutions.
- Choose Connected App when simplicity and legacy compatibility matter more.
- Spring '26 note: creation of new Connected Apps is disabled by default in orgs. For new integrations, prefer External Client Apps unless Connected App compatibility is explicitly required.
---
Required Inputs
Ask for or infer:
- App type: Connected App or ECA
- OAuth flow: auth code, PKCE, JWT bearer, device, client credentials
- Client type: confidential vs public
- Callback URLs / redirect surfaces
- Required scopes
- Distribution model: local org only vs packageable / multi-org
- Whether certificates or secret rotation are required
---
Workflow
1. Choose the app model
Decide whether a Connected App or ECA is the better long-term fit using the decision table above.
2. Choose the OAuth flow
| Use case | Default flow |
|---|---|
| backend web app | Authorization Code |
| SPA / mobile / public client | Authorization Code + PKCE |
| server-to-server / CI/CD | JWT Bearer |
| device / CLI auth | Device Flow |
| service account style app | Client Credentials (typically ECA) |
3. Start from the right template
Read the appropriate template before generating — do not build from scratch:
| Template | Use case |
|---|---|
assets/connected-app-basic.xml | Simple API integration, minimal OAuth |
assets/connected-app-oauth.xml | Web app with full OAuth 2.0 configuration |
assets/connected-app-jwt.xml | JWT bearer / server-to-server |
assets/connected-app-canvas.xml | Embedding external apps in Salesforce UI (Canvas) |
assets/external-client-app.xml | ECA header file — all new ECA builds start here |
assets/eca-global-oauth.xml | ECA global OAuth settings (scopes, PKCE, rotation) |
assets/eca-oauth-settings.xml | ECA per-app OAuth settings |
assets/eca-policies.xml | ECA configurable policies |
If you need source-controlled ECA OAuth security metadata, retrieve it from an org first and treat the retrieved file as the schema source of truth:
sf project retrieve start --metadata ExtlClntAppOauthSecuritySettings:<AppName> --target-org <alias>4. Apply security hardening
Read references/security-checklist.md for the full 120-point security checklist. Favor:
- Least-privilege scopes
- Explicit callback URLs
- PKCE for public clients
- Certificate-based auth where appropriate
- Rotation-ready secret / key handling
- IP restrictions when realistic and maintainable
5. Validate deployment readiness
Read references/testing-validation-guide.md before handoff. Confirm:
- Metadata file naming is correct (see Gotchas below)
- Scopes are justified
- Callback and auth model match the real client type
- Secrets are not embedded in source
6. Handle errors
If deployment fails, check the error output for:
DUPLICATE_VALUE— a Connected App or ECA with this name already exists; rename or retrieve-then-update insteadINVALID_CROSS_REFERENCE_KEY— theexternalClientApplicationname in an ECA settings file doesn't match the.eca-meta.xmlfilename exactlyINSUFFICIENT_ACCESS_OR_READONLY— user lacks the "Manage Connected Apps" permission- If any step fails, do not proceed to the next step — surface the error to the user with the specific message above
---
Rules / Constraints
| Rule | Rationale |
|---|---|
| Never commit consumer secrets to source control | Credential exposure risk |
Never use Full scope by default | Unnecessary privilege; request only what the app needs |
| Always use PKCE for public clients (mobile, SPA) | Prevents auth code interception |
| Never use wildcard or overly broad callback URLs | Token interception risk |
| ECA OAuth security settings must be retrieved from org before editing | File schema is not fully documented; retrieve-first ensures accuracy |
Use <alias> placeholders in CLI commands, never hardcoded org URLs | Org URLs vary per environment |
Detect actual packageDirectory from sfdx-project.json before writing files | Projects may not use the default force-app/main/default/ layout |
---
Metadata Notes That Matter
Connected App
Default source location (verify via sfdx-project.json → packageDirectories):
<packageDir>/connectedApps/
External Client App
ECA metadata spans multiple top-level source directories. Default locations (verify via sfdx-project.json):
| Directory | Metadata type | File suffix |
|---|---|---|
<packageDir>/externalClientApps/ | ExternalClientApplication | .eca-meta.xml |
<packageDir>/extlClntAppGlobalOauthSets/ | ExtlClntAppGlobalOauthSettings | .ecaGlblOauth-meta.xml |
<packageDir>/extlClntAppOauthSettings/ | ExtlClntAppOauthSettings | .ecaOauth-meta.xml |
<packageDir>/extlClntAppOauthSecuritySettings/ | ExtlClntAppOauthSecuritySettings | .ecaOauthSecurity-meta.xml |
<packageDir>/extlClntAppOauthPolicies/ | ExtlClntAppOauthConfigurablePolicies | .ecaOauthPlcy-meta.xml |
<packageDir>/extlClntAppPolicies/ | ExtlClntAppConfigurablePolicies | .ecaPlcy-meta.xml |
---
Gotchas
| Gotcha | Detail |
|---|---|
.ecaGlblOauth not .ecaGlobalOauth | The global OAuth suffix is abbreviated — using the long form will break deployment |
.ecaPlcy not .ecaPolicy | Same abbreviation pattern — the general policy suffix is short form |
.ecaOauthSecurity for security settings | Use .ecaOauthSecurity, not .ecaSecurity |
| ECA OAuth security settings are retrieve-only | Cannot be created from scratch in source — always retrieve from org first |
| Spring '26: new Connected Apps disabled by default | New orgs block Connected App creation; use ECA unless explicitly required |
| Consumer key is generated post-deploy | You cannot set the consumer key in metadata — retrieve it after first deployment |
---
Output Expectations
When finishing, confirm and report in this order:
1. App type chosen — Connected App or External Client App 2. OAuth flow chosen 3. Files created or updated — list each metadata file path 4. Security decisions — scopes, PKCE, certs, secrets, IP policy 5. Next deployment / testing step
Suggested output shape:
App: <name>
Type: Connected App | External Client App
Flow: <oauth flow>
Files: <paths>
Security: <scopes, PKCE, certs, secrets, IP policy>
Next step: <deploy, retrieve consumer key, or test auth flow>
Score: <x>/120---
Cross-Skill Integration
| Need | Delegate to | Reason |
|---|---|---|
| Named Credential / callout runtime config | building-sf-integrations | runtime integration setup |
| Deploy app metadata | deploying-metadata | org validation and deployment |
| Apex token or refresh handling | generating-apex | implementation logic |
---
Score Guide
| Score | Meaning |
|---|---|
| 80+ | production-ready OAuth app config |
| 54–79 | workable but needs hardening review |
| < 54 | block deployment until fixed |
---
Reference File Index
| File | When to read |
|---|---|
assets/connected-app-basic.xml | Step 3 — template for simple Connected App with minimal OAuth |
assets/connected-app-oauth.xml | Step 3 — template for full OAuth 2.0 Connected App |
assets/connected-app-jwt.xml | Step 3 — template for JWT bearer / server-to-server Connected App |
assets/connected-app-canvas.xml | Step 3 — template for Canvas app embedding in Salesforce UI |
assets/external-client-app.xml | Step 3 — ECA header file template |
assets/eca-global-oauth.xml | Step 3 — ECA global OAuth settings template (PKCE, rotation, callbacks) |
assets/eca-oauth-settings.xml | Step 3 — ECA per-app OAuth settings template |
assets/eca-policies.xml | Step 3 — ECA configurable policies template |
references/oauth-flows-reference.md | Step 2 — detailed OAuth flow comparison and decision guide |
references/security-checklist.md | Step 4 — full 120-point security scoring checklist |
references/testing-validation-guide.md | Step 5 — pre-deployment validation and testing guide |
references/migration-guide.md | When migrating from Connected App to ECA patterns |
references/example-usage.md | Full end-to-end examples for common OAuth scenarios |
<?xml version="1.0" encoding="UTF-8"?>
<!--
Template: Connected App - Basic
Use Case: Simple API integration with minimal OAuth configuration
Replace placeholders:
- {{APP_NAME}}: Application name (no spaces, alphanumeric + underscore)
- {{CONTACT_EMAIL}}: Administrator contact email
- {{DESCRIPTION}}: Brief description of the app's purpose
- {{CALLBACK_URL}}: OAuth callback URL (must be HTTPS in production)
-->
<ConnectedApp xmlns="http://soap.sforce.com/2006/04/metadata">
<label>{{APP_NAME}}</label>
<contactEmail>{{CONTACT_EMAIL}}</contactEmail>
<description>{{DESCRIPTION}}</description>
<oauthConfig>
<callbackUrl>{{CALLBACK_URL}}</callbackUrl>
<isAdminApproved>false</isAdminApproved>
<isConsumerSecretOptional>false</isConsumerSecretOptional>
<scopes>Api</scopes>
<scopes>RefreshToken</scopes>
</oauthConfig>
<oauthPolicy>
<ipRelaxation>ENFORCE</ipRelaxation>
<refreshTokenPolicy>infinite</refreshTokenPolicy>
</oauthPolicy>
</ConnectedApp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Template: Connected App - Canvas App
Use Case: Embedding external applications within Salesforce UI
Replace placeholders:
- {{APP_NAME}}: Application name (no spaces, alphanumeric + underscore)
- {{CONTACT_EMAIL}}: Administrator contact email
- {{DESCRIPTION}}: Brief description of the app's purpose
- {{CANVAS_URL}}: URL of your canvas application
- {{CALLBACK_URL}}: OAuth callback URL (must be HTTPS)
Canvas Access Methods:
- Chatter Feed: Embed in Chatter
- Chatter Tab: Add to Chatter tab
- Publisher: Add to publisher actions
- Visualforce Page: Embed via Visualforce
- Mobile Card: Show on mobile record pages
- OpenCTI: Use in Service Cloud CTI
- LayoutSection: Embed in page layouts
-->
<ConnectedApp xmlns="http://soap.sforce.com/2006/04/metadata">
<label>{{APP_NAME}}</label>
<contactEmail>{{CONTACT_EMAIL}}</contactEmail>
<description>{{DESCRIPTION}}</description>
<oauthConfig>
<callbackUrl>{{CALLBACK_URL}}</callbackUrl>
<isAdminApproved>true</isAdminApproved>
<isConsumerSecretOptional>false</isConsumerSecretOptional>
<scopes>Api</scopes>
<scopes>RefreshToken</scopes>
</oauthConfig>
<oauthPolicy>
<ipRelaxation>ENFORCE</ipRelaxation>
<refreshTokenPolicy>infinite</refreshTokenPolicy>
</oauthPolicy>
<!-- Canvas Configuration -->
<canvasConfig>
<canvasUrl>{{CANVAS_URL}}</canvasUrl>
<accessMethod>Get</accessMethod>
<!-- Canvas Locations - uncomment as needed -->
<locations>Chatter</locations>
<locations>Visualforce</locations>
<!-- <locations>ChatterFeed</locations> -->
<!-- <locations>Publisher</locations> -->
<!-- <locations>MobileNav</locations> -->
<!-- <locations>OpenCTI</locations> -->
<!-- <locations>LayoutSection</locations> -->
<!-- Canvas Options -->
<lifecycleClass></lifecycleClass>
<options>HideHeader</options>
<options>HideShare</options>
<!-- SAML Initiation Method -->
<samlInitiationMethod>None</samlInitiationMethod>
</canvasConfig>
</ConnectedApp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Template: Connected App - JWT Bearer Flow
Use Case: Server-to-server integration, CI/CD pipelines, headless automation
Replace placeholders:
- {{APP_NAME}}: Application name (no spaces, alphanumeric + underscore)
- {{CONTACT_EMAIL}}: Administrator contact email
- {{DESCRIPTION}}: Brief description of the app's purpose
- {{CERTIFICATE_NAME}}: Name of the certificate uploaded to Salesforce
Prerequisites:
1. Create a self-signed certificate or use CA-signed certificate
2. Upload certificate to Salesforce (Setup > Certificate and Key Management)
3. Use the certificate name (not file name) in this template
Note: No callback URL needed for JWT Bearer flow
Note: Consumer Secret is optional when using certificate authentication
-->
<ConnectedApp xmlns="http://soap.sforce.com/2006/04/metadata">
<label>{{APP_NAME}}</label>
<contactEmail>{{CONTACT_EMAIL}}</contactEmail>
<description>{{DESCRIPTION}} - JWT Bearer Authentication</description>
<oauthConfig>
<!-- JWT Bearer flow doesn't require callback URL, but Salesforce requires one -->
<callbackUrl>https://localhost/oauth/callback</callbackUrl>
<!-- Certificate for JWT signing -->
<certificate>{{CERTIFICATE_NAME}}</certificate>
<isAdminApproved>true</isAdminApproved>
<isConsumerSecretOptional>true</isConsumerSecretOptional>
<!-- API access scope - typically all that's needed for server-to-server -->
<scopes>Api</scopes>
<!-- RefreshToken not needed for JWT - each request gets new access token -->
</oauthConfig>
<oauthPolicy>
<ipRelaxation>ENFORCE</ipRelaxation>
<refreshTokenPolicy>zero</refreshTokenPolicy>
</oauthPolicy>
<!-- Require connected app to be assigned via permission sets or profiles -->
<permissionSetLicense>
<license>Salesforce</license>
</permissionSetLicense>
</ConnectedApp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Template: Connected App - Full OAuth
Use Case: Web application with complete OAuth 2.0 configuration
Replace placeholders:
- {{APP_NAME}}: Application name (no spaces, alphanumeric + underscore)
- {{CONTACT_EMAIL}}: Administrator contact email
- {{DESCRIPTION}}: Brief description of the app's purpose
- {{CALLBACK_URL}}: OAuth callback URL (must be HTTPS)
- {{LOGOUT_URL}}: URL to redirect after logout (optional)
OAuth Scopes (uncomment as needed):
- Api: REST/SOAP API access
- RefreshToken: Offline access via refresh token
- Full: Complete access (use sparingly)
- OpenID: OpenID Connect
- Web: Web browser access
- ChatterApi: Chatter REST API
- CustomPermissions: Custom permission access
-->
<ConnectedApp xmlns="http://soap.sforce.com/2006/04/metadata">
<label>{{APP_NAME}}</label>
<contactEmail>{{CONTACT_EMAIL}}</contactEmail>
<description>{{DESCRIPTION}}</description>
<oauthConfig>
<callbackUrl>{{CALLBACK_URL}}</callbackUrl>
<isAdminApproved>true</isAdminApproved>
<isConsumerSecretOptional>false</isConsumerSecretOptional>
<isIntrospectAllTokens>false</isIntrospectAllTokens>
<!-- Core API Access -->
<scopes>Api</scopes>
<scopes>RefreshToken</scopes>
<!-- OpenID Connect (for user identity) -->
<scopes>OpenID</scopes>
<scopes>Profile</scopes>
<scopes>Email</scopes>
<!-- Uncomment additional scopes as needed -->
<!-- <scopes>Web</scopes> -->
<!-- <scopes>ChatterApi</scopes> -->
<!-- <scopes>CustomPermissions</scopes> -->
<!-- <scopes>Wave</scopes> -->
</oauthConfig>
<oauthPolicy>
<ipRelaxation>ENFORCE</ipRelaxation>
<refreshTokenPolicy>infinite</refreshTokenPolicy>
<singleLogoutUrl>{{LOGOUT_URL}}</singleLogoutUrl>
</oauthPolicy>
<!--
NOTE: refreshTokenValidityPeriod requires specific element ordering per XSD.
For custom token lifetimes, configure via Setup UI after deployment.
Using refreshTokenPolicy=infinite is the safest metadata-deployable option.
-->
<!-- Session Policy -->
<sessionPolicy>
<sessionTimeout>120</sessionTimeout>
</sessionPolicy>
</ConnectedApp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Template: External Client App - Global OAuth Settings
Use Case: Configure OAuth settings that apply globally to the ECA
Replace placeholders:
- {{APP_NAME}}: The ExternalClientApplication API name (must match .eca file)
- {{LABEL}}: Display label for this global OAuth settings configuration
- {{CALLBACK_URL}}: OAuth callback URL (must be HTTPS for web, custom scheme for mobile)
- {{PKCE_REQUIRED}}: true for public clients (mobile, SPA), false for confidential
- {{SECRET_OPTIONAL}}: true for public clients using PKCE only
- {{SECRET_FOR_REFRESH}}: true to require secret for refresh token requests
- {{ROTATE_KEY}}: true to enable consumer key rotation (recommended for production)
- {{ROTATE_SECRET}}: true to enable consumer secret rotation (recommended for production)
Security Recommendations:
- PKCE: REQUIRED for mobile and SPA applications (public clients)
- Key/Secret Rotation: Enable for production apps (can be automated via API)
- Consumer Secret Optional: Only true when using PKCE exclusively
- Introspect All Tokens: Enable if you need to validate tokens programmatically
File Naming: [AppName].ecaGlblOauth-meta.xml
NOTE: The file suffix is .ecaGlblOauth (abbreviated), NOT .ecaGlobalOauth
-->
<ExtlClntAppGlobalOauthSettings xmlns="http://soap.sforce.com/2006/04/metadata">
<callbackUrl>{{CALLBACK_URL}}</callbackUrl>
<externalClientApplication>{{APP_NAME}}</externalClientApplication>
<isConsumerSecretOptional>{{SECRET_OPTIONAL}}</isConsumerSecretOptional>
<isIntrospectAllTokens>false</isIntrospectAllTokens>
<isPkceRequired>{{PKCE_REQUIRED}}</isPkceRequired>
<isSecretRequiredForRefreshToken>{{SECRET_FOR_REFRESH}}</isSecretRequiredForRefreshToken>
<label>{{LABEL}}</label>
<shouldRotateConsumerKey>{{ROTATE_KEY}}</shouldRotateConsumerKey>
<shouldRotateConsumerSecret>{{ROTATE_SECRET}}</shouldRotateConsumerSecret>
</ExtlClntAppGlobalOauthSettings>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Template: External Client App - Instance OAuth Settings
Use Case: Configure OAuth scopes for the ECA
Replace placeholders:
- {{APP_NAME}}: The ExternalClientApplication API name (must match .eca file)
- {{LABEL}}: Display label for this OAuth settings configuration
- {{SCOPES}}: Comma-separated OAuth scopes (e.g., "Api, RefreshToken, OpenID")
Common Scope Combinations:
- API Integration: Api, RefreshToken
- Web App with Identity: Api, RefreshToken, OpenID, Profile, Email
- Server-to-Server: Api
- Mobile App: Api, RefreshToken, OpenID
Available Scopes:
- Api: REST/SOAP API access
- RefreshToken: Offline access via refresh token
- OpenID: OpenID Connect (user identity)
- Profile: User profile information
- Email: User email address
- Web: Web browser access
- ChatterApi: Chatter REST API
- CustomPermissions: Custom permission access
IMPORTANT: OAuth flows (Authorization Code, Client Credentials, etc.) are
configured via the Admin UI or ExtlClntAppOauthConfigurablePolicies, NOT here.
File Naming: [AppName].ecaOauth-meta.xml
-->
<ExtlClntAppOauthSettings xmlns="http://soap.sforce.com/2006/04/metadata">
<commaSeparatedOauthScopes>{{SCOPES}}</commaSeparatedOauthScopes>
<externalClientApplication>{{APP_NAME}}</externalClientApplication>
<label>{{LABEL}}</label>
</ExtlClntAppOauthSettings>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Template: External Client App - Configurable Policies
Use Case: Admin-configurable security policies for the ECA
Note: This file is AUTO-GENERATED on first deployment.
Admins can modify these values in Setup UI or via Metadata API.
Replace placeholders:
- {{IP_RELAXATION}}: IP restriction policy
- {{REFRESH_TOKEN_POLICY}}: Refresh token lifetime policy
- {{SESSION_TIMEOUT}}: Session timeout in minutes
IP Relaxation Options:
- ENFORCE: Enforce IP restrictions (recommended)
- BYPASS: Bypass IP restrictions (use with caution)
- ENFORCE_ACTIVATED_USERS: Enforce for activated users only
Refresh Token Policies:
- infinite: Tokens never expire
- zero: No refresh tokens (use with JWT)
- specific_lifetime: Expire after specified period
File Naming: [AppName].ecaPlcy-meta.xml
-->
<ExtlClntAppConfigurablePolicies xmlns="http://soap.sforce.com/2006/04/metadata">
<!-- IP Restriction Policy -->
<ipRelaxation>{{IP_RELAXATION}}</ipRelaxation>
<!-- Refresh Token Lifetime -->
<refreshTokenPolicy>{{REFRESH_TOKEN_POLICY}}</refreshTokenPolicy>
<!-- <refreshTokenValidityPeriod>30</refreshTokenValidityPeriod> -->
<!-- Session Timeout (minutes) -->
<sessionTimeout>{{SESSION_TIMEOUT}}</sessionTimeout>
</ExtlClntAppConfigurablePolicies>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Template: External Client Application - Header File
Use Case: Modern OAuth app with enhanced security and metadata compliance
Replace placeholders:
- {{APP_NAME}}: Application name (no spaces, alphanumeric + underscore)
- {{CONTACT_EMAIL}}: Administrator contact email
- {{DESCRIPTION}}: Brief description of the app's purpose
- {{ICON_URL}}: URL to app icon (optional)
- {{LOGO_URL}}: URL to app logo (optional)
- {{DISTRIBUTION_STATE}}: Local or Packageable
Distribution States:
- Local: Available only in this org
- Packageable: Can be packaged and distributed via 2GP
Common Source Files for ECA:
1. externalClientApps/[AppName].eca-meta.xml (this file)
2. extlClntAppGlobalOauthSets/[AppName].ecaGlblOauth-meta.xml (global OAuth settings)
3. extlClntAppOauthSettings/[AppName].ecaOauth-meta.xml (OAuth scopes / parent link)
4. extlClntAppOauthSecuritySettings/[AppName].ecaOauthSecurity-meta.xml (optional, retrieve-first)
5. extlClntAppOauthPolicies/[AppName].ecaOauthPlcy-meta.xml or extlClntAppPolicies/[AppName].ecaPlcy-meta.xml (optional policy metadata)
Minimum API Version: 61.0
-->
<ExternalClientApplication xmlns="http://soap.sforce.com/2006/04/metadata">
<contactEmail>{{CONTACT_EMAIL}}</contactEmail>
<description>{{DESCRIPTION}}</description>
<distributionState>{{DISTRIBUTION_STATE}}</distributionState>
<iconUrl>{{ICON_URL}}</iconUrl>
<isProtected>false</isProtected>
<label>{{APP_NAME}}</label>
<logoUrl>{{LOGO_URL}}</logoUrl>
</ExternalClientApplication>
Credits & Acknowledgments
This skill is built on established Salesforce OAuth and identity platform patterns, incorporating security best practices and architectural guidance from the Salesforce platform ecosystem.
configuring-connected-apps
Salesforce Connected Apps & External Client Apps skill for Claude Code.
Overview
This skill helps you create and manage:
- Connected Apps: Traditional OAuth-enabled applications for Salesforce integration
- External Client Apps (ECAs): Modern, security-first applications with enhanced controls (API 61.0+)
Features
| Feature | Description |
|---|---|
| App Generation | Create Connected Apps or ECAs from requirements |
| Security Scoring | 120-point validation across 6 categories |
| Template Library | Pre-built templates for common OAuth flows |
| Migration Support | Guidance for Connected App → ECA migration |
| Best Practices | Built-in security recommendations |
Quick Start
Create a Connected App
Use the configuring-connected-apps skill to create a Connected App named "MyIntegration"
with API and RefreshToken scopes for server-to-server integration.Create an External Client App
Use the configuring-connected-apps skill to create an External Client App named
"MobileApp" for a mobile application with PKCE enabled.Review Existing Apps
Use the configuring-connected-apps skill to review and score my existing Connected Apps
for security best practices.Scoring Categories
| Category | Points | Focus |
|---|---|---|
| Security | 30 | PKCE, rotation, certificates |
| OAuth Configuration | 25 | Callbacks, flows, tokens |
| Metadata Compliance | 20 | Required fields, API version |
| Best Practices | 20 | Minimal scopes, admin approval |
| Scopes | 15 | Least privilege principle |
| Documentation | 10 | Description, contact info |
When to Use Each App Type
Choose Connected App When:
- Simple, single-org integration
- Legacy system compatibility needed
- Quick setup is priority
- No cross-org deployment needs
Choose External Client App When:
- Multi-org or ISV distribution
- Automated secret rotation required
- Enhanced audit logging needed
- Full metadata compliance required
- Packaging in 2GP
Directory Structure
configuring-connected-apps/
├── skills/
│ └── configuring-connected-apps/
│ └── SKILL.md # Main skill definition
├── assets/
│ ├── connected-app-basic.xml
│ ├── connected-app-oauth.xml
│ ├── connected-app-jwt.xml
│ ├── connected-app-canvas.xml
│ ├── external-client-app.xml
│ ├── eca-global-oauth.xml
│ ├── eca-oauth-settings.xml
│ └── eca-policies.xml
├── references/
│ └── example-usage.md
├── references/
│ ├── oauth-flows-reference.md
│ ├── security-checklist.md
│ └── migration-guide.md
└── README.mdDependencies
- deploying-metadata: For deploying apps to orgs
- sf-metadata: For creating related metadata (Named Credentials)
<!-- Parent: configuring-connected-apps/SKILL.md -->
configuring-connected-apps Examples
Real-world examples of using the configuring-connected-apps skill.
Example 1: Create a Basic API Integration Connected App
Request:
Use the configuring-connected-apps skill to create a Connected App named "DataSyncIntegration"
for syncing data with our external ERP system. It needs API access and refresh tokens.
Contact email: admin@company.comGenerated Output:
force-app/main/default/connectedApps/DataSyncIntegration.connectedApp-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ConnectedApp xmlns="http://soap.sforce.com/2006/04/metadata">
<label>DataSyncIntegration</label>
<contactEmail>admin@company.com</contactEmail>
<description>Data synchronization with external ERP system</description>
<oauthConfig>
<callbackUrl>https://erp.company.com/oauth/callback</callbackUrl>
<isAdminApproved>true</isAdminApproved>
<isConsumerSecretOptional>false</isConsumerSecretOptional>
<scopes>Api</scopes>
<scopes>RefreshToken</scopes>
</oauthConfig>
<oauthPolicy>
<ipRelaxation>ENFORCE</ipRelaxation>
<refreshTokenPolicy>infinite</refreshTokenPolicy>
</oauthPolicy>
</ConnectedApp>Score: 95/120 ⭐⭐⭐⭐
---
Example 2: Create a JWT Bearer App for CI/CD
Request:
Create a Connected App for our CI/CD pipeline using JWT Bearer flow.
Name it "GitHubActionsDeployment" and use certificate "GH_Actions_Cert".Generated Output:
force-app/main/default/connectedApps/GitHubActionsDeployment.connectedApp-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ConnectedApp xmlns="http://soap.sforce.com/2006/04/metadata">
<label>GitHubActionsDeployment</label>
<contactEmail>devops@company.com</contactEmail>
<description>CI/CD deployment via GitHub Actions - JWT Bearer flow</description>
<oauthConfig>
<callbackUrl>https://localhost/oauth/callback</callbackUrl>
<certificate>GH_Actions_Cert</certificate>
<isAdminApproved>true</isAdminApproved>
<isConsumerSecretOptional>true</isConsumerSecretOptional>
<scopes>Api</scopes>
</oauthConfig>
<oauthPolicy>
<ipRelaxation>ENFORCE</ipRelaxation>
<refreshTokenPolicy>zero</refreshTokenPolicy>
</oauthPolicy>
</ConnectedApp>Score: 108/120 ⭐⭐⭐⭐⭐
---
Example 3: Create a Mobile App with External Client App
Request:
Create an External Client App for our mobile app "FieldServiceMobile".
It needs PKCE enabled, refresh tokens, and should be packageable for distribution.Generated Files:
1. Header File: FieldServiceMobile.eca-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ExternalClientApplication xmlns="http://soap.sforce.com/2006/04/metadata">
<contactEmail>mobile-team@company.com</contactEmail>
<description>Field Service mobile application for technicians</description>
<distributionState>Packageable</distributionState>
<isProtected>false</isProtected>
<label>FieldServiceMobile</label>
</ExternalClientApplication>2. Global OAuth: FieldServiceMobile.ecaGlblOauth-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ExtlClntAppGlobalOauthSettings xmlns="http://soap.sforce.com/2006/04/metadata">
<callbackUrl>fieldservicemobile://oauth/callback</callbackUrl>
<externalClientApplication>FieldServiceMobile</externalClientApplication>
<isConsumerSecretOptional>true</isConsumerSecretOptional>
<isPkceRequired>true</isPkceRequired>
<label>Field Service Mobile Global OAuth</label>
<shouldRotateConsumerKey>true</shouldRotateConsumerKey>
<shouldRotateConsumerSecret>true</shouldRotateConsumerSecret>
</ExtlClntAppGlobalOauthSettings>3. OAuth Settings: FieldServiceMobile.ecaOauth-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ExtlClntAppOauthSettings xmlns="http://soap.sforce.com/2006/04/metadata">
<commaSeparatedOauthScopes>Api, RefreshToken, OpenID</commaSeparatedOauthScopes>
<externalClientApplication>FieldServiceMobile</externalClientApplication>
<label>Field Service Mobile OAuth Settings</label>
</ExtlClntAppOauthSettings>4. Optional OAuth Security Settings (retrieve-first)
sf project retrieve start \
--metadata ExtlClntAppOauthSecuritySettings:FieldServiceMobile \
--target-org my-devhubScore: 115/120 ⭐⭐⭐⭐⭐
---
Example 4: Review Existing Connected Apps
Request:
Review and score my existing Connected Apps for security best practices.Process: 1. Scan: Glob: **/*.connectedApp-meta.xml 2. Read each file 3. Validate against scoring criteria 4. Generate report
Sample Report:
📊 CONNECTED APP SECURITY REVIEW
════════════════════════════════════════════════════════════════
🔍 Apps Scanned: 3
📅 Review Date: 2025-01-15
┌────────────────────────────┬────────┬────────┬──────────────────┐
│ App Name │ Score │ Rating │ Critical Issues │
├────────────────────────────┼────────┼────────┼──────────────────┤
│ LegacyERPConnector │ 45/120 │ ⭐ │ 3 │
│ MobileAppV2 │ 82/120 │ ⭐⭐⭐ │ 0 │
│ DataSyncIntegration │ 95/120 │ ⭐⭐⭐⭐│ 0 │
└────────────────────────────┴────────┴────────┴──────────────────┘
🚨 CRITICAL ISSUES
LegacyERPConnector:
❌ Wildcard callback URL detected (https://*)
❌ Full scope granted (overly permissive)
❌ No IP restrictions configured
⚠️ Recommend: Migrate to External Client App
✅ RECOMMENDATIONS
1. LegacyERPConnector: URGENT - Fix security issues or migrate to ECA
2. MobileAppV2: Enable PKCE for mobile security
3. DataSyncIntegration: Consider enabling token rotation---
Example 5: Migrate Connected App to External Client App
Request:
Migrate our "SalesPortal" Connected App to an External Client App for better security.Process: 1. Read existing Connected App configuration 2. Map OAuth settings to ECA structure 3. Generate all required ECA files 4. Provide migration checklist
Output:
📦 MIGRATION PLAN: SalesPortal → External Client App
════════════════════════════════════════════════════════════════
📋 Source Configuration
Type: Connected App
OAuth Flow: Authorization Code
Scopes: Api, RefreshToken, OpenID
IP Policy: ENFORCE
📄 Files to Generate:
1. SalesPortalECA.eca-meta.xml
2. SalesPortalECA.ecaGlblOauth-meta.xml
3. SalesPortalECA.ecaOauth-meta.xml
4. (Optional) SalesPortalECA.ecaOauthSecurity-meta.xml after retrieve-first validation
🔄 Migration Steps:
1. ✓ Generate ECA metadata files
2. □ Deploy ECA to DevHub
3. □ Generate new Consumer Key/Secret
4. □ Update external application with new credentials
5. □ Test OAuth flow
6. □ Configure policies in subscriber orgs
7. □ Deactivate old Connected App
8. □ Monitor for 30 days before deletion
⚠️ Breaking Change: New Consumer Key/Secret required---
Deployment Commands
Deploy Connected App
sf project deploy start \
--source-dir force-app/main/default/connectedApps \
--target-org my-orgDeploy External Client App
sf project deploy start \
--metadata ExternalClientApplication:MyECAName \
--metadata ExtlClntAppGlobalOauthSettings:MyECAName \
--metadata ExtlClntAppOauthSettings:MyECAName \
--target-org my-devhubRetrieve Existing Apps
# Connected Apps
sf project retrieve start \
--metadata ConnectedApp:MyAppName \
--target-org my-org
# External Client Apps (header + companions)
sf project retrieve start \
--metadata ExternalClientApplication:MyECAName \
--metadata ExtlClntAppGlobalOauthSettings:MyECAName \
--metadata ExtlClntAppOauthSettings:MyECAName \
--target-org my-org<!-- Parent: configuring-connected-apps/SKILL.md -->
Migration Guide: Connected App → External Client App
Step-by-step guide for migrating from Connected Apps to External Client Apps (ECAs).
Why Migrate?
| Feature | Connected App | External Client App |
|---|---|---|
| Metadata Compliance | Partial | Full |
| Secret in Sandboxes | Visible | Hidden |
| Key Rotation | Manual | Automated via API |
| Multi-Org Distribution | Manual recreation | Native 2GP packaging |
| Audit Logging | Limited | MFA + full audit |
| Security Model | Open by default | Closed by default |
Migrate when:
- Distributing to multiple orgs (ISV, enterprise)
- Compliance requires audit trails
- DevOps needs automated credential rotation
- Moving to 2GP packaging
---
Migration Process
Phase 1: Assessment
1.1 Inventory Current Apps
# List all Connected Apps
sf org list metadata --metadata-type ConnectedApp --target-org <org>
# Retrieve for analysis
sf project retrieve start --metadata ConnectedApp --output-dir ./migration-review1.2 Document Configuration
For each Connected App, record:
| Setting | Value |
|---|---|
| App Name | |
| Consumer Key | |
| OAuth Scopes | |
| Callback URLs | |
| IP Restrictions | |
| Token Policy | |
| Certificate (if JWT) |
1.3 Identify Integrations
List all systems using each Connected App:
- External applications
- CI/CD pipelines
- Third-party tools
- Mobile applications
---
Phase 2: Planning
2.1 Choose Distribution Model
| Scenario | Distribution State |
|---|---|
| Single org only | Local |
| Multiple orgs, same company | Local per org or Packageable |
| ISV/AppExchange | Packageable |
2.2 Plan Credential Rollover
Timeline Example:
Week 1: Create ECA, deploy to DevHub
Week 2: Update integration systems with new credentials
Week 3: Test in sandbox environments
Week 4: Production cutover
Week 5-8: Monitor, keep old app active as fallback
Week 9: Deactivate Connected App
Week 12: Delete Connected App---
Phase 3: Create External Client App
3.1 Prepare Scratch Org (if needed)
// config/project-scratch-def.json
{
"orgName": "ECA Migration Dev",
"edition": "Developer",
"features": [
"ExternalClientApps",
"ExtlClntAppSecretExposeCtl"
]
}3.2 Create ECA Metadata Files
File 1: Header (MyApp.eca-meta.xml)
<?xml version="1.0" encoding="UTF-8"?>
<ExternalClientApplication xmlns="http://soap.sforce.com/2006/04/metadata">
<contactEmail>team@company.com</contactEmail>
<description>Migrated from Connected App: MyConnectedApp</description>
<distributionState>Local</distributionState>
<isProtected>false</isProtected>
<label>MyApp</label>
</ExternalClientApplication>File 2: Global OAuth (MyApp.ecaGlblOauth-meta.xml)
<?xml version="1.0" encoding="UTF-8"?>
<ExtlClntAppGlobalOauthSettings xmlns="http://soap.sforce.com/2006/04/metadata">
<callbackUrl>https://app.example.com/oauth/callback</callbackUrl>
<externalClientApplication>MyApp</externalClientApplication>
<isConsumerSecretOptional>false</isConsumerSecretOptional>
<isPkceRequired>true</isPkceRequired>
<label>MyApp Global OAuth</label>
<shouldRotateConsumerKey>true</shouldRotateConsumerKey>
<shouldRotateConsumerSecret>true</shouldRotateConsumerSecret>
</ExtlClntAppGlobalOauthSettings>File 3: OAuth Settings (MyApp.ecaOauth-meta.xml)
<?xml version="1.0" encoding="UTF-8"?>
<ExtlClntAppOauthSettings xmlns="http://soap.sforce.com/2006/04/metadata">
<commaSeparatedOauthScopes>Api, RefreshToken</commaSeparatedOauthScopes>
<externalClientApplication>MyApp</externalClientApplication>
<label>MyApp OAuth Settings</label>
</ExtlClntAppOauthSettings>Optional companion metadata (retrieve-first): MyApp.ecaOauthSecurity-meta.xml
- Use this when you need to source-control ECA OAuth security settings now supported by the CLI/SDR registry.
- Recommended workflow: retrieve from an org first, then commit the retrieved file under
extlClntAppOauthSecuritySettings/.
3.3 Deploy to DevHub/Org
sf project deploy start \
--metadata ExternalClientApplication:MyApp \
--metadata ExtlClntAppGlobalOauthSettings:MyApp \
--metadata ExtlClntAppOauthSettings:MyApp \
--target-org <target-org>3.4 Retrieve New Consumer Key
After deployment: 1. Go to Setup → External Client App Manager 2. Select your ECA 3. View Consumer Key (MFA required) 4. Securely store credentials
---
Phase 4: Update Integrations
4.1 Update External Systems
For each integrated system:
1. Update OAuth endpoint (if different environment) 2. Replace Consumer Key 3. Replace Consumer Secret 4. Test authentication flow 5. Verify API access
4.2 Configuration Mapping
| Connected App Setting | ECA Equivalent |
|---|---|
| Consumer Key | New Consumer Key (different) |
| Consumer Secret | New Consumer Secret (different) |
| Callback URL | Same (in ecaGlblOauth) |
| Scopes | Same (in ecaOauth) |
| IP Relaxation | In ecaPlcy (admin-managed) |
| Refresh Token Policy | In ecaPlcy (admin-managed) |
| OAuth security controls | In ecaOauthSecurity when source-controlled (retrieve-first) |
---
Phase 5: Testing
5.1 Test Checklist
- [ ] Authorization Code flow works
- [ ] Token refresh works
- [ ] API calls succeed
- [ ] Scopes are correct
- [ ] Error handling works
- [ ] Logout/revocation works
5.2 Test Script
# Test Authorization Code Flow
curl "https://login.salesforce.com/services/oauth2/authorize?\
response_type=code&\
client_id=<NEW_CONSUMER_KEY>&\
redirect_uri=<CALLBACK_URL>&\
scope=api%20refresh_token"
# Test Token Exchange
curl -X POST https://login.salesforce.com/services/oauth2/token \
-d "grant_type=authorization_code" \
-d "code=<AUTH_CODE>" \
-d "client_id=<NEW_CONSUMER_KEY>" \
-d "client_secret=<NEW_CONSUMER_SECRET>" \
-d "redirect_uri=<CALLBACK_URL>"---
Phase 6: Cutover
6.1 Production Deployment
# Deploy ECA to production
sf project deploy start \
--metadata ExternalClientApplication:MyApp \
--metadata ExtlClntAppGlobalOauthSettings:MyApp \
--metadata ExtlClntAppOauthSettings:MyApp \
--target-org production6.2 Cutover Steps
1. Deploy ECA to production 2. Configure policies in Setup 3. Update production integrations 4. Monitor for errors 5. Keep Connected App active as fallback
---
Phase 7: Decommission
7.1 Deactivate Connected App
1. Go to Setup → Connected Apps → Manage Connected Apps 2. Select the old Connected App 3. Click "Edit Policies" 4. Set "Permitted Users" to "Admin approved users are pre-authorized" 5. Remove all user/profile assignments
7.2 Monitor Period
- Monitor for 30 days minimum
- Check for authentication failures
- Investigate any traffic to old app
7.3 Delete Connected App
After monitoring period:
# Remove from source control
rm force-app/main/default/connectedApps/OldApp.connectedApp-meta.xml
# Or delete via Setup UI---
Rollback Plan
If migration fails:
1. Immediate: Revert external systems to old Consumer Key 2. Short-term: Keep Connected App active during transition 3. Long-term: Document issues and retry migration
---
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| "Invalid consumer key" | Using old key | Update to new ECA key |
| "Callback URL mismatch" | URL not in ECA config | Add URL to ecaGlblOauth |
| "Scope not allowed" | Scope not in ecaOauth | Add required scope |
| "User not authorized" | No policy assignment | Assign user via Permission Set |
| "MFA required" | ECA security | Complete MFA challenge |
---
Automation Script
#!/bin/bash
# migrate-connected-app.sh
APP_NAME=$1
TARGET_ORG=$2
echo "📦 Migrating Connected App: $APP_NAME"
# 1. Retrieve existing Connected App
sf project retrieve start \
--metadata "ConnectedApp:$APP_NAME" \
--target-org $TARGET_ORG \
--output-dir ./migration
# 2. Create ECA source directories
mkdir -p force-app/main/default/externalClientApps
mkdir -p force-app/main/default/extlClntAppGlobalOauthSets
mkdir -p force-app/main/default/extlClntAppOauthSettings
# Optional when you source-control security settings:
mkdir -p force-app/main/default/extlClntAppOauthSecuritySettings
# 3. Deploy ECA (assumes files are created)
sf project deploy start \
--source-dir force-app/main/default \
--target-org $TARGET_ORG
echo "✅ ECA deployed. Retrieve new Consumer Key from Setup."<!-- Parent: configuring-connected-apps/SKILL.md -->
OAuth Flows Reference for Connected Apps
Detailed OAuth flow patterns, configuration examples, and implementation guidance for the configuring-connected-apps skill.
---
Flow Selection Decision Tree
START
│
├─ Do you have a backend server?
│ ├─ YES: Can it securely store secrets?
│ │ ├─ YES: Authorization Code Flow (Web Server)
│ │ └─ NO: Authorization Code + PKCE (SPA/Mobile)
│ │
│ └─ NO: Is this server-to-server?
│ ├─ YES: JWT Bearer Flow
│ └─ NO: Device Authorization Flow (CLI/IoT)
│
└─ Is this for a specific integration user?
├─ YES: JWT Bearer Flow
└─ NO: Authorization Code Flow---
Authorization Code Flow (Web Server)
When to Use
- Web applications with backend server
- Can securely store consumer secret
- User-interactive flow needed
- Examples: Portal, Integration Hub, Admin Console
Connected App Configuration
Minimal scopes for API access:
<oauthConfig>
<callbackUrl>https://app.example.com/oauth/callback</callbackUrl>
<scopes>Api</scopes>
<scopes>RefreshToken</scopes>
<isConsumerSecretOptional>false</isConsumerSecretOptional>
</oauthConfig>With OpenID Connect:
<oauthConfig>
<callbackUrl>https://app.example.com/oauth/callback</callbackUrl>
<scopes>Api</scopes>
<scopes>RefreshToken</scopes>
<scopes>OpenID</scopes>
<isIdTokenEnabled>true</isIdTokenEnabled>
<isConsumerSecretOptional>false</isConsumerSecretOptional>
</oauthConfig>Security Checklist
- [ ] HTTPS callback URL (no localhost in production)
- [ ] Consumer secret stored in environment variables (never in code)
- [ ] State parameter validated (CSRF protection)
- [ ] Authorization code used only once
- [ ] Refresh token rotation enabled
- [ ] IP restrictions configured (optional)
Common Issues
Problem: "redirect_uri_mismatch" error
- Cause: Callback URL doesn't match exactly
- Fix: Ensure exact match including protocol, domain, path, and query parameters
Problem: "invalid_client_id" error
- Cause: Consumer key incorrect or app not deployed
- Fix: Verify consumer key from Setup > App Manager
---
Authorization Code + PKCE (Public Clients)
When to Use
- Single Page Applications (React, Vue, Angular)
- Mobile apps (iOS, Android)
- Desktop apps
- Any client that cannot securely store secrets
Connected App Configuration
<oauthConfig>
<callbackUrl>myapp://oauth/callback</callbackUrl>
<scopes>Api</scopes>
<scopes>RefreshToken</scopes>
<isConsumerSecretOptional>true</isConsumerSecretOptional>
<isPkceRequired>true</isPkceRequired>
</oauthConfig>
<oauthPolicy>
<refreshTokenPolicy>infinite</refreshTokenPolicy>
<isRefreshTokenRotationEnabled>true</isRefreshTokenRotationEnabled>
</oauthPolicy>External Client App Configuration
<!-- ecaGlblOauth-meta.xml -->
<ExtlClntAppGlobalOauthSettings xmlns="http://soap.sforce.com/2006/04/metadata">
<callbackUrl>myapp://oauth/callback</callbackUrl>
<externalClientApplication>MyMobileApp</externalClientApplication>
<isConsumerSecretOptional>true</isConsumerSecretOptional>
<isPkceRequired>true</isPkceRequired>
<isSecretRequiredForRefreshToken>false</isSecretRequiredForRefreshToken>
<label>Mobile App OAuth Settings</label>
</ExtlClntAppGlobalOauthSettings>Implementation Pattern (JavaScript)
// Generate PKCE verifier and challenge
function generatePKCE() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const codeVerifier = base64URLEncode(array);
return crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
.then(buffer => ({
codeVerifier,
codeChallenge: base64URLEncode(new Uint8Array(buffer))
}));
}
// Store verifier in sessionStorage (cleared on close)
const { codeVerifier, codeChallenge } = await generatePKCE();
sessionStorage.setItem('pkce_verifier', codeVerifier);
// Authorization URL
const authUrl = `https://login.salesforce.com/services/oauth2/authorize?` +
`response_type=code` +
`&client_id=${CLIENT_ID}` +
`&redirect_uri=${REDIRECT_URI}` +
`&scope=api%20refresh_token` +
`&state=${STATE}` +
`&code_challenge=${codeChallenge}` +
`&code_challenge_method=S256`;Security Checklist
- [ ] PKCE required in Connected App config
- [ ] Consumer secret optional
- [ ] Refresh token rotation enabled
- [ ] Code verifier stored securely (sessionStorage, keychain)
- [ ] State parameter validated
- [ ] Deep link callback handled securely (mobile)
---
JWT Bearer Flow (Server-to-Server)
When to Use
- CI/CD pipelines (GitHub Actions, Jenkins)
- Backend integrations without user interaction
- Service accounts
- Scheduled jobs
Prerequisites
1. Generate X.509 Certificate:
# Generate private key
openssl genrsa -out server.key 2048
# Generate certificate signing request
openssl req -new -key server.key -out server.csr
# Self-sign certificate (valid 1 year)
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt2. Upload to Salesforce:
- Setup > Certificate and Key Management > Create Self-Signed Certificate
- Or use the certificate from step 1
Connected App Configuration
<oauthConfig>
<certificate>JWTAuthCertificate</certificate>
<consumerKey>AUTO_GENERATED</consumerKey>
<scopes>Api</scopes>
<scopes>Web</scopes>
<isAdminApproved>true</isAdminApproved>
</oauthConfig>
<oauthPolicy>
<ipRelaxation>ENFORCE</ipRelaxation>
</oauthPolicy>Important: No callbackUrl needed for JWT Bearer flow.
Pre-Authorization
Option 1: Permission Set
<!-- permissionsets/IntegrationUser.permissionset-meta.xml -->
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<label>API Integration User</label>
<connectedAppSettings>
<connectedApp>MyJWTApp</connectedApp>
<enabled>true</enabled>
</connectedAppSettings>
<hasActivationRequired>false</hasActivationRequired>
</PermissionSet>Assign to integration user: Setup > Users > [User] > Permission Set Assignments
Option 2: Profile Setup > Manage Connected Apps > [App] > Edit Policies > Permitted Users = "Admin approved users are pre-authorized"
Implementation Pattern (Node.js)
const jwt = require('jsonwebtoken');
const axios = require('axios');
const fs = require('fs');
async function getAccessToken() {
const privateKey = fs.readFileSync('server.key', 'utf8');
const claims = {
iss: process.env.CONSUMER_KEY,
sub: 'integration@company.com', // Pre-authorized user
aud: 'https://login.salesforce.com',
exp: Math.floor(Date.now() / 1000) + 300 // 5 min
};
const assertion = jwt.sign(claims, privateKey, { algorithm: 'RS256' });
const response = await axios.post('https://login.salesforce.com/services/oauth2/token',
new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: assertion
})
);
return response.data.access_token;
}Security Checklist
- [ ] Private key stored securely (secrets manager, not in repo)
- [ ] Certificate uploaded to Salesforce
- [ ] User pre-authorized via Permission Set
- [ ] IP restrictions configured
- [ ] Token expiration set (exp claim)
- [ ] Audience (aud) set correctly (login vs test.salesforce.com)
Common Issues
Problem: "user hasn't approved this consumer" error
- Cause: User not pre-authorized
- Fix: Assign Permission Set or configure admin pre-approval
Problem: "invalid_grant" error
- Cause: Certificate mismatch or expired token
- Fix: Verify certificate name matches
<certificate>tag, check exp claim
---
Device Authorization Flow
When to Use
- CLI tools (sf CLI, custom CLIs)
- Smart TVs, Set-top boxes
- IoT devices without keyboard
- Any device with limited input capability
Connected App Configuration
<oauthConfig>
<callbackUrl>http://localhost:8080</callbackUrl>
<scopes>Api</scopes>
<scopes>RefreshToken</scopes>
<isConsumerSecretOptional>true</isConsumerSecretOptional>
</oauthConfig>Implementation Pattern (Python)
import requests
import time
CLIENT_ID = 'your_consumer_key'
DEVICE_CODE_URL = 'https://login.salesforce.com/services/oauth2/device/code'
TOKEN_URL = 'https://login.salesforce.com/services/oauth2/token'
# Step 1: Request device code
response = requests.post(DEVICE_CODE_URL, data={
'client_id': CLIENT_ID,
'scope': 'api refresh_token'
})
data = response.json()
# Step 2: Display user code
print(f"Visit: {data['verification_uri']}")
print(f"Enter code: {data['user_code']}")
# Step 3: Poll for token
device_code = data['device_code']
interval = data['interval'] # Polling interval in seconds
while True:
time.sleep(interval)
token_response = requests.post(TOKEN_URL, data={
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
'client_id': CLIENT_ID,
'code': device_code
})
if token_response.status_code == 200:
tokens = token_response.json()
print(f"Access Token: {tokens['access_token']}")
break
elif token_response.json().get('error') == 'authorization_pending':
continue # User hasn't authorized yet
else:
print(f"Error: {token_response.json()}")
breakSecurity Checklist
- [ ] Consumer secret optional
- [ ] Polling interval respected (don't spam)
- [ ] Device code expires after timeout
- [ ] Refresh token stored securely
---
Client Credentials Flow (ECA Only)
When to Use
- Service accounts (not tied to specific user)
- Background processes
- Microservices
- Requires External Client App (not available in Connected Apps)
External Client App Configuration
<!-- extlClntAppOauthSettings/MyServiceApp.ecaOauth-meta.xml -->
<ExtlClntAppOauthSettings xmlns="http://soap.sforce.com/2006/04/metadata">
<commaSeparatedOauthScopes>Api</commaSeparatedOauthScopes>
<externalClientApplication>MyServiceApp</externalClientApplication>
<label>Service OAuth Settings</label>
</ExtlClntAppOauthSettings>Source-control note: if you also need to manage client-credentials security settings in source, retrieve ExtlClntAppOauthSecuritySettings:MyServiceApp from an org first and commit the retrieved .ecaOauthSecurity-meta.xml file as your starting point.
Implementation Pattern
curl -X POST https://login.salesforce.com/services/oauth2/token \
-d "grant_type=client_credentials" \
-d "client_id=<CONSUMER_KEY>" \
-d "client_secret=<CONSUMER_SECRET>"Security Checklist
- [ ] Execution user configured
- [ ] Consumer secret rotated regularly
- [ ] Scopes minimal (least privilege)
- [ ] IP restrictions enabled
---
Refresh Token Patterns
Standard Refresh
async function refreshAccessToken(refreshToken) {
const response = await axios.post('https://login.salesforce.com/services/oauth2/token',
new URLSearchParams({
grant_type: 'refresh_token',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: refreshToken
})
);
return response.data;
}With Token Rotation (Recommended)
When isRefreshTokenRotationEnabled=true, each refresh returns a NEW refresh token:
async function refreshWithRotation(refreshToken) {
const response = await refreshAccessToken(refreshToken);
// Store NEW refresh token (old one is now invalid)
await secureStorage.set('refresh_token', response.refresh_token);
await secureStorage.set('access_token', response.access_token);
return response;
}Refresh Token Policies
| Policy | Description | Use Case |
|---|---|---|
infinite | Never expires | Trusted integrations |
immediately | Expires on use | Maximum security |
zero | Not issued | Access token only |
Configuration:
<oauthPolicy>
<refreshTokenPolicy>infinite</refreshTokenPolicy>
<isRefreshTokenRotationEnabled>true</isRefreshTokenRotationEnabled>
</oauthPolicy>---
Named Credentials Integration
Why Use Named Credentials
- Secrets managed by Salesforce (not in code)
- Automatic token refresh
- Per-user or per-org authentication
- Audit trail in Setup Audit Trail
Create Named Credential for JWT Flow
<!-- namedCredentials/SalesforceAPI.namedCredential-meta.xml -->
<NamedCredential xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Salesforce API</label>
<endpoint>https://yourinstance.salesforce.com</endpoint>
<protocol>NoAuthentication</protocol>
<principalType>NamedUser</principalType>
<oauthConfig>
<certificate>JWTAuthCertificate</certificate>
<consumerKey>YOUR_CONSUMER_KEY</consumerKey>
<oauthFlows>JwtBearer</oauthFlows>
<username>integration@company.com</username>
</oauthConfig>
</NamedCredential>Use in Apex
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:SalesforceAPI/services/data/v66.0/query?q=SELECT+Id+FROM+Account');
req.setMethod('GET');
Http http = new Http();
HttpResponse res = http.send(req);---
Token Introspection
Use: Validate token status and metadata
curl -X POST https://login.salesforce.com/services/oauth2/introspect \
-d "token=<ACCESS_TOKEN>" \
-d "client_id=<CONSUMER_KEY>" \
-d "client_secret=<CONSUMER_SECRET>" \
-d "token_type_hint=access_token"---
Token Revocation
Use: Invalidate tokens on logout
curl -X POST https://login.salesforce.com/services/oauth2/revoke \
-d "token=<TOKEN>"---
Error Handling Patterns
OAuth Error Response Structure
{
"error": "invalid_grant",
"error_description": "authentication failure"
}Common Errors
| Error Code | Meaning | Resolution |
|---|---|---|
invalid_client_id | Consumer key invalid | Verify key from Setup |
invalid_client | Secret incorrect | Check consumer secret |
redirect_uri_mismatch | Callback URL mismatch | Match exactly with config |
invalid_grant | Auth code expired/used | Request new authorization |
unsupported_grant_type | Flow not enabled | Enable in Connected App |
invalid_scope | Scope not allowed | Check available scopes |
access_denied | User declined | User must approve |
Retry Logic Example
async function callSalesforceAPI(accessToken, retries = 1) {
try {
return await axios.get('https://instance.salesforce.com/services/data/v66.0/query', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
} catch (error) {
if (error.response?.status === 401 && retries > 0) {
// Token expired, refresh and retry
const newToken = await refreshAccessToken();
return callSalesforceAPI(newToken, retries - 1);
}
throw error;
}
}---
Scoring Impact by Flow
| Flow | Security Score Impact | Best Practices Score |
|---|---|---|
| Authorization Code + PKCE | +10 (PKCE enabled) | +10 (modern flow) |
| JWT Bearer | +5 (certificate) | +15 (server-to-server best practice) |
| Device Authorization | +5 (secret optional) | +10 (appropriate for CLI) |
| Username-Password | -10 (deprecated) | -10 (anti-pattern) |
Recommendation: JWT Bearer or Authorization Code + PKCE score highest (90-100/120).
---
Testing OAuth Flows
Postman Collection Variables
{
"login_url": "https://login.salesforce.com",
"client_id": "{{CONSUMER_KEY}}",
"client_secret": "{{CONSUMER_SECRET}}",
"redirect_uri": "https://localhost/oauth/callback",
"username": "test@company.com",
"password": "password123"
}Quick Test: JWT Bearer
# Generate JWT (requires jq)
JWT=$(python3 -c "
import jwt, time, os
claims = {
'iss': os.getenv('CONSUMER_KEY'),
'sub': 'integration@company.com',
'aud': 'https://login.salesforce.com',
'exp': int(time.time()) + 300
}
with open('server.key') as f:
print(jwt.encode(claims, f.read(), algorithm='RS256'))
")
# Get token
curl -X POST https://login.salesforce.com/services/oauth2/token \
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
-d "assertion=$JWT" | jq---
Migration Strategies
Connected App → External Client App
Step 1: Create equivalent ECA
# Read existing Connected App
Grep: pattern="<oauthConfig>" path="force-app/main/default/connectedApps/"
# Create new ECA with same scopes
# Use templates: external-client-app.xml, eca-global-oauth.xmlStep 2: Parallel operation
- Deploy ECA alongside Connected App
- Update one integration at a time
- Monitor both apps
Step 3: Cutover
- Update all integrations to use new Consumer Key
- Disable old Connected App
- Archive after 30 days
Scoring benefit: ECA typically scores 15-20 points higher due to modern security model.
---
Quick Reference
Template Selection by Flow
| Flow | Template File |
|---|---|
| Authorization Code (basic) | connected-app-oauth.xml |
| JWT Bearer | connected-app-jwt.xml |
| Mobile/SPA (PKCE) | external-client-app.xml + eca-global-oauth.xml |
| Device Authorization | connected-app-basic.xml (secret optional) |
| Client Credentials | eca-oauth-settings.xml + optional retrieve-first ecaOauthSecurity metadata (ECA only) |
Salesforce OAuth Endpoints
| Environment | Base URL |
|---|---|
| Production | https://login.salesforce.com |
| Sandbox | https://test.salesforce.com |
| Custom Domain | https://yourdomain.my.salesforce.com |
Key Endpoints
- Authorize:
/services/oauth2/authorize - Token:
/services/oauth2/token - Revoke:
/services/oauth2/revoke - Introspect:
/services/oauth2/introspect - Device Code:
/services/oauth2/device/code - UserInfo (OpenID):
/services/oauth2/userinfo
---
Related Resources
- Flow Diagrams: Included above (Authorization Code, JWT Bearer, Device Authorization)
- Security Checklist: references/security-checklist.md
- Migration Guide: references/migration-guide.md
- Main Skill: SKILL.md
<!-- Parent: configuring-connected-apps/SKILL.md -->
Security Checklist for Connected Apps & ECAs
Use this checklist before deploying any OAuth application to production.
Pre-Deployment Checklist
OAuth Configuration
- [ ] Callback URLs are specific (no wildcards)
- [ ] All URLs use HTTPS (required for production)
- [ ] PKCE is enabled for public clients (mobile, SPA)
- [ ] Consumer secret is protected (not in source control)
- [ ] Minimal scopes selected (principle of least privilege)
- [ ] No deprecated scopes in use
Token Policies
- [ ] Access token expiration is configured appropriately
- [ ] Refresh token policy matches use case
infiniteonly for trusted server applicationsspecific_lifetimefor user-facing appszerofor JWT Bearer flows- [ ] Token rotation enabled for ECAs in production
- [ ] IP restrictions configured for server-to-server apps
Access Control
- [ ] Admin pre-authorization required for sensitive apps
- [ ] User assignment via Permission Set or Profile
- [ ] Connected App policies reviewed in Setup
- [ ] High Assurance session required if needed
External Client Apps (Additional)
- [ ] Distribution state correctly set (Local vs Packageable)
- [ ] Consumer key rotation enabled for production
- [ ] Consumer secret rotation enabled for production
- [ ] Policies file reviewed after first deployment
---
Security Scoring Criteria
Critical (Block Deployment)
| Issue | Impact | Fix |
|---|---|---|
| Wildcard callback URL | Token hijacking | Use specific URLs |
| HTTP callback URL | Credential interception | Use HTTPS only |
| Full scope without justification | Over-privileged access | Use minimal scopes |
| Consumer secret in code | Credential leak | Use environment variables |
High Priority
| Issue | Impact | Fix |
|---|---|---|
| PKCE disabled for mobile/SPA | Auth code interception | Enable PKCE |
| No IP restrictions (server) | Unauthorized access | Configure IP ranges |
| Infinite refresh tokens (user app) | Long-term compromise | Set expiration |
| No token rotation (ECA) | Compromised credentials | Enable rotation |
Medium Priority
| Issue | Impact | Fix |
|---|---|---|
| Missing description | Audit difficulty | Add clear description |
| Generic contact email | Incident response delay | Use team email |
| Introspection disabled | Token validation gaps | Enable if needed |
| No logout URL | Session persistence | Configure logout |
---
Scope Security Guide
Recommended Scopes by Use Case
| Use Case | Recommended Scopes |
|---|---|
| API Integration | Api, RefreshToken |
| User Authentication | OpenID, Profile, Email |
| Full API + Identity | Api, RefreshToken, OpenID, Profile |
| Chatter Integration | Api, ChatterApi |
| Server-to-Server | Api only |
Scopes to Avoid
| Scope | Risk | Alternative |
|---|---|---|
Full | Complete access to everything | Use specific scopes |
Web + Api together | Redundant, increases attack surface | Choose one |
RefreshToken for JWT | Unnecessary, JWT generates new tokens | Remove scope |
---
IP Restriction Policies
Policy Options
| Policy | Description | Use Case |
|---|---|---|
ENFORCE | Strict IP enforcement | Production server-to-server |
BYPASS | No IP restrictions | Development only |
ENFORCE_ACTIVATED_USERS | Enforce for active users | Mixed environments |
Recommended Configuration
<!-- Production: Server-to-Server -->
<oauthPolicy>
<ipRelaxation>ENFORCE</ipRelaxation>
</oauthPolicy>
<!-- Production: User-Facing -->
<oauthPolicy>
<ipRelaxation>ENFORCE_ACTIVATED_USERS</ipRelaxation>
</oauthPolicy>
<!-- Development Only -->
<oauthPolicy>
<ipRelaxation>BYPASS</ipRelaxation>
</oauthPolicy>---
Certificate Management (JWT Bearer)
Certificate Requirements
- [ ] RSA 2048-bit or higher key size
- [ ] Valid not-before and not-after dates
- [ ] Uploaded to Salesforce Certificate and Key Management
- [ ] Private key stored securely (HSM, Vault, secure storage)
Certificate Rotation
1. Generate new certificate before expiration 2. Upload new certificate to Salesforce 3. Update Connected App to use new certificate 4. Update external system with new private key 5. Test authentication 6. Remove old certificate after transition period
---
Monitoring & Audit
What to Monitor
- [ ] Login History for Connected App users
- [ ] OAuth Usage in Setup
- [ ] Event Monitoring for token events (Shield required)
- [ ] API Usage limits and patterns
Audit Checklist
- [ ] Review Connected Apps quarterly
- [ ] Remove unused applications
- [ ] Rotate credentials annually
- [ ] Verify scope appropriateness
- [ ] Check for policy violations
---
Incident Response
If Credentials Are Compromised
1. Immediately rotate Consumer Secret 2. Revoke all active tokens 3. Review login and access logs 4. Update external systems with new credentials 5. Investigate scope of compromise 6. Document and report incident
Commands for Response
# List all Connected Apps
sf org list metadata --metadata-type ConnectedApp --target-org <org>
# Retrieve for review
sf project retrieve start --metadata ConnectedApp:<AppName> --target-org <org>---
Compliance Considerations
GDPR/Privacy
- Ensure data access matches user consent
- Document data flows through Connected Apps
- Implement data retention policies
SOC 2
- Enable audit logging
- Implement access reviews
- Document security controls
- Use certificate-based authentication
HIPAA
- Enable High Assurance sessions
- Restrict data access scopes
- Implement IP restrictions
- Use encrypted connections only
<!-- Parent: configuring-connected-apps/SKILL.md -->
Testing & Validation Guide
This guide documents tested External Client App (ECA) and Connected App patterns based on systematic validation testing (December 2025).
Test Matrix
| Component Type | Template / Source | Test Status |
|---|---|---|
| Connected App (Basic) | connected-app-basic.xml | ✅ Verified |
| Connected App (Full OAuth) | connected-app-oauth.xml | ✅ Verified |
| Connected App (JWT Bearer) | connected-app-jwt.xml | ✅ Verified |
| Connected App (Canvas) | connected-app-canvas.xml | ⚠️ Location-dependent |
| External Client App | external-client-app.xml | ✅ Verified |
| ECA Global OAuth | eca-global-oauth.xml | ✅ Verified |
| ECA OAuth Settings | eca-oauth-settings.xml | ✅ Verified |
| ECA OAuth Security Settings | retrieve-first (ExtlClntAppOauthSecuritySettings) | ✅ CLI/registry-supported |
| ECA Configurable Policies | eca-policies.xml | ✅ Naming updated |
---
Critical File Naming Conventions
External Client App Files
| Metadata Type | Source Directory | File Suffix | Example |
|---|---|---|---|
| ExternalClientApplication | externalClientApps/ | .eca-meta.xml | externalClientApps/MyApp.eca-meta.xml |
| ExtlClntAppOauthSettings | extlClntAppOauthSettings/ | .ecaOauth-meta.xml | extlClntAppOauthSettings/MyApp.ecaOauth-meta.xml |
| ExtlClntAppGlobalOauthSettings | extlClntAppGlobalOauthSets/ | .ecaGlblOauth-meta.xml | extlClntAppGlobalOauthSets/MyApp.ecaGlblOauth-meta.xml |
| ExtlClntAppOauthSecuritySettings | extlClntAppOauthSecuritySettings/ | .ecaOauthSecurity-meta.xml | extlClntAppOauthSecuritySettings/MyApp.ecaOauthSecurity-meta.xml |
| ExtlClntAppOauthConfigurablePolicies | extlClntAppOauthPolicies/ | .ecaOauthPlcy-meta.xml | extlClntAppOauthPolicies/MyApp.ecaOauthPlcy-meta.xml |
| ExtlClntAppConfigurablePolicies | extlClntAppPolicies/ | .ecaPlcy-meta.xml | extlClntAppPolicies/MyApp.ecaPlcy-meta.xml |
⚠️ CRITICAL:
- Use
.ecaGlblOauth(abbreviated), NOT.ecaGlobalOauth - Use
.ecaPlcy, NOT.ecaPolicy ExtlClntAppOauthSecuritySettingsis now source-supported; prefer retrieve-first until you have an org-validated sample file
Connected App Files
| Metadata Type | File Suffix | Example |
|---|---|---|
| ConnectedApp | .connectedApp-meta.xml | MyApp.connectedApp-meta.xml |
---
Common Deployment Errors & Solutions
Error: "Invalid or missing field" in ECA OAuth Settings
Cause: Using wrong schema with non-existent fields
Wrong Schema (FAILS):
<ExtlClntAppOauthSettings>
<isAdminApproved>true</isAdminApproved> <!-- DOES NOT EXIST -->
<isCodeCredentialsEnabled>true</isCodeCredentialsEnabled> <!-- DOES NOT EXIST -->
<scopes>Api</scopes> <!-- WRONG FORMAT -->
</ExtlClntAppOauthSettings>Correct Schema:
<ExtlClntAppOauthSettings xmlns="http://soap.sforce.com/2006/04/metadata">
<commaSeparatedOauthScopes>Api, RefreshToken</commaSeparatedOauthScopes>
<externalClientApplication>MyApp</externalClientApplication>
<label>MyApp OAuth Settings</label>
</ExtlClntAppOauthSettings>Error: "Missing required field" in ECA Global OAuth
Cause: Missing externalClientApplication or label
Wrong (FAILS):
<ExtlClntAppGlobalOauthSettings>
<callbackUrl>https://example.com/callback</callbackUrl>
<isPkceRequired>true</isPkceRequired>
</ExtlClntAppGlobalOauthSettings>Correct:
<ExtlClntAppGlobalOauthSettings xmlns="http://soap.sforce.com/2006/04/metadata">
<callbackUrl>https://example.com/callback</callbackUrl>
<externalClientApplication>MyApp</externalClientApplication>
<isConsumerSecretOptional>true</isConsumerSecretOptional>
<isIntrospectAllTokens>false</isIntrospectAllTokens>
<isPkceRequired>true</isPkceRequired>
<isSecretRequiredForRefreshToken>false</isSecretRequiredForRefreshToken>
<label>MyApp Global OAuth</label>
<shouldRotateConsumerKey>false</shouldRotateConsumerKey>
<shouldRotateConsumerSecret>false</shouldRotateConsumerSecret>
</ExtlClntAppGlobalOauthSettings>Error: "Organization is not configured to support location"
Cause: Canvas app using location that requires org feature enablement
Problematic Locations:
AppLauncher- Requires feature enablement- Some other locations may have similar requirements
Solution: Use universally available locations:
<canvasApp>
<locations>Visualforce</locations>
</canvasApp>Error: Certificate not found (JWT Bearer)
Cause: Referenced certificate doesn't exist in org
Wrong:
<certificate>NonExistent_Cert</certificate>Solution: Create certificate first, then reference it:
# 1. Create certificate in org (Setup > Certificate and Key Management)
# 2. Reference in Connected App
<certificate>My_JWT_Cert</certificate>---
Field Reference
ExtlClntAppOauthSettings (ECA OAuth)
| Field | Type | Required | Description |
|---|---|---|---|
commaSeparatedOauthScopes | String | Yes | Comma-separated scopes (e.g., "Api, RefreshToken") |
externalClientApplication | String | Yes | Reference to parent ECA API name |
label | String | Yes | Display label |
Available Scopes:
Api- REST/SOAP API accessRefreshToken- Offline accessOpenID- OpenID ConnectProfile- User profile infoEmail- User emailWeb- Web browser accessChatterApi- Chatter REST API
ExtlClntAppGlobalOauthSettings (ECA Global OAuth)
| Field | Type | Required | Description |
|---|---|---|---|
callbackUrl | URL | Yes | OAuth redirect URI |
externalClientApplication | String | Yes | Reference to parent ECA |
isConsumerSecretOptional | Boolean | No | True for public clients with PKCE |
isIntrospectAllTokens | Boolean | No | Enable token introspection |
isPkceRequired | Boolean | No | Require PKCE (recommended for public clients) |
isSecretRequiredForRefreshToken | Boolean | No | Require secret for refresh |
label | String | Yes | Display label |
shouldRotateConsumerKey | Boolean | No | Enable key rotation |
shouldRotateConsumerSecret | Boolean | No | Enable secret rotation |
---
Deployment Order
For External Client Apps, deploy in this order:
# 1. Deploy base ECA first
sf project deploy start --metadata ExternalClientApplication:MyApp --target-org [alias]
# 2. Deploy OAuth settings
sf project deploy start --metadata ExtlClntAppOauthSettings:MyApp --target-org [alias]
# 3. Deploy Global OAuth (if needed)
sf project deploy start --metadata ExtlClntAppGlobalOauthSettings:MyApp --target-org [alias]
# 4. Deploy companion security/policy metadata when you use it
sf project deploy start --metadata ExtlClntAppOauthSecuritySettings:MyApp --target-org [alias]
sf project deploy start --metadata ExtlClntAppOauthConfigurablePolicies:MyApp --target-org [alias]
sf project deploy start --metadata ExtlClntAppConfigurablePolicies:MyApp --target-org [alias]
# Or deploy the whole package directory when all ECA source directories live under it
sf project deploy start --source-dir force-app/main/default --target-org [alias]---
Post-Deployment Steps
Client Credentials Flow (ECA)
Client Credentials flow still requires post-deployment admin validation. Even with broader metadata support, verify these items in Setup after deployment:
1. Create Permission Set with "Salesforce API Integration" permission 2. Assign Permission Set to the External Client App 3. Confirm the run-as / execution-user behavior and any org-specific OAuth security settings
JWT Bearer Flow (Connected App)
1. Upload Certificate to org before deployment 2. Pre-authorize Users after deployment (Setup > Manage Connected Apps)
---
Deployment Checklist
External Client App
- [ ]
.eca-meta.xmlfile exists underexternalClientApps/ - [ ]
.ecaOauth-meta.xmlfile exists underextlClntAppOauthSettings/ - [ ]
.ecaGlblOauth-meta.xmluses abbreviated suffix underextlClntAppGlobalOauthSets/ - [ ] Optional companion files use the right suffixes:
.ecaOauthSecurity,.ecaOauthPlcy,.ecaPlcy - [ ] All companion files reference the same
externalClientApplicationname - [ ] Required files include
labelwhere applicable - [ ]
commaSeparatedOauthScopesuses string format, not individual<scopes>tags - [ ] Callback URL is HTTPS (or custom scheme for mobile)
- [ ] PKCE enabled for public clients
Connected App
- [ ]
.connectedApp-meta.xmlfile exists - [ ]
contactEmailis valid - [ ] Callback URL matches OAuth flow requirements
- [ ] Certificate exists in org (for JWT Bearer)
- [ ] Canvas locations are supported by org (if using Canvas)
---
Tested Configurations
Minimal ECA (API Integration)
Files:
├── externalClientApps/MyApp.eca-meta.xml
├── extlClntAppOauthSettings/MyApp.ecaOauth-meta.xml
└── extlClntAppGlobalOauthSets/MyApp.ecaGlblOauth-meta.xml
Scopes: Api, RefreshToken
PKCE: false (confidential client)Mobile App ECA (PKCE)
Files:
├── externalClientApps/MobileApp.eca-meta.xml
├── extlClntAppOauthSettings/MobileApp.ecaOauth-meta.xml
└── extlClntAppGlobalOauthSets/MobileApp.ecaGlblOauth-meta.xml
Scopes: Api, RefreshToken, OpenID
PKCE: true (public client)
Callback: com.example.app://oauth/callbackServer-to-Server ECA
Files:
├── externalClientApps/ServiceApp.eca-meta.xml
├── extlClntAppOauthSettings/ServiceApp.ecaOauth-meta.xml
└── extlClntAppOauthSecuritySettings/ServiceApp.ecaOauthSecurity-meta.xml # retrieve-first when source controlling OAuth security settings
Scopes: Api
Note: Client Credentials still requires post-deployment Permission Set assignment and admin verification---
Last Updated: April 2026 Based on testing with SF CLI and API v66.0
Related skills
Forks & variants (1)
Configuring Connected Apps has 1 known copy in the catalog totaling 526 installs. They canonicalize to this original listing.
- forcedotcom - 526 installs
How it compares
Use this over manual Setup UI clicks when you want version-controlled, repeatable Connected App metadata across dev, staging, and production orgs.
FAQ
Connected App or ECA for new work?
Prefer External Client App for new regulated or packageable solutions; Connected Apps are for simple legacy single-org cases.
Can I create ECA OAuth security settings from scratch?
No. Retrieve ExtlClntAppOauthSecuritySettings from the org first; the file schema is retrieve-only.
May consumer secrets live in git?
Never commit consumer secrets; consumer keys are generated post-deploy and must be retrieved from the org.
Is Configuring Connected Apps safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.