Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(integrations): add support for salesforce cdp #3363

Merged
merged 13 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs-v2/integrations/all/salesforce-cdp.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: Salesforce (Data Cloud)
sidebarTitle: Salesforce (Data Cloud)
provider: salesforce-cdp
---

import Overview from "/snippets/overview.mdx";
import PreBuiltTooling from "/snippets/generated/salesforce-cdp/PreBuiltTooling.mdx";

import PreBuiltUseCases from "/snippets/generated/salesforce-cdp/PreBuiltUseCases.mdx"

<Overview />
<PreBuiltTooling />
<PreBuiltUseCases />

## Access requirements
| Pre-Requisites | Status | Comment|
| - | - | - |
| Paid dev account | ❓ | |
| Paid test account | ✅ | Requires an active Salesforce Data Cloud subscription. |
| Partnership | ❓ | |
| App review | ❓ | |
| Security audit | ❓ | |


## Setup guide

_No setup guide yet._

<Tip>Need help getting started? Get help in the [community](https://nango.dev/slack).</Tip>

<Note>Contribute improvements to the setup guide by [editing this page](https://github.com/nangohq/nango/tree/master/docs-v2/integrations/all/salesforce.mdx)</Note>


## Useful links
- [Salesforce Data Cloud OAuth documentation](https://developer.salesforce.com/docs/marketing/marketing-cloud-growth/guide/mc-connect-apis-data-cloud.html)
- [Web API docs (their REST API)](https://developer.salesforce.com/docs/marketing/marketing-cloud-growth/references/mc-summary/mc-summary.html)

<Note>Contribute useful links by [editing this page](https://github.com/nangohq/nango/tree/master/docs-v2/integrations/all/salesforce-cdp.mdx)</Note>

## Connection Configuration in Nango

- Salesforce (Data Cloud) uses a different API base URL, called the `instance_url`, for each customer.

- Nango automatically retrieves the `instance_url` (e.g. `https://yourInstance.salesforce.com/`) from Salesforce and stores it in the [Connection config](/guides/api-authorization/authorize-in-your-app-default-ui#apis-requiring-connection-specific-configuration-for-authorization) for you.

- If you use the Nango Proxy, it is automatically using the correct API base URL. But, if needed, you can retrieve the `instance_url` with the [backend SDK](/reference/sdks/node#get-a-connection-with-credentials) or [Connections API](/reference/api/connection/get).

## API gotchas

<Note>Contribute API gotchas by [editing this page](https://github.com/nangohq/nango/tree/master/docs-v2/integrations/all/salesforce-cdp.mdx)</Note>

## Going further

<Card title="Connect to Salesforce (Data Cloud)" icon="link" href="/integrations/all/salesforce-cdp/connect" horizontal>
Guide to connect to Salesforce (Data Cloud) using Connect UI
</Card>

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
118 changes: 118 additions & 0 deletions docs-v2/integrations/all/salesforce-cdp/connect.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: Salesforce (Data Cloud) - How do I link my account?
sidebarTitle: Salesforce (Data Cloud)
---

# Overview

To authenticate with Salesforce (Data Cloud), you need:
1. **Encoded JWT** - A unique string that enables client applications to access Salesforce (Data Cloud) resources without requiring users to provide their credentials.

This guide will walk you through generating your encoded JWT within Salesforce.

### Prerequisites:

- You must have a Salesforce account with an active Data Cloud subscription.

### Instructions:

#### Step 1: Generating your Encoded JWT
- Interacting with the Data Cloud API requires a signed digital certificate. You can use a private key and certificate issued by a certification authority. Alternatively, you can use OpenSSL to create a key and a self-signed digital certificate. Here's how to create a self-signed certificate with OpenSSL.
1. Run the following command to generate a 2048-bit RSA private key:

```
openssl genrsa 2048 > host.key && chmod 400 host.key
```
2. Use the private key to sign a certificate. Enter details about the certificate, or press Enter at each prompt to accept the default value:
```
openssl req -new -x509 -nodes -sha256 -days 365 -key host.key -out host.crt
```
- Now that you have created the above signed certificate, you will now need to create an app in Salesforce and upload the above signed certificate.
3. Login to your salesforce account and In the Setup, in the Quick Find box, enter apps, and then select **App Manager**.
<img src="/integrations/all/salesforce-cdp/app_manager.png" />
4. Click **New Connected App**.
5. For Connected App Name, enter an app name and your email address.
6. Select **Enable OAuth Settings**.
7. For **Callback URL**, enter `http://localhost:1717/OauthRedirect`.
8. Select Use **digital signatures**, and then click **Browse**.
9. Select your above self-signed certificate **(host.crt)**.
10. Add the OAuth scopes that are necessary for your use case. For example, if your use case requires you to ingest content, add the **Manage Data Cloud Ingestion API data (cdp_ingest_api)** scope.
11. Click **Save**.
<img src="/integrations/all/salesforce-cdp/creating_app.png" />
12. Click **Manage Consumer Details**.
<img src="/integrations/all/salesforce-cdp/consumer_keys.png" />
13. Copy the **Consumer Key** value. This value is also referred to as the **client ID**. You will use the client ID value when you are generaying your encoded (JWT) in the step below.
8. Now that your certificate is registered with Salesforce, you need to generate an encoded JWT. You can use the code from the following script to generate your encoded JWT offline.
```
import { readFileSync } from 'fs';
import jwt from 'jsonwebtoken';
import readlineSync from 'readline-sync';

const getUserInput = () => {
const clientId = readlineSync.question('Please enter your Salesforce client ID: ');
const username = readlineSync.question('Please enter your Salesforce username: ');
return { clientId, username };
};

const readPrivateKey = (path) => {
try {
return readFileSync(path, 'utf8');
} catch (error) {
console.error(`Error reading private key from ${path}:`, error);
throw error;
}
};

const createJwtClaims = (clientId, username) => {
const currentTime = Math.floor(Date.now() / 1000);
const expiry = currentTime + (10 * 365 * 24 * 60 * 60);

return {
iss: clientId,
sub: username,
aud: 'https://#.salesforce.com',
exp: expiry,
};
};

const generateJwtToken = (claims, privateKey) => {
try {
return jwt.sign(claims, privateKey, { algorithm: 'RS256' })
} catch (error) {
console.error('Error signing assertion:', error);
throw error;
}
};

const generateJwtAssertion = () => {
const { clientId, username } = getUserInput();
const privateKeyPath = 'host.key';

const privateKey = readPrivateKey(privateKeyPath);

const claims = createJwtClaims(clientId, username);

const token = generateJwtToken(claims, privateKey);

console.log('Generated Salesforce assertion:', token);
};

generateJwtAssertion();


```
- Run the script above in the same directory where your certificates were generated. It will prompt you for your **Client ID** obtained when creating a connected app and your **Username**, used when signing in to Salesforce. An encoded JWT will then be generated.

**Note**: The generated **Encoded JWT** is valid for ten years. After this period, you will need to regenerate your encoded JWT and reauthenticate.

#### Step 2: Enter credentials in the Connect UI

Once you have your **Encoded JWT**:
1. Open the form where you need to authenticate with Salesforce (Data Cloud).
2. Enter your **Encoded JWT** in the designated field.
3. Submit the form, and you should be successfully authenticated.

<img src="/integrations/all/salesforce-cdp/form.png" style={{maxWidth: "450px" }}/>

You are now connected to Salesforce (Data Cloud).

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs-v2/integrations/all/salesforce-cdp/form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs-v2/mint.json
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@
"integrations/all/sage-intacct",
"integrations/all/salesforce",
"integrations/all/salesforce-experience-cloud",
"integrations/all/salesforce-cdp",
"integrations/all/salesforce-sandbox",
"integrations/all/salesloft",
"integrations/all/sap-concur",
Expand Down
40 changes: 40 additions & 0 deletions docs-v2/snippets/generated/salesforce-cdp/PreBuiltTooling.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Pre-built tooling
<AccordionGroup>
<Accordion title="✅ Authorization">
| Tools | Status |
| - | - |
| Pre-built authorization (Two Step) | ✅ |
| Pre-built authorization UI | ✅ |
| Custom authorization UI | ✅ |
| End-user authorization guide | ✅ |
| Expired credentials detection | ✅ |
</Accordion>
<Accordion title="✅ Read & write data">
| Tools | Status |
| - | - |
| Pre-built integrations | 🚫 (time to contribute: &lt;48h) |
| API unification | ✅ |
| 2-way sync | ✅ |
| Webhooks from Nango on data modifications | ✅ |
| Real-time webhooks from 3rd-party API | 🚫 (time to contribute: &lt;48h) |
| Proxy requests | ✅ |
</Accordion>
<Accordion title="✅ Observability & data quality">
| Tools | Status |
| - | - |
| HTTP request logging | ✅ |
| End-to-type type safety | ✅ |
| Data runtime validation | ✅ |
| OpenTelemetry export | ✅ |
| Slack alerts on errors | ✅ |
| Integration status API | ✅ |
</Accordion>
<Accordion title="✅ Customization">
| Tools | Status |
| - | - |
| Create or customize use-cases | ✅ |
| Pre-configured pagination | 🚫 (time to contribute: &lt;48h) |
| Pre-configured rate-limit handling | 🚫 (time to contribute: &lt;48h) |
| Per-customer configurations | ✅ |
</Accordion>
</AccordionGroup>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Pre-built integrations

_No pre-built integration yet (time to contribute: &lt;48h)_

<Tip>Not seeing the integration you need? [Build your own](https://docs.nango.dev/guides/custom-integration-builder/overview) independently.</Tip>
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 38 additions & 1 deletion packages/providers/providers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6542,7 +6542,44 @@ salesforce-experience-cloud:
format: uri
pattern: '^https?://.*$'
automated: true

salesforce-cdp:
display_name: Salesforce (Data Cloud)
categories:
- storage
auth_mode: TWO_STEP
token_url: https://#.salesforce.com/services/oauth2/token
body_format: form
token_params:
grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer
assertion: ${credential.jwt}
token_headers:
content-type: application/x-www-form-urlencoded
additional_steps:
- body_format: form
token_params:
grant_type: urn:salesforce:grant-type:external:cdp
subject_token: ${step1.access_token}
subject_token_type: urn:ietf:params:oauth:token-type:access_token
token_headers:
content-type: application/x-www-form-urlencoded
token_url: ${step1.instance_url}/services/a360/token
Copy link
Collaborator

Choose a reason for hiding this comment

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

step1 is what in this context?

Copy link
Contributor Author

@hassan254-prog hassan254-prog Feb 7, 2025

Choose a reason for hiding this comment

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

step1 refers to the response from the initial request made before any additional steps, if provided. In subsequent steps, parameters are accessed dynamically using step{request_number}.

token_response_metadata:
- instance_url
proxy:
base_url: ${connectionConfig.instance_url}
token_response:
token: access_token
token_expiration: expires_in
token_expiration_strategy: expireIn
docs: https://docs.nango.dev/integrations/all/salesforce-cdp
docs_connect: https://docs.nango.dev/integrations/all/salesforce-cdp/connect
credentials:
jwt:
type: string
title: Encoded JWT
description: This is your pre-generated, encoded JSON Web Token (JWT)
secret: true
doc_section: '#step-1-generating-your-encoded-jwt'
sap-concur:
display_name: SAP Concur
categories:
Expand Down
12 changes: 10 additions & 2 deletions packages/server/lib/controllers/auth/postTwoStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
ErrorSourceEnum,
LogActionEnum,
getProvider,
linkConnection
linkConnection,
getConnectionMetadataFromTokenResponse
} from '@nangohq/shared';
import type { PostPublicTwoStepAuthorization, ProviderTwoStep } from '@nangohq/types';
import type { LogContext } from '@nangohq/logs';
Expand Down Expand Up @@ -68,7 +69,7 @@ export const postPublicTwoStepAuthorization = asyncWrapper<PostPublicTwoStepAuth
const bodyData: PostPublicTwoStepAuthorization['Body'] = val.data;
const queryString: PostPublicTwoStepAuthorization['Querystring'] = queryStringVal.data;
const { providerConfigKey }: PostPublicTwoStepAuthorization['Params'] = paramsVal.data;
const connectionConfig = queryString.params ? getConnectionConfig(queryString.params) : {};
let connectionConfig = queryString.params ? getConnectionConfig(queryString.params) : {};
let connectionId = queryString.connection_id || connectionService.generateConnectionId();
const hmac = 'hmac' in queryString ? queryString.hmac : undefined;
const isConnectSession = res.locals['authType'] === 'connectSession';
Expand Down Expand Up @@ -154,6 +155,13 @@ export const postPublicTwoStepAuthorization = asyncWrapper<PostPublicTwoStepAuth
return;
}

const tokenMetadata = getConnectionMetadataFromTokenResponse(credentials.raw, provider);

connectionConfig = {
...connectionConfig,
...tokenMetadata
};

const [updatedConnection] = await connectionService.upsertAuthConnection({
connectionId,
providerConfigKey,
Expand Down
4 changes: 2 additions & 2 deletions packages/server/lib/controllers/oauth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
getAdditionalAuthorizationParams,
getConnectionMetadataFromCallbackRequest,
missesInterpolationParam,
getConnectionMetadataFromTokenResponse,
missesInterpolationParamInObject
} from '../utils/utils.js';
import type { DBEnvironment, DBTeam, Provider, ProviderOAuth2 } from '@nangohq/types';
Expand Down Expand Up @@ -37,7 +36,8 @@ import {
ErrorSourceEnum,
interpolateObjectValues,
getProvider,
linkConnection
linkConnection,
getConnectionMetadataFromTokenResponse
} from '@nangohq/shared';
import publisher from '../clients/publisher.client.js';
import * as WSErrBuilder from '../utils/web-socket-error.js';
Expand Down
40 changes: 0 additions & 40 deletions packages/server/lib/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,46 +81,6 @@ export function getConnectionMetadataFromCallbackRequest(queryParams: any, provi
return arr != null && arr.length > 0 ? (Object.fromEntries(arr) as Record<string, string>) : {};
}

/**
* A helper function to extract the additional connection metadata returned from the Provider in the token response.
* It can parse booleans or strings only
*/
export function getConnectionMetadataFromTokenResponse(params: any, provider: Provider): Record<string, any> {
if (!params || !provider.token_response_metadata) {
return {};
}

const whitelistedKeys = provider.token_response_metadata;

const getValueFromDotNotation = (obj: any, key: string): any => {
return key.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj);
};

// Filter out non-strings, non-booleans & non-whitelisted keys.
const arr = Object.entries(params).filter(([k, v]) => {
const isStringValueOrBoolean = typeof v === 'string' || typeof v === 'boolean';
if (isStringValueOrBoolean && whitelistedKeys.includes(k)) {
return true;
}
// Check for dot notation keys
const dotNotationValue = getValueFromDotNotation(params, k);
return isStringValueOrBoolean && whitelistedKeys.includes(dotNotationValue);
});

// Add support for dot notation keys
const dotNotationArr = whitelistedKeys
.map((key) => {
const value = getValueFromDotNotation(params, key);
const isStringValueOrBoolean = typeof value === 'string' || typeof value === 'boolean';
return isStringValueOrBoolean ? [key, value] : null;
})
.filter(Boolean);

const combinedArr: [string, any][] = [...arr, ...dotNotationArr].filter((item) => item !== null) as [string, any][];

return combinedArr.length > 0 ? (Object.fromEntries(combinedArr) as Record<string, any>) : {};
}

export function parseConnectionConfigParamsFromTemplate(provider: Provider): string[] {
if (
provider.token_url ||
Expand Down
Loading
Loading