JWT-Based Access Tokens in Salesforce: A Complete Guide
Learn how Salesforce’s new JWT-Based Access Tokens differ from traditional opaque OAuth tokens, how they align with RFC 7519 and RFC 9068, what claims they contain, and what the changes mean for developers building secure integrations.
Anyone who has worked with the Salesforce Hosted MCP Servers or the Agentforce API will have set up an External Client App. During the setup process there is a small but significant step that tells you to enable the “Issue JSON Web Token (JWT)-based access tokens for named users” setting. In this article we’re going to deep dive into what this actually means.
The basics of OAuth 2.0 are simple: There is a client-server type setup where the client runs an authorisation / authentication flow to request access to a resource server. The flow can be system to system or can have a human in the loop, but ultimately when the client is successfully authenticated and authorised, the client receives an access token that is the required key for accessing protected resources. This value should be kept super secret because if you don’t, you might get on a list of “Salesforce OAuth Hacks” that are not actually Salesforce hacks but compromised client systems….
In order to reach protected resources, the access token (often referred to as the OAuth token or Bearer token) is added into the request’s authorization header like Authorization : Bearer [TOKEN] and when valid, access is granted.
Standard Salesforce access tokens are a “Session Id” that looks something like 00Dxx0000000000!AQEAQF… They start with the Org Id and then some random characters. It may hold meaning on the Salesforce end, but as far as any connecting client is concerned, it is just random string. It’s an opaque access token that needs to be stored, validated by Salesforce and destroyed when invalid. They have no meaning to the client whatsoever. JWT-Based access tokens change that.
A JSON Web Token (JWT) is an open standard for transmitting self-contained information as a JSON object. The information is digitally signed, allowing the recipient to verify who issued the token and that its contents has not been modified.
A JWT consists of a header, a payload and a signature. The header and payload are Base64URL encoded rather than encrypted, meaning anyone can decode and inspect their contents. The payload can contain information that is useful for implementing clients.
To improve interoperability, the IETF published RFC 9068, which defines a standard JWT Profile for OAuth 2.0 Access Tokens. Salesforce issues compliant JWT-Based Access Tokens from External Client Apps and Connected Apps when — Drum Roll — You check the “Issue JSON Web Token (JWT)-based access tokens for named users” option in the OAuth settings.
When you decode the content of a Salesforce issued JWT based token, it will look like something below. I have split out the header and the payload and added some information on the less obvious parameters.
Decoding is NOT the same as trust. ANYONE can decode the payload, but the signature validates that the token was issued by Salesforce and the payload has not been tampered with.
{
"tnk": "core/prod/00D000000000000",
"ver": "1.0",
"kid": "CORE_ATJWT.00D000000000000.1780376508115",
"tty": "sfdc-core-token",
"typ": "JWT",
"alg": "RS256"
}Standard Header Claims
kid: The key Identifier, used to point to the matching public key
Salesforce Custom Header Claims
- tnk: Tenant Key used as the org identifier
- ver: Version of the token format
- tty: Token Type
{
"scp": "mcp_api refresh_token",
"sub": "uid:005000000000000",
"roles": [],
"iss": "https://jwt-demo.my.salesforce.com",
"client_id": "3MVG95nWQGdmAiEoiOggybZ45Q...",
"aud": [
"https://api.salesforce.com",
"https://jwt-demo.my.salesforce.com"
],
"nbf": 1783346419,
"mty": "oauth",
"sfi": "0c94ba2c7ebec4e2ab883236797cc60...",
"sfap_op": "EinsteinHawkingC2CEnabled...",
"hsc": false,
"exp": 1783348234,
"iat": 1783346434
}Standard Payload Claims
- scp : Scopes issued
- sub: (User) Id
- iss: The Org Domain Url
- client_id: The Consumer Key
- aud: The APIs / Services allowed to use this token
Salesforce Custom Payload Claims
- mty: Mechanism Type
- sfi: Salesforce Identifier (To link back to Event Monitoring)
- sfap_op: Allowed Salesforce API Platform Operations
- hsc: High Security Session
How to validate a Salesforce JWT Signature
To validate the signature we need the public key that matches the ECA. Salesforce exposes a standard OpenID Connect Discovery endpoint for each org. This endpoint describes the OAuth and OpenID Connect configuration for your specific org and is one of the most useful URLs to have bookmarked when working with OAuth:
https://[MYDOMAIN].my.salesforce.com/.well-known/openid-configuration
/.well-known/openid-configuration endpoint highlighting the jwks_uri parameter.The response contains a jwks_uri property. Opening this URL returns the JSON Web Key Set (JWKS) for your org, containing the public keys that can be used to validate JWT signatures. The url usually looks like:
https://[MYDOMAIN].my.salesforce.com/id/keys
/id/keys endpoint highlighting the public key that matches the JWT's kid claimEvery JWT header contains a kid (Key ID) claim, identifying the signing key. Your application can use this value to select the matching key from the JWKS endpoint. Since these keys change infrequently you can cache them to prevent unnecessary callouts as an optimisation.
Note: jwt.io automatic checks for the public key based on these standard endpoints based on the value in the iss parameter. So if you copy your token (!! Be careful as it is a super secret !!) it will show as a valid signature. A great example of standardisation advantages right here. In your own implementation you have to built the validation logic yourself though.
Token Comparison
Now we know what the tokens look like, what they contain, how to validate the values so let’s compare the two token types side by side on more time:
+------------------------+--------------------+---------------------------+
| Category | Opaque Token | JWT-Based Access Token |
+------------------------+--------------------+---------------------------+
| Format | Random identifier | Signed JWT |
| Structure | None | Header.Payload.Signature |
| Readable by developers | No | Yes |
| Standard | Proprietary | RFC 7519 / RFC 9068 |
| Claims | Stored server-side | Embedded in token |
| Expiration visible | No | Yes (exp) |
| Issued time visible | No | Yes (iat) |
| Audience visible | No | Yes (aud) |
| Scope visible | No | Yes (scp/scope) |
| Signature | Hidden | Included |
| Signature verification | Salesforce only | Anyone with public key |
| Database lookup | Required | Not always required |
| Sliding expiration | Supported | Not supported |
| Refresh token flow | Supported | Supported |
| Revocation | Supported | Supported |
| Token size | Small | Larger |
| Client inspection | No | Yes |
| Typical use | Stateful sessions | Modern APIs |
| Agent API | Not Supported | Supported |
| Hosted MCP Servers | Not Supported | Supported |
| REST APIs | Supported | Supported |
| SOAP APIs | Supported | Apex WSDL Support Only |
| Connect / Tooling API | Supported | Supported |
| Native Data Cloud API | Supported | Supported |
| Frontdoor UI Bridge | Supported | Supported |
+------------------------+--------------------+---------------------------+Token Expiration
One of the most important differences is how the token expiration works. Opaque tokens work on a sliding scale: This means the token expiration is moved forward by the defined time out value each time an API is hit. As long as the user (or integration) continues to make requests within the configured timeout period, the access token remains valid. This is very similar behaviour to how the Salesforce UI works.
Get Justus van den Berg’s stories in your inbox
Join Medium for free to get updates from this writer.
Only when no activity occurs for the full timeout period does the session expire. This is one of the reasons Salesforce OAuth token responses don’t include the expires_in parameter. Since the expiration continually moves forward, any value returned during authentication would almost immediately become invalid.
JWT-Based Access Tokens behave differently. When Salesforce issues a JWT-Based Access Token, the expiration is embedded directly into the token using the standard exp claim. Unlike a session-based token, this value never changes. No matter how active the client is, the expiration timestamp remains fixed.
Once the token reaches the time specified in the exp claim, it is no longer valid and a new access token must be obtained, typically using the OAuth Refresh Token flow or by simply requesting a new token altogether depending on the type of OAuth flow.
In External Client Apps, the expiration policy can be configured in one of two ways:
- Based on the User’s Profile — The session timeout is inherited from the user’s Profile (or the org default if no profile-specific timeout is configured).
- Based on the External Client App — The timeout configured on the External Client App is used and Profile session timeout settings are ignored.
Although the exp claim provides a reliable indication of the token's intended lifetime, it does not guarantee the token will remain valid until that moment. For example, the token could become invalid because:
- The user is deactivated;
- The session is revoked;
- The ECA is disabled;
- An administrator terminates the session;
- Other Salesforce security policies/processes invalidate the session.
For this reason, OAuth clients should continue to treat a 401 Unauthorized response as the authoritative indication that a token is no longer valid. The exp claim is best viewed as an optimisation that allows clients to proactively refresh a token before it expires, rather than waiting for a failed API request.
Supported APIs and API Error Responses
JWT-Based Access Tokens work seamlessly with the standard Salesforce REST APIs. Keep in mind that there is no sliding window for the expiration time like with the opaque tokens: The token expires at the set time, so take this into consideration in your implementation. This also shows that the token process itself is decoupled from the APIs / MCP servers.
There also is a catch with regards to error handling: When the token expires and you call a REST API, you will not get invalid_token as the message value. The message is INVALID_JWT_FORMAT or INVALID_AUTH_HEADER with the errorCode always being INVALID_AUTH_HEADER. The HTTP Status code is still 401 . You’ll need to update the message value and/or errorCode values accordingly if you check against invalid_token .
[
{
"message" : "INVALID_JWT_FORMAT", // OR "INVALID_AUTH_HEADER",
"errorCode": "INVALID_AUTH_HEADER"
}
]JWT Token support for the SOAP API is interesting to say the least… The Apex WSDL works perfectly fine. The Metadata WSDL (even with valid token) responds with INVALID_SESSION_ID and the Partner WSDL responds with UNKNOWN_EXCEPTION. So I think it is safe to say the SOAP API is not properly supported.
When it comes to expired JWT tokens, the SOAP API behaves differently. Rather than returning an OAuth-style 401 Unauthorized response where the faultCode element contains INVALID_SESSION_ID, it responds with an HTTP 500 Internal Server Error containing an "Unexpected Error" message.
An unexpected error occurred. Please include this ErrorId if you contact support: 992687996–41233 (1513584111)
During my testing, the internal error code (1513584111) remained consistent whenever an expired JWT-Based Access Token was used. Although this strongly suggests it corresponds to this failure scenario, Salesforce does not document the meaning of this internal error code. The error only occurs for the partner and Apex WSDLs, the Metadata WSDL remains on INVALID_SESSION_ID. Now you know…
Generate a Frontdoor URL to Bridge into UI Sessions
JWT-Based Access Tokens can also be used with the Frontdoor endpoint, allowing an authenticated API session to transition into a browser-based Salesforce UI session. This is particularly useful for headless registration flows or integrations that need to authenticate through the API before redirecting the user into Salesforce.
Read all about it here: Generate a Frontdoor URL to Bridge into UI Sessions
Honorable mention: Native Data 360 (Data Cloud) API Access Tokens
The Native Data 360 (FKA Data Cloud) REST API uses it’s own token type. To fetch this type of token you take a standard Core Salesforce OAuth token and “convert” this into a Data 360 specific access token. JWT-Based Access tokens are supported for this operation and can be converted to DC specific access tokens.
The DC Api uses it’s own JWT Access Token but the details are outside the scope of this article. IMHO this is less important as the majority can be done using the Connect REST API.
Conclusion
JWT-Based Access Tokens are much more than a new way of formatting an OAuth access token. By adopting the industry-standard JWT Profile for OAuth 2.0 Access Tokens (RFC 9068), Salesforce gains all the benefits of a self-contained, digitally signed token while improving interoperability with modern identity platforms and OAuth tooling.
For developers, this introduces several practical advantages. Access tokens now contain useful information such as their expiration time, issuer, audience and granted scopes. Standard JWT libraries can validate signatures using Salesforce’s published JSON Web Key Sets (JWKS), making token validation considerably easier than with proprietary formats. The fixed expiration time also allows clients to proactively refresh tokens before they expire.
Another benefit is the simplified routing model. Rather than requiring clients to know where an organisation is hosted, Salesforce can route requests from the global https://api.salesforce.com endpoint to the correct instance based on information contained within the access token. This gives Salesforce greater flexibility to evolve its infrastructure, such as moving organisations between Hyperforce regions, without requiring customers to change their integrations.
From an implementation perspective there are a few changes to be aware of:
- JWT-Based Access Tokens have a fixed expiration rather than Salesforce’s traditional sliding session timeout.
- Clients should continue to treat HTTP 401 Unauthorized as the authoritative indication that a token is no longer valid, even though the
expclaim allows proactive refreshes. - Signature validation now follows industry standards using OpenID Connect Discovery and JWKS.
- Existing integrations may need to update their error handling to account for JWT-specific responses from some Salesforce APIs.
Overall, JWT-Based Access Tokens represent a significant step towards modern OAuth standards. They make Salesforce integrations more interoperable, easier to validate, and better aligned with the wider identity ecosystem while introducing only a small number of implementation changes for existing applications.
Final Note
At the time of writing I am a Salesforce employee. The above article describes my personal views and techniques only. The content is in no way, shape or form official advice of any kind. It’s purely meant to be informative.
Anything from the article is not per definition the view of Salesforce as an Organization. Always speak to a certified implementation partner before implementing anything your read on the internet, especially when it’s security related and/or consumption based.








