-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
@W-16553557 Add passwordless login helpers #173
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
/** | ||
* @jest-environment node | ||
*/ | ||
/* eslint header/header: "off", max-lines:"off" */ | ||
/* | ||
* Copyright (c) 2022, salesforce.com, inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
|
||
import nock from 'nock'; | ||
import {ShopperLogin} from '../../lib/shopperLogin'; | ||
import * as slasHelper from './slasHelper'; | ||
import { | ||
authorizePasswordless, | ||
getPasswordLessAccessToken, | ||
} from './passwordlessHelper'; | ||
|
||
type slasHelperType = typeof slasHelper; | ||
// Mock the module | ||
jest.mock('./slasHelper', () => { | ||
const actualUtils = jest.requireActual<slasHelperType>('./slasHelper'); | ||
const createCodeVerifierMock = jest.fn(() => 'code_verifier'); | ||
return { | ||
...actualUtils, | ||
createCodeVerifier: createCodeVerifierMock, // Mock the specific function | ||
}; | ||
}); | ||
|
||
const authorizePasswordlessCustomerMock = jest.fn(); | ||
const getPasswordLessAccessTokenMock = jest.fn(); | ||
|
||
const createMockSlasClient = () => | ||
({ | ||
clientConfig: { | ||
parameters: { | ||
shortCode: 'short_code', | ||
organizationId: 'organization_id', | ||
clientId: 'client_id', | ||
siteId: 'site_id', | ||
}, | ||
}, | ||
authorizePasswordlessCustomer: authorizePasswordlessCustomerMock, | ||
getPasswordLessAccessToken: getPasswordLessAccessTokenMock, | ||
} as unknown as ShopperLogin<{ | ||
shortCode: string; | ||
organizationId: string; | ||
clientId: string; | ||
siteId: string; | ||
}>); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
nock.cleanAll(); | ||
}); | ||
|
||
describe('authorizePasswordless is working', () => { | ||
test('Correct parameters are used to call SLAS Client authorize', async () => { | ||
const mockSlasClient = createMockSlasClient(); | ||
const {clientId, organizationId, siteId} = | ||
mockSlasClient.clientConfig.parameters; | ||
|
||
const credentials = { | ||
clientSecret: 'slas_private_secret', | ||
}; | ||
const parameters = { | ||
callbackURI: 'www.something.com/callback', | ||
usid: 'a_usid', | ||
userid: 'a_userid', | ||
locale: 'a_locale', | ||
mode: 'callback', | ||
}; | ||
const authHeaderExpected = `Basic ${slasHelper.stringToBase64( | ||
`${clientId}:${credentials.clientSecret}` | ||
)}`; | ||
await authorizePasswordless(mockSlasClient, credentials, parameters); | ||
const expectedReqOptions = { | ||
headers: { | ||
Authorization: authHeaderExpected, | ||
}, | ||
parameters: { | ||
organizationId, | ||
}, | ||
body: { | ||
user_id: parameters.userid, | ||
mode: parameters.mode, | ||
locale: parameters.locale, | ||
channel_id: siteId, | ||
callback_uri: parameters.callbackURI, | ||
usid: parameters.usid, | ||
}, | ||
}; | ||
expect(authorizePasswordlessCustomerMock).toBeCalledWith( | ||
expectedReqOptions, | ||
Check warning on line 95 in src/static/helpers/passwordlessHelper.test.ts GitHub Actions / linux-tests (12)
Check warning on line 95 in src/static/helpers/passwordlessHelper.test.ts GitHub Actions / linux-tests (14)
Check warning on line 95 in src/static/helpers/passwordlessHelper.test.ts GitHub Actions / linux-tests (16)
|
||
true | ||
); | ||
}); | ||
}); | ||
|
||
describe('getPasswordLessAccessToken is working', () => { | ||
test('Correct parameters are used to call SLAS Client authorize', async () => { | ||
const mockSlasClient = createMockSlasClient(); | ||
const {clientId, organizationId} = mockSlasClient.clientConfig.parameters; | ||
|
||
const credentials = { | ||
clientSecret: 'slas_private_secret', | ||
}; | ||
const parameters = { | ||
pwdlessLoginToken: '123456', | ||
dnt: '1', | ||
}; | ||
const authHeaderExpected = `Basic ${slasHelper.stringToBase64( | ||
`${clientId}:${credentials.clientSecret}` | ||
)}`; | ||
await getPasswordLessAccessToken(mockSlasClient, credentials, parameters); | ||
const expectedReqOptions = { | ||
headers: { | ||
Authorization: authHeaderExpected, | ||
}, | ||
parameters: { | ||
organizationId, | ||
}, | ||
body: { | ||
dnt: parameters.dnt, | ||
code_verifier: 'code_verifier', | ||
grant_type: 'client_credentials', | ||
hint: 'pwdless_login', | ||
pwdless_login_token: parameters.pwdlessLoginToken, | ||
}, | ||
}; | ||
expect(getPasswordLessAccessTokenMock).toBeCalledWith(expectedReqOptions); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/* | ||
* Copyright (c) 2024, Salesforce, Inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
import {ShopperLogin, TokenResponse} from '../../lib/shopperLogin'; | ||
import {createCodeVerifier, stringToBase64} from './slasHelper'; | ||
|
||
/** | ||
* Function to send passwordless login token | ||
* **Note** At the moment, passwordless is only supported on private client | ||
* @param slasClient a configured instance of the ShopperLogin SDK client. | ||
* @param credentials - the id and password and clientSecret (if applicable) to login with. | ||
* @param credentials.clientSecret? - secret associated with client ID | ||
* @param parameters - parameters to pass in the API calls. | ||
* @param parameters.callbackURI? - URI to send the passwordless login token to. Must be listed in your SLAS configuration. Required when mode is callback | ||
* @param parameters.usid? - Unique Shopper Identifier to enable personalization. | ||
* @param parameters.userid - User Id for login | ||
* @param parameters.locale - The locale of the template. Not needed for the callback mode | ||
* @param parameters.mode - Medium of sending login token | ||
* @returns Promise of Response | ||
*/ | ||
export async function authorizePasswordless( | ||
slasClient: ShopperLogin<{ | ||
shortCode: string; | ||
organizationId: string; | ||
clientId: string; | ||
siteId: string; | ||
}>, | ||
credentials: { | ||
clientSecret: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might want to throw if this information is not provided as we are doing here. This comment also applies to the |
||
}, | ||
parameters: { | ||
callbackURI?: string; | ||
usid?: string; | ||
userid: string; | ||
locale?: string; | ||
mode: string; | ||
} | ||
): Promise<Response> { | ||
const authHeaderIdSecret = `Basic ${stringToBase64( | ||
`${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` | ||
)}`; | ||
const tokenBody = { | ||
user_id: parameters.userid, | ||
mode: parameters.mode, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT: It doesn't look like we have have a pattern for throwing when required params are not included, so I don't expect you to add it now because I like consistency more. But I'll take note on that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It also seems like typescript checks for the existence of these params that are directly required. But when it's parameters within an object like ShopperLogin, typescript doesn't automatically give an error that expected arguments don't exist There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't reply on TypeScript types for things like that because this lib is compiled into JS and it can be used by a project that isn't TS. Typescript won't bake in any kind of parameter checking so there is the possibility that you this code would make a server request that would for sure fail if that param is required. But again, no need to change that now, it doesn't seem like it's a pattern that we have, and we should probably fix that in another PR in one big swoop. |
||
...(parameters.locale && {locale: parameters.locale}), | ||
...(parameters.usid && {usid: parameters.usid}), | ||
channel_id: slasClient.clientConfig.parameters.siteId, | ||
...(parameters.callbackURI && {callback_uri: parameters.callbackURI}), | ||
}; | ||
|
||
return slasClient.authorizePasswordlessCustomer( | ||
{ | ||
headers: { | ||
Authorization: authHeaderIdSecret, | ||
}, | ||
parameters: { | ||
organizationId: slasClient.clientConfig.parameters.organizationId, | ||
}, | ||
body: tokenBody, | ||
}, | ||
true | ||
); | ||
} | ||
|
||
/** | ||
* Function to login user with passwordless login token | ||
* **Note** At the moment, passwordless is only supported on private client | ||
* @param slasClient a configured instance of the ShopperLogin SDK client. | ||
* @param credentials - the id and password and clientSecret (if applicable) to login with. | ||
* @param credentials.clientSecret? - secret associated with client ID | ||
* @param parameters - parameters to pass in the API calls. | ||
* @param parameters.callbackURI? - URI to send the passwordless login token to. Must be listed in your SLAS configuration. Required when mode is callback | ||
* @param parameters.pwdlessLoginToken - Passwordless login token | ||
* @param parameters.dnt? - Optional parameter to enable Do Not Track (DNT) for the user. | ||
* @returns Promise of Response or Object | ||
*/ | ||
export async function getPasswordLessAccessToken( | ||
slasClient: ShopperLogin<{ | ||
shortCode: string; | ||
organizationId: string; | ||
clientId: string; | ||
siteId: string; | ||
}>, | ||
credentials: { | ||
clientSecret: string; | ||
}, | ||
parameters: { | ||
pwdlessLoginToken: string; | ||
dnt?: string; | ||
} | ||
): Promise<TokenResponse> { | ||
const codeVerifier = createCodeVerifier(); | ||
const authHeaderIdSecret = `Basic ${stringToBase64( | ||
`${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` | ||
)}`; | ||
|
||
const tokenBody = { | ||
grant_type: 'client_credentials', | ||
hint: 'pwdless_login', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These parameters come from the API docs: https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-login?meta=getPasswordLessAccessToken |
||
pwdless_login_token: parameters.pwdlessLoginToken, | ||
code_verifier: codeVerifier, | ||
...(parameters.dnt && {dnt: parameters.dnt}), | ||
}; | ||
return slasClient.getPasswordLessAccessToken({ | ||
headers: { | ||
Authorization: authHeaderIdSecret, | ||
}, | ||
parameters: { | ||
organizationId: slasClient.clientConfig.parameters.organizationId, | ||
}, | ||
body: tokenBody, | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: The passwordless login API's are part of SLAS right? And from the looks of it, the content of this file is somewhat small, is there a reason you didn't slap it in the slasHelpers, sounds like that is the right context for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but it was moved to its own file because adding these functions fails the 500 line rule for
slasHelpers
in lintingThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats super lame, seems a little arbitrary.
It definitely seems like the SLAS passwordless helper should be located along with all the other SLAS helpers. Can you propose an alternative (non-breaking) solution here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed that it's pretty arbitrary, when the line limit was implemented (3 years ago) I don't think the team originally envisioned a helper file growing to this size. I think it'd be ok to update the line limit or ignore it for this file:
Ignore:
/* eslint-disable max-lines */
Update max lines:
commerce-sdk-isomorphic/.eslintrc.json
Line 57 in 2af2671
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll override the max-lines for this file to make it as flexible as possible for new helpers as part of this epic