I am trying to put together an incredibly simple OAuth integration with Salesforce, in order to export some Case data for one of my clients. I have set up a Salesforce developer account and my client has given me access to a sandbox for the purposes of testing the integration. I have set up a Connected App within my developer account.

I'm using Node.js with Typescript for the integration using the salesforce-oauth2 library.

I can successfully authorized my Connected App within my client's sandbox account. It seems the OAuth handshake is working just fine. Here are some details of my application:

  • IP restrictions are relaxed
  • Refresh token is set to expire in 99999 months
  • The app is configured with these scopes: "api refresh_token offline_access"
  • My session timeout value is "None"

Once I've made the authorization, I can successfully export the data I want from the sandbox. This is to say that it seems the access_token exchange for resources is working just fine.

I have also configured the tokens to be introspectable and, if I introspect the tokens once I've created them, they also look (mostly*) fine:

  "active": true,
  "scope": "api refresh_token",
  "client_id": "MY_CLIENT_ID",
  "username": "MY_USERNAME",
  "sub": "https://test.salesforce.com/id/XXXXXXXXXXXXX/XXXXXXXXXX",
  "token_type": "refresh_token",
  "iat": 1658159555,
  "nbf": 1658159555

  "active": true,
  "scope": "api refresh_token",
  "client_id": "MY_CLIENT_ID",
  "username": "MY_USERNAME",
  "sub": "https://test.salesforce.com/id/XXXXXXXXXXXXX/XXXXXXXXXXX",
  "token_type": "access_token",
  "exp": 1658200540,
  "iat": 1658157340,
  "nbf": 1658157340
  • I say mostly because the minted tokens do not have all of the scopes I've requested - where is "offline_access"?

My problems come up when I try to refresh the access token. As far as I can tell from documentation there shouldn't be any issue in refreshing this token at any time. Obviously refreshing it before each call would be wasteful but that's not my problem right now.

I'm following the instructions laid out here to the letter.

I create a request like this:

curl -X POST -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'token=$MY_REFRESH_TOKEN' + \
      '&client_id=$MY_CLIENT_ID' + \
      '&client_secret=$MY_CLIENT_SECRET' + \
      '&grant_type=refresh_token' \

Which gives me the following response:

  "error": "invalid_grant",
  "error_description": "expired access/refresh token"

This cannot be correct because in my program I've literally just minted both access and refresh tokens - they are brand new and cannot be expired. This is also proven through token introspection.

I know there is a limit to the number of refresh tokens you can have attached to a user. I have verified that the issue persists even if I revoke all of my tokens.

I have scoured seemingly every article on the internet related to Salesforce refresh token issues. I'm confident (though obviously not certain) that there's something within Salesforce itself that I'm not configuring correctly that's leading to this not working.

Some thoughts I've had:

  • is it related to needing the "full" permission?
  • is it related to the fact that I have a connection into a sandbox account?
  • does a sandbox account have to "unlock" refresh_token permissions even though I've requested them and seemingly have them granted?
  • is there an unknown parameter I'm missing in my request?

If anyone has guidance I'd appreciate it - I find Salesforce's documentation to be extremely lacklustre.

  • 2
    Assuming that you are having trouble with this specifically when trying to work with a sandbox org, this might be as simple as changing from https://login.salesforce.com/... to https://test.salesforce.com/... login = production and developer edition orgs. test = sandbox orgs.
    – Derek F
    Commented Jul 19, 2022 at 11:07
  • Thanks for the suggestion @DerekF but it seems not. The response is the same. My account (where I've set up the Connected App) is a developer edition org. The account I want to grant access to my connected app is a Sandbox org.
    – mattsch
    Commented Jul 19, 2022 at 12:11
  • Connected apps are created in one org but can be used ("installed", though that's slightly different) in any other org. The important part is what type of org you're trying to access in a given transaction (so you do want to be using test.salesforce.com here). Just as a sanity check, the access/refresh token that you get is only valid for the specific org that you logged in to when presented with the login window.
    – Derek F
    Commented Jul 19, 2022 at 12:58
  • Yes, that's expected. It's just standard OAuth, right? Access/refresh tokens are associated with a grant, which only exists between (in this case) a Connected App and an authorizing organization (my client's sandbox). Should the minted token have the "offline_access" scope on the token? I'm not sure why it wouldn't be there if I request it and grant it. I'm wondering if somehow the lack of that scope makes Salesforce thinks my token has expired.
    – mattsch
    Commented Jul 19, 2022 at 15:54
  • The scope is clearly missing, that's definitely an issue. Since this sounds like a batch type of export, the best way to go is to use a A2A grant type. SF doesn't support client creds but there's an equivalent in JWT Bearer. Thought #2: there are OOTB or OSS tools that do this: SFDMU or even out of the box Data Loader
    – identigral
    Commented Jul 19, 2022 at 16:43

1 Answer 1


It's bizarre but the fix for this seems to have been twofold.

First, I removed the refresh_token scope from the authorization request. Instead, I only requested api and offline_access. I read here that refresh_token and offline_access are essentially synonymous so I thought I'd try each without the other.

Secondly, in the library I'm using, I needed to pass https://test.salesforce.com as the "base URL" when performing the refresh token exchange. I understand from the comments on my question that this is likely because I'm integrating with a Sandbox and not a Production environment.

For anyone who happens to be a JS/TS user having the same problem with the salesforce-oauth2 library, here's the code I have that seems to work.

async refreshAccessToken(
  connectionId: number,
  accessToken: SalesforceAccessPayload
): Promise<SalesforceAccessPayload> {
  const refreshTokenParams = {
    base_url: 'https://test.salesforce.com',
    client_id: SALESFORCE_CLIENT_ID,
    client_secret: SALESFORCE_CLIENT_SECRET,
    redirect_uri: OAUTH_CLIENT_CONFIG.redirect_uri,
    refresh_token: accessToken.refresh_token,

  return new Promise((resolve, reject) => {
    oauth2.refresh(refreshTokenParams, async (error: Error, payload: SalesforceAccessPayload) => {
      if (error != null) {
        reject(new Error(`Salesforce refresh_token exchange error: ${error.message}`));
      } else {
        // The refreshed access token response returned from Salesforce doesn't contain the refresh token
        // so we need to manually attach the refresh token to the new access token and then save it.
        const newAccessToken = { ...payload, refresh_token: accessToken.refresh_token };

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .