Skip to content

Comments

fix: Salesforce tokens add token_lifetime#27264

Merged
emrysal merged 6 commits intomainfrom
salesforce-get-token-expiry
Jan 27, 2026
Merged

fix: Salesforce tokens add token_lifetime#27264
emrysal merged 6 commits intomainfrom
salesforce-get-token-expiry

Conversation

@joeauyeung
Copy link
Contributor

@joeauyeung joeauyeung commented Jan 26, 2026

What does this PR do?

Currently, Salesforce OAuth tokens do not include an expiry date. This means we refresh Salesforce tokens on every request, which is inefficient and can cause rate limiting issues.

This PR adds token lifetime tracking by:

  1. Calling Salesforce's token introspection endpoint during OAuth connect to get the token lifetime
  2. Storing token_lifetime in the credential alongside the access token
  3. Checking if the token is still valid before making requests (using issued_at + token_lifetime with a 5-minute buffer)
  4. Only refreshing when the token is actually expired
  5. Adding a refreshFn to the jsforce connection that handles unexpected expiry by re-introspecting to recalibrate the lifetime

Visual Demo (For contributors especially)

N/A - Backend-only change with no UI impact.

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A - no docs changes needed.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  1. Connect a Salesforce account via the integrations page
  2. Verify the credential is stored with a token_lifetime value
  3. Make API calls that use Salesforce (e.g., CRM lookups during booking)
  4. Verify tokens are not refreshed on every request (check logs or network traffic)
  5. Wait for token to expire (or manually set issued_at to an old value) and verify refresh works correctly

Human Review Checklist

  • Verify time unit handling: issued_at is in milliseconds (string), token_lifetime is in seconds
  • Verify the introspection endpoint URL format (${instanceUrl}/services/oauth2/introspect) is correct
  • Check that hasAttemptedRefresh flag doesn't cause issues if the service instance is reused across multiple requests
  • Verify CredentialRepository.updateWhereId exists and works as expected

Checklist

  • I have read the contributing guide
  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have checked if my changes generate no new warnings

Updates since last revision

Added comprehensive test coverage:

  • getSalesforceTokenLifetime.test.ts: Unit tests for the new introspection function (5 tests covering lifetime calculation, endpoint parameters, Basic auth, error handling, and different lifetimes)
  • CrmService.integration.test.ts: Added "Token lifecycle management" test suite (4 tests covering valid token skip, expired token refresh, legacy credentials without token_lifetime, and 5-minute buffer behavior)
  • Fixed type error in CrmService.ts by extracting refreshToken variable after the null check to ensure type safety in the refreshFn callback

Link to Devin run: https://app.devin.ai/sessions/649172013c17485889563f15ca2c83e6
Requested by: joe@cal.com (@joeauyeung)

- Add unit tests for getSalesforceTokenLifetime function
- Add integration tests for token lifecycle management in CrmService
- Fix type error in CrmService.ts by extracting refreshToken variable

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
* @param forceIntrospection - If true, always introspect to recalibrate token_lifetime (e.g., after unexpected expiry)
* @param existingTokenLifetime - The current token_lifetime to reuse if not forcing introspection
*/
private refreshAccessToken = async ({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract the call to refresh the access token


// Introspect if forced or if we don't have a token_lifetime yet
let tokenLifetime = existingTokenLifetime;
if (forceIntrospection || !tokenLifetime) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll query the introspection endpoint if there is no token_lifetime property on the credential or if we get a token error if the credential was marked as not expired but was. This is to handle the unlikely case that an org changes their token life spans.

};
};

private getClient = async (credential: CredentialPayload) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same method as before to get the jsforce client


// Check if token is still valid
// issued_at is in milliseconds (string), token_lifetime is in seconds
const BUFFER_MS = 5 * 60 * 1000; // 5 minutes buffer before expiry
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added new check to see if the token is still valid to avoid making a token call on every request. All access tokens are still valid until they are expired. Issuing a new token does not invalidate any existing tokens.

instanceUrl: credentialKey.instance_url,
accessToken: credentialKey.access_token,
refreshToken: credentialKey.refresh_token,
refreshFn: async (conn, callback) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New addition to the jsforce.connection if we get an token error then retry to refetch the token only once

this.hasAttemptedRefresh = true;

try {
// Force introspection to recalibrate token_lifetime after unexpected expiry
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we handle this case. It will be very unlikely.

}): Promise<number> {
const { consumer_key, consumer_secret } = await getSalesforceAppKeys();

const response = await fetch(`${instanceUrl}/services/oauth2/introspect`, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to hit this introspection endpoint to calculate if a token is still valid after it was issued.

@joeauyeung joeauyeung marked this pull request as ready for review January 27, 2026 03:42
@graphite-app graphite-app bot added the enterprise area: enterprise, audit log, organisation, SAML, SSO label Jan 27, 2026
@graphite-app graphite-app bot requested a review from a team January 27, 2026 03:42
@graphite-app graphite-app bot added the core area: core, team members only label Jan 27, 2026
@joeauyeung joeauyeung added ready-for-e2e and removed enterprise area: enterprise, audit log, organisation, SAML, SSO labels Jan 27, 2026
@github-actions
Copy link
Contributor

E2E results are ready!

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/app-store/salesforce/lib/CrmService.ts">

<violation number="1" location="packages/app-store/salesforce/lib/CrmService.ts:262">
P1: `hasAttemptedRefresh` is set the first time `refreshFn` runs and never cleared, so every later token expiry immediately fails with “Token refresh already attempted.” Reset the flag after the refresh attempt so the connection can refresh tokens again while still preventing concurrent refresh loops.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@github-actions
Copy link
Contributor

Devin AI is addressing Cubic AI's review feedback

A Devin session has been created to address the issues identified by Cubic AI.

View Devin Session

@devin-ai-integration
Copy link
Contributor

I reviewed the Cubic AI feedback regarding the hasAttemptedRefresh flag issue. The confidence score for this issue is 8/10, which is below the 9/10 threshold required for automated fixes.

Issue summary: The hasAttemptedRefresh flag is set when refreshFn runs but never cleared, which could cause subsequent token refresh attempts to fail with "Token refresh already attempted."

Why this is being skipped: Per the review guidelines, only issues with confidence scores of 9/10 or higher should be automatically fixed. This issue has a confidence score of 8/10.

Recommendation: A human reviewer should evaluate whether this is a valid concern. If the SalesforceCRMService instance is reused across multiple requests where tokens expire at different times, the flag would need to be reset after a successful refresh to allow future refreshes. However, if a new service instance is created for each request, this may not be an issue in practice.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
Copy link
Contributor

@emrysal emrysal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, happy to merge & release.

@emrysal emrysal merged commit 01ae6f1 into main Jan 27, 2026
51 checks passed
@emrysal emrysal deleted the salesforce-get-token-expiry branch January 27, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core area: core, team members only ready-for-e2e size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants