From 0b1688caf493ab4afa32648c76ad4b71a00ae530 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 28 Oct 2020 18:07:35 -0400 Subject: [PATCH] :zap: Feature/salesforce jwt bearer (#1082) * Salesforce - OAuth 2.0 JWT Bearer Flow for Server-to-Server Integration * :zap: Small improvements * :zap: Small fix Co-authored-by: Craig McElroy --- .../SalesforceJwtApi.credentials.ts | 54 +++++++++++ .../nodes/Salesforce/GenericFunctions.ts | 92 ++++++++++++++++--- .../nodes/Salesforce/Salesforce.node.ts | 35 +++++++ packages/nodes-base/package.json | 1 + 4 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 packages/nodes-base/credentials/SalesforceJwtApi.credentials.ts diff --git a/packages/nodes-base/credentials/SalesforceJwtApi.credentials.ts b/packages/nodes-base/credentials/SalesforceJwtApi.credentials.ts new file mode 100644 index 0000000000..ef0b7d9579 --- /dev/null +++ b/packages/nodes-base/credentials/SalesforceJwtApi.credentials.ts @@ -0,0 +1,54 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class SalesforceJwtApi implements ICredentialType { + name = 'salesforceJwtApi'; + displayName = 'Salesforce JWT API'; + documentationUrl = 'salesforce'; + properties = [ + { + displayName: 'Environment Type', + name: 'environment', + type: 'options' as NodePropertyTypes, + options: [ + { + name: 'Production', + value: 'production', + }, + { + name: 'Sandbox', + value: 'sandbox', + }, + ], + default: 'production', + }, + { + displayName: 'Client ID', + name: 'clientId', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + description: 'Consumer Key from Salesforce Connected App.', + }, + { + displayName: 'Username', + name: 'username', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Private Key', + name: 'privateKey', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + required: true, + description: 'Use the multiline editor. Make sure it is in standard PEM key format:
-----BEGIN PRIVATE KEY-----
KEY DATA GOES HERE
-----END PRIVATE KEY-----', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts index 5f8139c7da..1c1064a2dd 100644 --- a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -13,19 +13,33 @@ import { INodePropertyOptions, } from 'n8n-workflow'; +import * as moment from 'moment-timezone'; + +import * as jwt from 'jsonwebtoken'; + export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('salesforceOAuth2Api'); - const subdomain = ((credentials!.accessTokenUrl as string).match(/https:\/\/(.+).salesforce\.com/) || [])[1]; - const options: OptionsWithUri = { - method, - body: method === 'GET' ? undefined : body, - qs, - uri: `https://${subdomain}.salesforce.com/services/data/v39.0${uri || endpoint}`, - json: true, - }; + const authenticationMethod = this.getNodeParameter('authentication', 0, 'oAuth2') as string; + try { - //@ts-ignore - return await this.helpers.requestOAuth2.call(this, 'salesforceOAuth2Api', options); + if (authenticationMethod === 'jwt') { + // https://help.salesforce.com/articleView?id=remoteaccess_oauth_jwt_flow.htm&type=5 + const credentialsType = 'salesforceJwtApi'; + const credentials = this.getCredentials(credentialsType); + const response = await getAccessToken.call(this, credentials as IDataObject); + const { instance_url, access_token } = response; + const options = getOptions.call(this, method, (uri || endpoint), body, qs, instance_url as string); + options.headers!.Authorization = `Bearer ${access_token}`; + //@ts-ignore + return await this.helpers.request(options); + } else { + // https://help.salesforce.com/articleView?id=remoteaccess_oauth_web_server_flow.htm&type=5 + const credentialsType = 'salesforceOAuth2Api'; + const credentials = this.getCredentials(credentialsType); + const subdomain = ((credentials!.accessTokenUrl as string).match(/https:\/\/(.+).salesforce\.com/) || [])[1]; + const options = getOptions.call(this, method, (uri || endpoint), body, qs, `https://${subdomain}.salesforce.com`); + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, credentialsType, options); + } } catch (error) { if (error.response && error.response.body && error.response.body[0] && error.response.body[0].message) { // Try to return the error prettier @@ -36,7 +50,6 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin } export async function salesforceApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any - const returnData: IDataObject[] = []; let responseData; @@ -54,8 +67,6 @@ export async function salesforceApiRequestAllItems(this: IExecuteFunctions | ILo return returnData; } - - /** * Sorts the given options alphabetically * @@ -70,3 +81,56 @@ export function sortOptions(options: INodePropertyOptions[]): void { return 0; }); } + +function getOptions(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any, qs: IDataObject, instanceUrl: string): OptionsWithUri { + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body: method === 'GET' ? undefined : body, + qs, + uri: `${instanceUrl}/services/data/v39.0${endpoint}`, + json: true + }; + + //@ts-ignore + return options; +} + +function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, credentials: IDataObject): Promise { + const now = moment().unix(); + const authUrl = credentials.environment === 'sandbox' ? 'https://test.salesforce.com' : 'https://login.salesforce.com'; + + const signature = jwt.sign( + { + 'iss': credentials.clientId as string, + 'sub': credentials.username as string, + 'aud': authUrl, + 'exp': now + 3 * 60, + }, + credentials.privateKey as string, + { + algorithm: 'RS256', + header: { + 'alg': 'RS256', + }, + } + ); + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + form: { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: signature, + }, + uri: `${authUrl}/services/oauth2/token`, + json: true + }; + + //@ts-ignore + return this.helpers.request(options); +} diff --git a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts index 26981c2038..9cf20ef9b6 100644 --- a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts +++ b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts @@ -127,9 +127,44 @@ export class Salesforce implements INodeType { { name: 'salesforceOAuth2Api', required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, + { + name: 'salesforceJwtApi', + required: true, + displayOptions: { + show: { + authentication: [ + 'jwt', + ], + }, + }, }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'OAuth2', + value: 'oAuth2', + }, + { + name: 'OAuth2 JWT', + value: 'jwt', + }, + ], + default: 'oAuth2', + description: 'OAuth Authorization Flow', + }, { displayName: 'Resource', name: 'resource', diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 49e930fa73..197a57e46f 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -152,6 +152,7 @@ "dist/credentials/RundeckApi.credentials.js", "dist/credentials/ShopifyApi.credentials.js", "dist/credentials/SalesforceOAuth2Api.credentials.js", + "dist/credentials/SalesforceJwtApi.credentials.js", "dist/credentials/SentryIoApi.credentials.js", "dist/credentials/SentryIoServerApi.credentials.js", "dist/credentials/SentryIoOAuth2Api.credentials.js",