> ## Documentation Index
> Fetch the complete documentation index at: https://docs.superblocks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Set up OAuth 2.0 Token Exchange for Salesforce

export const Alert = ({type, title, children}) => {
  const getIcon = () => {
    switch (type) {
      case 'info':
        return "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M10 0C4.477 0 0 4.477 0 10s4.477 10 10 10 10-4.477 10-10S15.523 0 10 0zm0 15c-.552 0-1-.448-1-1s.448-1 1-1 1 .448 1 1-.448 1-1 1zm1-3H9V6h2v6z' fill='%230099FF'/%3E%3C/svg%3E";
      case 'success':
        return "data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10 0C4.477 0 0 4.477 0 10s4.477 10 10 10 10-4.477 10-10S15.523 0 10 0zm4.293 6.293L9 11.586 5.707 8.293c-.391-.391-1.024-.391-1.414 0s-.391 1.024 0 1.414l4 4c.391.391 1.024.391 1.414 0l6-6c.391-.391.391-1.024 0-1.414s-1.024-.391-1.414 0z' fill='%230CC26D'/%3E%3C/svg%3E";
      case 'warning':
        return "data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbDpzcGFjZT0ncHJlc2VydmUnIHdpZHRoPScxMDgwJyBoZWlnaHQ9JzEwODAnPjxyZWN0IHdpZHRoPScxMDAlJyBoZWlnaHQ9JzEwMCUnIGZpbGw9J3RyYW5zcGFyZW50Jy8+PHBhdGggZD0nTTEzLjc5NCAxMC43NSA4LjMgMS4yNWExLjUgMS41IDAgMCAwLTIuNiAwbC01LjQ5NCA5LjVBMS40OTQgMS40OTQgMCAwIDAgMS41IDEzaDExYTEuNDkzIDEuNDkzIDAgMCAwIDEuMjk0LTIuMjVNNi41IDUuNWEuNS41IDAgMCAxIDEgMFY4YS41LjUgMCAwIDEtMSAwek03IDExYS43NS43NSAwIDEgMSAwLTEuNS43NS43NSAwIDAgMSAwIDEuNScgc3R5bGU9J3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7ZmlsbDojZmY5ZjM1O2ZpbGwtcnVsZTpub256ZXJvO29wYWNpdHk6MScgdHJhbnNmb3JtPSd0cmFuc2xhdGUoLjAyIDE5LjMwNSlzY2FsZSg3Ny4xNCknLz48L3N2Zz4=";
      case 'danger':
        return "data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 0C4.477 0 0 4.477 0 10s4.477 10 10 10 10-4.477 10-10S15.523 0 10 0zm5.707 4.293L10 9.586 4.293 4.293c-.391-.391-1.024-.391-1.414 0s-.391 1.024 0 1.414L8.586 11l-5.707 5.293c-.391.391-.391 1.024 0 1.414s1.024.391 1.414 0L10 12.414l5.707 5.293c.391.391 1.024.391 1.414 0s.391-1.024 0-1.414L11.414 11l5.707-5.293c.391-.391.391-1.024 0-1.414s-1.024-.391-1.414 0z' fill='%23F45252'/%3E%3C/svg%3E";
      case 'note':
        return "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M10 0C4.477 0 0 4.477 0 10s4.477 10 10 10 10-4.477 10-10S15.523 0 10 0zm0 15c-.552 0-1-.448-1-1s.448-1 1-1 1 .448 1 1-.448 1-1 1zm1-3H9V6h2v6z' fill='%230099FF'/%3E%3C/svg%3E";
      default:
        return "";
    }
  };
  return <div className={`alert alert--${type}`}>
      <div className="alert-icon" style={{
    backgroundImage: `url("${getIcon()}")`,
    backgroundRepeat: 'no-repeat',
    backgroundPosition: 'center center',
    backgroundSize: '20px',
    width: '24px',
    height: '24px',
    position: 'absolute',
    left: '16px',
    top: '16px'
  }} />
      <div className="alert-content">
        {title && <div className="alert-title">{title}</div>}
        <div className="alert-body">{children}</div>
      </div>
    </div>;
};

This guide describes how to configure Salesforce to accept OAuth 2.0 Token Exchange authentication from Superblocks. With this auth method, Superblocks performs a machine-to-machine token exchange to obtain a Salesforce access token on behalf of the current user without requiring the user to log in to Salesforce separately and without storing long-lived Salesforce credentials in Superblocks.

Once configured, every Salesforce API call made through Superblocks will be executed as the specific user who is running the application, allowing Salesforce to enforce its existing record-level visibility rules, field-level security, and sharing settings.

## Prerequisites

To follow this guide, you'll need:

* A Superblocks organization with enterprise SSO configured using an OIDC-based identity provider (for example, [Okta OIDC](/admin/org-administration/auth/single-sign-on/okta#create-an-oidc-integration) or Microsoft Entra ID)
* A Salesforce org (production or sandbox) with API access enabled
* Salesforce **System Administrator** profile or a profile with the following permissions:
  * **Manage External Client Apps**
  * **View Setup and Configuration**
  * **Customize Application**
  * **Author Apex**

<Alert type="warning">
  This flow requires an OIDC-based SSO connection. If your Superblocks SSO uses a SAML-only flow that does not issue an OIDC access token, please contact [support@superblocks.com](mailto:support@superblocks.com) before proceeding.
</Alert>

## Token exchange flow

When using this auth method:

1. Your user logs in to Superblocks using your enterprise SSO identity provider (for example, Okta)
2. When the user runs a Superblocks API that includes a Salesforce step, a request is sent from the browser to the Superblocks Data Plane, including the user's identity provider (IDP) JWT
3. The Superblocks Agent extracts the user's IDP access token and sends a token exchange request to Salesforce's OAuth token endpoint using the `urn:ietf:params:oauth:grant-type:token-exchange` grant type as defined in [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)
4. Salesforce invokes the custom [APEX Token Exchange Handler](#step-2-write-the-apex-token-exchange-handler) you deploy, which validates the IDP token, extracts the user's email, and maps it to a Salesforce user
5. Salesforce returns a scoped access token for that user
6. The Superblocks Data Plane uses the access token to execute the Salesforce API call on behalf of the user

<Alert type="info">
  The IDP access token used as the `subject_token` in the exchange is only available when the user has authenticated via an enterprise SSO connection. It is not available for username/password logins.
</Alert>

## Setup instructions

### Step 1: Create a Salesforce External Client App

An External Client App registers Superblocks as an OAuth client in your Salesforce org and enables the Token Exchange grant type.

1. Log in to Salesforce and navigate to **Setup**

2. In the Quick Find box, search for **External Client Apps Manager**

3. Click **New External Client App**

4. Fill in the **Basic Information** section:
   * **External Client App Name**: `Superblocks`
   * **API Name**: `Superblocks` (auto-populated)
   * **Contact Email**: your administrator email

5. In the **OAuth Settings** section, check **Enable OAuth Settings**

6. Set **Callback URL** to `https://login.salesforce.com/services/oauth2/success`

   <Alert type="info">
     The Callback URL is required by the form but is not used by the Token Exchange flow. Any valid HTTPS URL works.
   </Alert>

7. Under **OAuth Scopes**, add the following scopes:
   * `Manage user data via APIs (api)`
   * `Perform requests on your behalf at any time (refresh_token, offline_access)` — optional, only needed if your use case requires offline access

8. Scroll down to the **OAuth 2.0 Token Exchange Flows** section and check **Enable Token Exchange Flow**

9. Click **Save**, then click **Continue**

After saving, Salesforce takes a few minutes to provision the app. Once available:

1. From External Client App Manager, click the arrow next to your app and select **Edit Settings**
2. Click the **Settings** tab, scroll down and open the **OAuth Settings** dropwdown
3. Click on **Consumer Key and Secret** (you may need to verify via email)
4. Copy and save the **Consumer Key** (Client ID) and **Consumer Secret** (Client Secret) — you will need these when configuring the Superblocks integration
5. Go back to External Client App Manager, click the arrow next to your app and select **Edit Policies**
6. Under **OAuth Policies** → **Permitted Users**, select **Admin approved users are pre-authorized**

<Alert type="warning">
  You must assign profiles to the app after creation. If you do not, token exchange requests will fail with "user hasn't approved this consumer".
</Alert>

### Step 2: Write the APEX Token Exchange Handler

Salesforce requires a custom APEX class that extends Auth.Oauth2TokenExchangeHandler. This handler is responsible for validating the incoming JWT and mapping it to a Salesforce user. When Salesforce receives a token exchange request, it invokes this handler, which is responsible for validating the incoming IDP access token and mapping the incoming identity provider token to the Salesforce user for whom Salesforce should issue the exchanged access token.

The handler below decodes the IDP access token (a JWT), extracts the `email` claim, and looks up the matching active Salesforce user.

#### Deploy the handler class

1. In Salesforce Setup, search for **Developer Console** and open it (or use the Salesforce CLI / VS Code Salesforce Extension)
2. Go to **File** → **New** → **Apex Class**
3. Name the class `SuperblocksTokenExchangeHandler`
4. Replace the default content with the following:

```apex theme={null}
global class SuperblocksTokenExchangeHandler extends Auth.Oauth2TokenExchangeHandler {

    global override Auth.TokenValidationResult validateIncomingToken(
        String appDeveloperName,
        Auth.IntegratingAppType appType,
        String incomingToken,
        Auth.OAuth2TokenExchangeType tokenType
    ) {
        try {
            List<String> jwtParts = incomingToken.split('\\.');
            if (jwtParts.size() < 2) {
                return new Auth.TokenValidationResult(false);
            }

            String payload = jwtParts[1];
            Integer remainder = Math.mod(payload.length(), 4);
            if (remainder > 0) {
                payload = payload + '===='.substring(0, 4 - remainder);
            }

            payload = payload.replace('-', '+').replace('_', '/');

            String payloadJson = EncodingUtil.base64Decode(payload).toString();
            Map<String, Object> claims =
                (Map<String, Object>) JSON.deserializeUntyped(payloadJson);

            String identityValue = (String) claims.get('sub');
            if (String.isBlank(identityValue)) {
                identityValue = (String) claims.get('email');
            }
            if (String.isBlank(identityValue)) {
                identityValue = (String) claims.get('upn');
            }

            if (String.isBlank(identityValue)) {
                return new Auth.TokenValidationResult(false);
            }

            Auth.UserData userData = new Auth.UserData(
                (String) claims.get('sub'),
                null,
                null,
                null,
                identityValue,
                null,
                identityValue,
                null,
                null,
                null,
                null
            );

            return new Auth.TokenValidationResult(
                true,
                null,
                userData,
                incomingToken,
                tokenType,
                null
            );

        } catch (Exception e) {
            return new Auth.TokenValidationResult(false);
        }
    }

    global override User getUserForTokenSubject(
        Id networkId,
        Auth.TokenValidationResult result,
        Boolean canCreateUser,
        String appDeveloperName,
        Auth.IntegratingAppType appType
    ) {
        String identityValue = result.getUserData().email;

        List<User> users = [
            SELECT Id, Email, Username
            FROM User
            WHERE IsActive = true
            AND (Email = :identityValue OR Username = :identityValue)
            LIMIT 1
        ];

        if (!users.isEmpty()) {
            return users[0];
        }

        return null;
    }
}
```

5. Click **Save**

<Alert type="info">
  **Customizing the email claim**: The claim name used to look up the user (`sub` in the example above) must match the claim included in your IDP's access tokens. Common alternatives are `email`, `preferred_username`, or `upn`. Check your IDP's access token format to confirm the correct claim name.
</Alert>

<Alert type="warning">
  **Test the handler in a sandbox first.** The handler runs with the permissions of the System Administrator who owns the External Client App. Verify that user lookup works correctly before deploying to production.
</Alert>

#### Optional: validate the token signature

The example above decodes the JWT payload without verifying the signature. For production environments, you may want to add signature validation using your IDP's public JWKS endpoint. Salesforce provides the `Auth.JWTUtil` class for this purpose, or you can make a callout to your IDP's `jwks_uri` endpoint to retrieve the public key.

If you implement signature validation, you will need to add your IDP's JWKS endpoint to the Salesforce **Remote Site Settings** (Setup → Remote Site Settings → New Remote Site).

### Step 3: Create and Configure the Token Exchange Handler

Salesforce requires a Token Exchange Handler record to link your Apex class to your External Client App.

1. In Salesforce Setup, search for **Token Exchange Handlers**

2. Click **New**

3. Configure the following:
   * **Name**: Superblocks Token Exchange Handler
   * **Apex Class**: SuperblocksTokenExchangeHandler
   * **Supported Token Types**: JWT

4. Save

### Link the handler to your app

1. Open the Token Exchange Handler you just created

2. Click **Enable New App**

3. Select your External Client App

4. Configure:

   * **Run As User**: Select a valid Salesforce user that the handler should run as. This user provides the execution context for the handler itself and must have sufficient permissions to perform user lookup and validation logic. The Salesforce user for whom the access token is issued is still determined dynamically by the handler based on the incoming token.

5. Save

<Alert type="warning">
  If the handler is not linked to the app, Salesforce will return:
  "token handler not found"
</Alert>

### Step 4: Configure the Superblocks integration

Once your Salesforce External Client App and APEX handler are in place and the Token Exchange Handler is linked, configure the Salesforce integration in Superblocks.

1. In Superblocks, navigate to the **Integrations** page

2. Search for **Salesforce** and click **New Integration** (or edit an existing one)

3. In the **Authentication** dropdown, select **OAuth 2.0 Token Exchange**

4. Set **Subject token source** to **Login Identity Provider**

5. Fill in the remaining fields using the values from your External Client App:

   | <div style={{width: 160}}>Field</div> | Required                                   | Description                                                                                                                                         |
   | ------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
   | **Token URL**                         | <span className="table-checkmark">✓</span> | `https://login.salesforce.com/services/oauth2/token` for production orgs. Use `https://test.salesforce.com/services/oauth2/token` for sandbox orgs. |
   | **Client ID**                         | <span className="table-checkmark">✓</span> | The **Consumer Key** from your External Client App                                                                                                  |
   | **Client secret**                     | <span className="table-checkmark">✓</span> | The **Consumer Secret** from your External Client App                                                                                               |
   | **Scope**                             | <span className="table-x">✗</span>         | `api` — required to make Salesforce REST API calls. Add additional scopes separated by spaces as needed.                                            |

6. Click **Test Connection** to verify the configuration, then click **Save**

<Alert type="info">
  The **Subject token type** field defaults to `urn:ietf:params:oauth:token-type:access_token`, which is the correct value for IDP access tokens. You do not need to change this.
</Alert>

## Testing & troubleshooting

### Test the integration

With the integration configured, verify that it works end-to-end:

1. In a Superblocks application, create a new **Backend API**
2. Add a step that uses your Salesforce integration
3. Add a SOQL query step, for example:
   ```sql theme={null}
   SELECT Id, Name, Username FROM User WHERE Id = :$userId LIMIT 1
   ```
   Where `$userId` resolves to the current Salesforce user
4. Click **Run API**

If everything is working, the step should return data for the Salesforce user that corresponds to the logged-in Superblocks user.

### Common errors

| <div style={{width: 260}}>Error message</div>  | Why it's happening                                                                                                              | Resolution                                                                                                                                                                                                                                                |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Could not find identity provider token`       | The user is not logged in via an enterprise SSO connection, or the SSO connection is not configured to issue OIDC access tokens | Confirm that your Superblocks SSO uses an OIDC-based flow. Contact [support@superblocks.com](mailto:support@superblocks.com) if you need assistance.                                                                                                      |
| `Identity provider token expired`              | The user's IDP session has expired                                                                                              | Ask the user to refresh the browser or log out and back in to obtain a fresh IDP token                                                                                                                                                                    |
| `Could not find a user JWT`                    | The integration is being used in a Workflow, Scheduled Job, or public App where there is no logged-in user context              | Token Exchange requires an active user session. Use this integration type in Backend APIs only, not in Workflows or Scheduled Jobs.                                                                                                                       |
| `Token exchange failed`                        | Superblocks received an error from the Salesforce token endpoint                                                                | Check the error details returned by Salesforce. Common causes: the External Client App is not yet provisioned (wait a few minutes after creation), the Token Exchange Flow is not enabled on the External Client App, or the handler class is not linked. |
| `MISSING_EMAIL_CLAIM` (from your APEX handler) | The IDP access token does not include an `email` claim                                                                          | Update the `SuperblocksTokenExchangeHandler` to use the claim name your IDP includes. Check the IDP access token using [jwt.io](https://jwt.io) to confirm the correct claim.                                                                             |
| `USER_NOT_FOUND` (from your APEX handler)      | No active Salesforce user was found with the email from the IDP token                                                           | Verify that the user exists in Salesforce with an exactly matching email address (case-insensitive) and that their account is active.                                                                                                                     |
