Home / apex / apex-jwt-bearer-flow
apex-jwt-bearer-flow
OAuth 2.0 JWT Bearer Token Flow for server-to-server authentication from Apex — Connected App with certificate, Auth.JWT/Auth.JWS to mint signed assertions, token endpoint exchange, and the failure modes (clock skew, certificate not found, user not pre-authorized). NOT for choosing which OAuth flow to use — use integration/oauth-flows-and-connected-apps. NOT for Connected App setup — use admin/connected-apps-and-auth.
Open the skill folder on GitHub
Apex JWT Bearer Flow
The OAuth 2.0 JWT Bearer Token Flow lets server-side code obtain an access token without a stored password and without an interactive login. The client presents a short-lived JSON Web Token signed with a private key; the authorization server (Salesforce, or an external IdP) verifies the signature against a registered public key and returns an access token.
In Salesforce Apex this comes up in two shapes. The outbound shape is the one this skill covers: Apex calling a non-Salesforce system (another Salesforce org, a Heroku service, an internal API, a SaaS that supports JWT bearer) and needing an access token. The two correct ways to implement it are (a) a Named Credential with "JWT Bearer" auth — Salesforce manages the assertion and token exchange for you — and (b) Apex that builds the assertion with Auth.JWT and Auth.JWS, posts it to /oauth2/token, and parses the response.
The inbound shape — an external system getting a token into Salesforce via JWT — is configured entirely on the Connected App ("Use digital signatures" + uploaded certificate, "Pre-authorized" profile/permset). No Apex is needed. That is out of scope for this skill.
The mistakes are predictable. The JWT exp claim must be inside the next ~5 minutes (Salesforce rejects assertions older than 5 minutes); aud must be the server's login URL (not your org's); sub must be a username the Connected App has been pre-authorized to issue tokens for; and the certificate used to sign must match the public key uploaded to the Connected App. Any one of these mismatches returns the same opaque invalid_grant error.
Recommended Workflow
- Prefer Named Credentials over hand-rolled Apex. Setup → Named
Credentials → New, choose "JWT Bearer" identity type. Salesforce manages signing, refresh, and token caching. Reach for Apex
Auth.JWTonly when the target system requires a non-standard claim or a non-Salesforce IdP that Named Credentials does not support. - **Generate or import the certificate in Setup → Certificate and
Key Management.** Use a self-signed certificate for sandboxes; in production use a CA-issued certificate if the target system requires it. Note the label — you reference it from
Auth.JWS. - Configure the Connected App on the target side. Upload the
public key (the .crt exported from Setup), enable "Use digital signatures", add the API scope you need, and pre-authorize the user(s) the
subclaim will identify (Profiles → Manage Profiles or Permission Sets). - Write the assertion builder. Use
Auth.JWTfor the claims,Auth.JWSto sign with the certificate label. Setiss(consumer key),sub(username),aud(target login URL exactly —https://login.salesforce.comorhttps://test.salesforce.comfor Salesforce-to-Salesforce),expto now + 3 minutes. - POST the assertion to the token endpoint. Use form-encoded
body with
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearerandassertion=<jwt>. Parse the response —access_tokenon success,{error, error_description}on failure. - Handle the failure responses, not just
invalid_grant.user hasn't approved this consumermeans pre-authorization is missing;invalid_assertionusually means clock skew or wrongaud;invalid_client_idmeans theissdoesn't match a Connected App. - Cache the access token until ~30s before expiry. The token
endpoint is rate-limited (per-org, per-Connected-App). Re-exchange only on expiry or on a 401 from the resource server.
When To Reach For This Skill
Use it when a backend Apex job (Schedulable, Queueable, callout from a trigger) needs to call an external system as a fixed service account, and the target supports JWT bearer. Use it for org-to-org integration where one org calls the other unattended. Use it when storing a password (even encrypted) in Custom Metadata is unacceptable — JWT bearer has no password.
Do not use it for user-context callouts. If the calling user's permissions matter at the destination, use Named Credentials with "Per User" auth and the standard OAuth web-server flow. JWT bearer is impersonation by a service principal; it bypasses the user's interactive login.
What This Skill Does Not Cover
| Topic | See instead |
|---|---|
| Inbound JWT (external system → Salesforce) | security/connected-app-jwt-inbound (Connected App config; no Apex) |
| OAuth 2.0 user-agent / web-server flow | apex/named-credentials-oauth-user-flow |
| JWKS rotation and discovery endpoints | integration/jwt-key-rotation |
| MuleSoft / external orchestration of JWT | integration/mulesoft-jwt-passthrough |