Connected App Troubleshooting
OAuth via Connected Apps has a consistent set of failure modes that admins hit repeatedly: refresh tokens silently revoked, users not assigned to the app, IP restrictions blocking the caller, callback URL mismatches, and the error catalog being opaque ("invalid_grant" can mean five different things).
This skill is the diagnostic playbook. It assumes a Connected App exists and an OAuth flow is failing; the input is the error symptom, the output is the next action.
What this skill is NOT. Designing the OAuth architecture (which
flow to pick, JWT setup, certificate management) is
security/oauth-flows-and-connected-apps. SAML / SSO debugging is
admin/sso-saml-troubleshooting. This skill is for the
"OAuth-via-Connected-App is failing; what now" moment.
Before Starting
- Capture the error code verbatim.
invalid_grantvsinvalid_client_idvsunsupported_grant_typemean different things. - Identify the flow. Web Server, JWT Bearer, User Agent, Device, Refresh Token, Username-Password (deprecated for most uses).
- Identify the user. Known interactive user, dedicated integration user, Connected App pre-approved user.
- Pull Login History. Setup → Login History filtered by the user / time window. The Salesforce side captures every login attempt with status code; many failures show here even when the client only sees a generic OAuth error.
Core Concepts
The OAuth error catalog
| Error code | Common cause | Fix |
|---|---|---|
invalid_grant |
Refresh token revoked / expired / not issued for this client | Verify Refresh Token Policy on the Connected App; re-authorize the user |
invalid_client_id |
Wrong Consumer Key | Check the Consumer Key matches the Connected App in the right org |
invalid_client_credentials |
Wrong Consumer Secret | Reset / fetch the secret in Setup |
unsupported_grant_type |
Connected App's "Permitted Users" or "OAuth Policies" don't permit this flow | Edit the Connected App; ensure the flow's grant type is enabled |
redirect_uri_mismatch |
Callback URL in app doesn't match the request's redirect_uri | Update the Connected App's callback URL list |
inactive_user |
User is deactivated | Activate the user OR use a different user |
IP_RESTRICTED |
Connected App's IP relaxation = "Enforce IP restrictions" + caller IP not on user's profile login range | Either: relax to "Relax IP restrictions for activated devices", OR add IP to user's profile range |
error=login_required (silent) |
Session expired but Connected App is configured for SSO bypass | User must re-authenticate; auto-refresh isn't applicable |
OAUTH_APP_BLOCKED |
Setup → Connected Apps Usage → admin blocked the app | Unblock or pick a different app |
Refresh Token Policy: the silent-killer setting
On the Connected App, Refresh Token Policy has four values:
| Value | Behavior |
|---|---|
| Refresh token is valid until revoked | Token works indefinitely until explicitly revoked. |
| Immediately expire refresh token | Refresh token expires after one use. |
| Expire refresh token if not used for N days | Sliding window; inactive refresh tokens expire. |
| Expire refresh token after N days | Hard expiry from issuance. |
The default in older orgs was "Immediately expire refresh token" or a short-window equivalent — the integration works for one access-token lifetime, then dies on first refresh. This is the single most-common silent OAuth failure for server-to-server integrations.
Right answer for server-to-server: "Refresh token is valid until revoked." Pair with credential-rotation discipline.
IP Relaxation
| Setting | Behavior |
|---|---|
| Enforce IP restrictions | Apply user's profile IP range; calls from outside fail with IP_RESTRICTED. |
| Relax IP restrictions for activated devices | First use prompts a device-verification email; subsequent calls from the verified device skip IP check. |
| Relax IP restrictions | Skip IP check for this Connected App. |
For server-to-server integrations from cloud infrastructure (AWS, Azure, etc.) where the IP set is large or rotating, "Relax IP restrictions" plus a tightly-scoped integration user is the standard pattern.
User assignment to the Connected App
The user authenticating via the Connected App must be authorized to use it. Two models:
- All users may self-authorize. Anyone with API access can use the app; the user sees a consent screen on first use.
- Admin-pre-approved users only. The app must be assigned to
the user via a profile or permission set's
"Connected App Access" entries. Without assignment, the user
gets
OAUTH_APP_BLOCKEDor similar.
Server-to-server integrations should use admin-pre-approved + a dedicated integration user. The integration user has only the permsets needed; the Connected App is assigned to those permsets specifically.
Login History as the diagnostic source
Setup → Login History (or LoginHistory SOQL):
Statuscolumn tells you "Success" / "Failed: User not assigned to Connected App" / "Failed: Restricted IP" / "Invalid Password" / etc.SourceIpverifies the caller's IP.Applicationconfirms the Connected App in use.AuthenticationServiceIdidentifies the auth method.
Failures often show in Login History with a clearer cause than the client receives. Always check Login History before guessing.
Common Patterns
Pattern A — JWT Bearer Flow setup that "works once, then dies"
Symptom. First call succeeds; subsequent calls fail with
invalid_grant.
Cause. Refresh Token Policy isn't relevant for JWT Bearer (JWT itself is the credential, no refresh token), but the JWT signature validation may fail if:
- Certificate expired.
- Wrong certificate referenced (Connected App keyed to one cert, client signing with another).
- Username mismatch (
subclaim must be the username, exactly).
Right answer. Check certificate expiry. Match the certificate
in the Connected App's Use digital signatures setting against
the cert the client signs with. Test with a fresh JWT generated
manually to isolate variables.
Pattern B — Server-to-server integration's refresh token dies on day 2
Symptom. Web Server flow integration works on Monday; fails
Tuesday with invalid_grant.
Cause. Refresh Token Policy is "Immediately expire refresh token" (default in older orgs) or a short window.
Right answer. Set Refresh Token Policy = "Refresh token is valid until revoked." Re-authorize the integration user (initial authorization captures a new refresh token under the new policy).
Pattern C — User can't access the Connected App
Symptom. User OAuth flow fails with OAUTH_APP_BLOCKED or a
silent fall-through.
Diagnosis. Setup → Apps → Connected Apps → click the app → "OAuth Policies" → "Permitted Users":
All users may self-authorize— user gets consent prompt.Admin approved users are pre-authorized— user must be assigned via profile or permset.
For "Admin approved" model: Setup → Profiles or Permission Sets → edit the user's profile/permset → "Connected App Access" → enable the app.
Pattern D — Cloud-hosted integration hits IP restriction
Symptom. Integration on AWS Lambda / Heroku fails with
IP_RESTRICTED from a different IP each time.
Cause. Connected App IP Relaxation = "Enforce IP restrictions"
- user's profile has a tight IP range that doesn't match the cloud IP pool.
Right answer. Connected App IP Relaxation = "Relax IP restrictions" (cloud IPs are too dynamic for IP-range pinning). Compensate by tightening the integration user's permission scope and rotating credentials regularly.
Pattern E — Connected App admin-blocked for security review
Symptom. All flows for the app fail with
OAUTH_APP_BLOCKED.
Cause. Setup → Connected Apps OAuth Usage → admin set the app to "Block" while reviewing or revoking access.
Fix. Unblock the app, OR (if the block was intentional) migrate the integration to a different Connected App that's been approved.
Decision Guidance
| Symptom | Diagnosis | Fix |
|---|---|---|
invalid_grant after first day |
Refresh token policy too aggressive | Set to "Valid until revoked" |
invalid_grant immediately on first call |
Refresh token never issued (wrong scope, wrong user) | Add refresh_token scope; re-auth |
invalid_client_id |
Wrong Consumer Key | Verify against Connected App |
IP_RESTRICTED from cloud infra |
Tight IP enforcement | Relax IP restrictions |
OAUTH_APP_BLOCKED |
User not assigned OR app admin-blocked | Profile / permset assignment OR unblock |
redirect_uri_mismatch |
Callback URL not in app's list | Add the URL to the app |
| Silent failure / popup blocked | OAuth Web Server flow + popup blocker | Test in incognito; or use device flow |
unsupported_grant_type |
Flow not enabled in Connected App's OAuth Policies | Edit app; enable the grant type |
| Inactive user error | Integration user deactivated | Reactivate or migrate to new user |
| First call works, every subsequent call fails | Refresh token policy = "Immediately expire" | Set to "Valid until revoked" |
Recommended Workflow
- Capture the OAuth error code verbatim from the client.
- Pull Login History for the user + time window. Failures often show clearer causes there.
- Match against the error catalog. Most issues fit a known shape.
- Apply the fix in the Connected App settings or user / profile assignment.
- Test with a fresh credential (don't rely on the cached one that may be in a bad state).
- Verify in Login History that the test attempt now shows Success.
Review Checklist
- Error code captured exactly from the client.
- Login History checked for the user / time window.
- Connected App Refresh Token Policy is "Valid until revoked" for server-to-server.
- User is assigned to the Connected App via profile or permset.
- Callback URL list includes every URL the client uses.
- IP Relaxation matches the integration's IP source (relax for cloud, enforce for on-prem).
- Cert (for JWT Bearer) is current and matches client.
- Test post-fix verifies Success in Login History.
Salesforce-Specific Gotchas
- Default Refresh Token Policy in older orgs is "Immediately expire" — works for one access-token lifetime, then dies. (See
references/gotchas.md§ 1.) invalid_grantis overloaded — five different causes; check Login History to disambiguate. (Seereferences/gotchas.md§ 2.)- User assignment to Connected App is required for "Admin pre-approved" mode. Profile or permset entry. (See
references/gotchas.md§ 3.) redirect_urimismatch is character-exact — trailing slashes matter. (Seereferences/gotchas.md§ 4.)- JWT
subclaim must be the user's Username, not Email. (Seereferences/gotchas.md§ 5.) - Connected App admin-block is a separate setting from disabling OAuth. (See
references/gotchas.md§ 6.) - IP Relaxation interacts with the user's profile IP range — both must permit the caller. (See
references/gotchas.md§ 7.)
Output Artifacts
| Artifact | Description |
|---|---|
| Error → Cause → Fix mapping | Translated explanation for the specific OAuth error |
| Connected App settings change | Refresh Token Policy, IP Relaxation, Permitted Users, Callback URL |
| User assignment update | Profile / permset Connected App Access entry |
| Verification via Login History | Confirms the test post-fix shows Success |
Related Skills
security/oauth-flows-and-connected-apps— designing the OAuth architecture; this skill is the troubleshooting half.admin/sso-saml-troubleshooting— SAML-specific debugging (different runtime).apex/jwt-bearer-flow— JWT-specific setup including certificate management.admin/external-credentials-setup— modern External Credentials (post-Named-Credential migration) interact with OAuth flows.