Skip to content

Commit

Permalink
[Salesforce] Alternative authentication flow (#2023)
Browse files Browse the repository at this point in the history
* Implementation of a flow where the conventional OAuth refreshToken flow is attempted and the username+password flow is tried if that fails

* Throw an error if no authentication method returns rather than returning undefined, fixes build

* WIP - generate new request client that is extended with password generated access tokens. No Build

* Exports createRequestClient from core and uses it in salesforce

* Correctly use login.salesforce url rather than individual instance url when authenticating

* Reverts refreshAccessToken to original version. Uses process.env. client ID and secret to pull from chamber

* Uses original access_token reference for result of refresh request

* References salesforce_client_id/secret rather than the actions_ prefixed verion, which doesn't exist

* Adds username+password auth to all actions, including dynamic fields

* Removes unused auth methods in the Salesforce class

* Uses username+password for refreshAccessToken flow where appropriate, uses security_token field to construct password sent to salesforce, updates descriptions

* Unit test for username+password flow

* Tests functionality when no security_token is provided as well

* Adds a comment
  • Loading branch information
nick-Ag authored May 21, 2024
1 parent 11dfb3d commit 17d9395
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 23 deletions.
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {
export { createTestEvent } from './create-test-event'
export { createTestIntegration } from './create-test-integration'
export { default as createInstance } from './request-client'
export { default as createRequestClient } from './create-request-client'
export { defaultValues } from './defaults'
export {
IntegrationError,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import nock from 'nock'
import createRequestClient from '../../../../../core/src/create-request-client'
import Salesforce from '../sf-operations'
import Salesforce, { authenticateWithPassword } from '../sf-operations'
import { API_VERSION } from '../sf-operations'
import type { GenericPayload } from '../sf-types'
import { Settings } from '../generated-types'

const settings = {
instanceUrl: 'https://test.salesforce.com/'
Expand Down Expand Up @@ -771,4 +772,70 @@ describe('Salesforce', () => {
)
})
})

describe('Username & Password flow', () => {
const usernamePasswordOnly: Settings = {
username: '[email protected]',
auth_password: 'gary1997',
instanceUrl: 'https://spongebob.salesforce.com/',
isSandbox: false
}

const usernamePasswordAndToken: Settings = {
username: '[email protected]',
auth_password: 'gary1997',
instanceUrl: 'https://spongebob.salesforce.com/',
isSandbox: false,
security_token: 'abc123'
}

process.env['SALESFORCE_CLIENT_ID'] = 'id'
process.env['SALESFORCE_CLIENT_SECRET'] = 'secret'

it('should authenticate using the username and password flow when only the username and password are provided', async () => {
nock('https://login.salesforce.com/services/oauth2/token')
.post('', {
grant_type: 'password',
client_id: 'id',
client_secret: 'secret',
username: usernamePasswordOnly.username,
password: usernamePasswordOnly.auth_password
})
.reply(201, {
access_token: 'abc'
})

const res = await authenticateWithPassword(
usernamePasswordOnly.username as string, // tells typescript that these are defined
usernamePasswordOnly.auth_password as string,
usernamePasswordOnly.security_token,
usernamePasswordOnly.isSandbox
)

expect(res.accessToken).toEqual('abc')
})

it('should authenticate using the username and password flow when the username, password and security token are provided', async () => {
nock('https://login.salesforce.com/services/oauth2/token')
.post('', {
grant_type: 'password',
client_id: 'id',
client_secret: 'secret',
username: usernamePasswordAndToken.username,
password: `${usernamePasswordAndToken.auth_password}${usernamePasswordAndToken.security_token}`
})
.reply(201, {
access_token: 'abc'
})

const res = await authenticateWithPassword(
usernamePasswordAndToken.username as string, // tells typescript that these are defined
usernamePasswordAndToken.auth_password as string,
usernamePasswordAndToken.security_token,
usernamePasswordAndToken.isSandbox
)

expect(res.accessToken).toEqual('abc')
})
})
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ActionDefinition, IntegrationError } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import Salesforce from '../sf-operations'
import Salesforce, { generateSalesforceRequest } from '../sf-operations'
import {
bulkUpsertExternalId,
bulkUpdateRecordId,
Expand Down Expand Up @@ -181,7 +181,7 @@ const action: ActionDefinition<Settings, Payload> = {
customFields: customFields
},
perform: async (request, { settings, payload }) => {
const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

if (payload.operation === 'create') {
if (!payload.name) {
Expand All @@ -208,7 +208,7 @@ const action: ActionDefinition<Settings, Payload> = {
}
},
performBatch: async (request, { settings, payload }) => {
const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

if (payload[0].operation === 'upsert') {
if (!payload[0].name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
recordMatcherOperator,
batch_size
} from '../sf-properties'
import Salesforce from '../sf-operations'
import Salesforce, { generateSalesforceRequest } from '../sf-operations'

const OBJECT_NAME = 'Case'

Expand All @@ -35,7 +35,7 @@ const action: ActionDefinition<Settings, Payload> = {
customFields: customFields
},
perform: async (request, { settings, payload }) => {
const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

if (payload.operation === 'create') {
return await sf.createRecord(payload, OBJECT_NAME)
Expand All @@ -56,7 +56,7 @@ const action: ActionDefinition<Settings, Payload> = {
}
},
performBatch: async (request, { settings, payload }) => {
const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

return sf.bulkHandler(payload, OBJECT_NAME)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
recordMatcherOperator,
batch_size
} from '../sf-properties'
import Salesforce from '../sf-operations'
import Salesforce, { generateSalesforceRequest } from '../sf-operations'

const OBJECT_NAME = 'Contact'

Expand Down Expand Up @@ -132,7 +132,7 @@ const action: ActionDefinition<Settings, Payload> = {
customFields: customFields
},
perform: async (request, { settings, payload }) => {
const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

if (payload.operation === 'create') {
if (!payload.last_name) {
Expand All @@ -159,7 +159,7 @@ const action: ActionDefinition<Settings, Payload> = {
}
},
performBatch: async (request, { settings, payload }) => {
const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

if (payload[0].operation === 'upsert') {
if (!payload[0].last_name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
recordMatcherOperator,
batch_size
} from '../sf-properties'
import Salesforce from '../sf-operations'
import Salesforce, { generateSalesforceRequest } from '../sf-operations'
import { PayloadValidationError } from '@segment/actions-core'
const OPERATIONS_WITH_CUSTOM_FIELDS = ['create', 'update', 'upsert']

Expand All @@ -39,7 +39,10 @@ const action: ActionDefinition<Settings, Payload> = {
},
dynamicFields: {
customObjectName: async (request, data) => {
const sf: Salesforce = new Salesforce(data.settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(
data.settings.instanceUrl,
await generateSalesforceRequest(data.settings, request)
)

return sf.customObjectName()
}
Expand All @@ -49,7 +52,7 @@ const action: ActionDefinition<Settings, Payload> = {
throw new PayloadValidationError('Custom fields are required for this operation.')
}

const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

if (payload.operation === 'create') {
return await sf.createRecord(payload, payload.customObjectName)
Expand All @@ -74,7 +77,7 @@ const action: ActionDefinition<Settings, Payload> = {
throw new PayloadValidationError('Custom fields are required for this operation.')
}

const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

return sf.bulkHandler(payload, payload[0].customObjectName)
}
Expand Down

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

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DestinationDefinition } from '@segment/actions-core'
import { DestinationDefinition } from '@segment/actions-core'
import type { Settings } from './generated-types'
// This has to be 'cases' because 'case' is a Javascript reserved word
import cases from './cases'
Expand All @@ -7,6 +7,7 @@ import opportunity from './opportunity'
import customObject from './customObject'
import contact from './contact'
import account from './account'
import { authenticateWithPassword } from './sf-operations'

interface RefreshTokenResponse {
access_token: string
Expand All @@ -33,9 +34,39 @@ const destination: DestinationDefinition<Settings> = {
'Enable to authenticate into a sandbox instance. You can log in to a sandbox by appending the sandbox name to your Salesforce username. For example, if a username for a production org is [email protected] and the sandbox is named test, the username to log in to the sandbox is [email protected]. If you are already authenticated, please disconnect and reconnect with your sandbox username.',
type: 'boolean',
default: false
},
username: {
label: 'Username',
description:
'The username of the Salesforce account you want to connect to. When all three of username, password, and security token are provided, a username-password flow is used to authenticate. This field is hidden to all users except those who have opted in to the username+password flow.',
type: 'string'
},
auth_password: {
// auth_ prefix is used because password is a reserved word
label: 'Password',
description:
'The password of the Salesforce account you want to connect to. When all three of username, password, and security token are provided, a username-password flow is used to authenticate. This field is hidden to all users except those who have opted in to the username+password flow.',
type: 'string'
},
security_token: {
label: 'Security Token',
description:
'The security token of the Salesforce account you want to connect to. When all three of username, password, and security token are provided, a username-password flow is used to authenticate. This value will be appended to the password field to construct the credential used for authentication. This field is hidden to all users except those who have opted in to the username+password flow.',
type: 'string'
}
},
refreshAccessToken: async (request, { auth, settings }) => {
if (settings.username && settings.auth_password) {
const { accessToken } = await authenticateWithPassword(
settings.username,
settings.auth_password,
settings.security_token,
settings.isSandbox
)

return { accessToken }
}

// Return a request that refreshes the access_token if the API supports it
const baseUrl = settings.isSandbox ? 'https://test.salesforce.com' : 'https://login.salesforce.com'
const res = await request<RefreshTokenResponse>(`${baseUrl}/services/oauth2/token`, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
recordMatcherOperator,
batch_size
} from '../sf-properties'
import Salesforce from '../sf-operations'
import Salesforce, { generateSalesforceRequest } from '../sf-operations'

const OBJECT_NAME = 'Lead'

Expand Down Expand Up @@ -139,7 +139,7 @@ const action: ActionDefinition<Settings, Payload> = {
customFields: customFields
},
perform: async (request, { settings, payload }) => {
const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

if (payload.operation === 'create') {
if (!payload.last_name) {
Expand All @@ -166,7 +166,7 @@ const action: ActionDefinition<Settings, Payload> = {
}
},
performBatch: async (request, { settings, payload }) => {
const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

if (payload[0].operation === 'upsert') {
if (!payload[0].last_name) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ActionDefinition, IntegrationError } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import Salesforce from '../sf-operations'
import Salesforce, { generateSalesforceRequest } from '../sf-operations'
import {
bulkUpsertExternalId,
bulkUpdateRecordId,
Expand Down Expand Up @@ -56,7 +56,7 @@ const action: ActionDefinition<Settings, Payload> = {
customFields: customFields
},
perform: async (request, { settings, payload }) => {
const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

if (payload.operation === 'create') {
if (!payload.close_date || !payload.name || !payload.stage_name) {
Expand All @@ -83,7 +83,7 @@ const action: ActionDefinition<Settings, Payload> = {
}
},
performBatch: async (request, { settings, payload }) => {
const sf: Salesforce = new Salesforce(settings.instanceUrl, request)
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

if (payload[0].operation === 'upsert') {
if (!payload[0].close_date || !payload[0].name || !payload[0].stage_name) {
Expand Down
Loading

0 comments on commit 17d9395

Please sign in to comment.