diff --git a/.eslintrc.json b/.eslintrc.json index 1ebee1b6..ef6c9f4c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -91,6 +91,12 @@ "rules": { "@typescript-eslint/explicit-module-boundary-types": "error" } + }, + { + "files": ["./src/static/helpers/slasHelper.ts"], + "rules": { + "max-lines": "off" + } } ], "settings": { diff --git a/src/static/helpers/slasHelper.test.ts b/src/static/helpers/slasHelper.test.ts index e84251b2..d2048da8 100644 --- a/src/static/helpers/slasHelper.test.ts +++ b/src/static/helpers/slasHelper.test.ts @@ -58,6 +58,8 @@ const authenticateCustomerMock = jest.fn(() => ({url})); const getAccessTokenMock = jest.fn(() => expectedTokenResponse); const logoutCustomerMock = jest.fn(() => expectedTokenResponse); const generateCodeChallengeMock = jest.fn(() => 'code_challenge'); +const authorizePasswordlessCustomerMock = jest.fn(); +const getPasswordLessAccessTokenMock = jest.fn(); const createMockSlasClient = () => ({ @@ -73,6 +75,8 @@ const createMockSlasClient = () => getAccessToken: getAccessTokenMock, logoutCustomer: logoutCustomerMock, generateCodeChallenge: generateCodeChallengeMock, + authorizePasswordlessCustomer: authorizePasswordlessCustomerMock, + getPasswordLessAccessToken: getPasswordLessAccessTokenMock, } as unknown as ShopperLogin<{ shortCode: string; organizationId: string; @@ -435,6 +439,150 @@ describe('Registered B2C user flow', () => { }); }); +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 parametersAuthorizePasswordless = { + callbackURI: 'www.something.com/callback', + usid: 'a_usid', + userid: 'a_userid', + locale: 'a_locale', + mode: 'callback', + }; + const authHeaderExpected = `Basic ${slasHelper.stringToBase64( + `${clientId}:${credentialsPrivate.clientSecret}` + )}`; + await slasHelper.authorizePasswordless( + mockSlasClient, + credentialsPrivate, + parametersAuthorizePasswordless + ); + const expectedReqOptions = { + headers: { + Authorization: authHeaderExpected, + }, + parameters: { + organizationId, + }, + body: { + user_id: parametersAuthorizePasswordless.userid, + mode: parametersAuthorizePasswordless.mode, + locale: parametersAuthorizePasswordless.locale, + channel_id: siteId, + callback_uri: parametersAuthorizePasswordless.callbackURI, + usid: parametersAuthorizePasswordless.usid, + }, + }; + expect(authorizePasswordlessCustomerMock).toBeCalledWith( + expectedReqOptions, + true + ); + }); + test('Throw when required parameters missing', async () => { + const mockSlasClient = { + clientConfig: { + parameters: { + shortCode: 'short_code', + organizationId: 'organization_id', + clientId: 'client_id', + }, + }, + authorizePasswordlessCustomer: authorizePasswordlessCustomerMock, + getPasswordLessAccessToken: getPasswordLessAccessTokenMock, + } as unknown as ShopperLogin<{ + shortCode: string; + organizationId: string; + clientId: string; + siteId: string; + }>; + const parametersAuthorizePasswordless = { + callbackURI: 'www.something.com/callback', + usid: 'a_usid', + userid: 'a_userid', + locale: 'a_locale', + mode: 'callback', + }; + await expect( + slasHelper.authorizePasswordless( + mockSlasClient, + credentialsPrivate, + parametersAuthorizePasswordless + ) + ).rejects.toThrow( + 'Required argument channel_id is not provided through clientConfig.parameters.siteId' + ); + }); +}); + +describe('getPasswordLessAccessToken is working', () => { + test('Correct parameters are used to call SLAS Client helper', async () => { + const mockSlasClient = createMockSlasClient(); + const {clientId, organizationId} = mockSlasClient.clientConfig.parameters; + + const parametersPasswordlessToken = { + pwdlessLoginToken: '123456', + dnt: '1', + }; + const authHeaderExpected = `Basic ${slasHelper.stringToBase64( + `${clientId}:${credentialsPrivate.clientSecret}` + )}`; + await slasHelper.getPasswordLessAccessToken( + mockSlasClient, + credentialsPrivate, + parametersPasswordlessToken + ); + const expectedReqOptions = { + headers: { + Authorization: authHeaderExpected, + }, + parameters: { + organizationId, + }, + body: { + dnt: parametersPasswordlessToken.dnt, + code_verifier: expect.stringMatching(/./) as string, + grant_type: 'client_credentials', + hint: 'pwdless_login', + pwdless_login_token: parametersPasswordlessToken.pwdlessLoginToken, + }, + }; + expect(getPasswordLessAccessTokenMock).toBeCalledWith(expectedReqOptions); + }); + test('Throw when required parameters missing', async () => { + const mockSlasClient = { + clientConfig: { + parameters: { + shortCode: 'short_code', + clientId: 'client_id', + }, + }, + authorizePasswordlessCustomer: authorizePasswordlessCustomerMock, + getPasswordLessAccessToken: getPasswordLessAccessTokenMock, + } as unknown as ShopperLogin<{ + shortCode: string; + organizationId: string; + clientId: string; + siteId: string; + }>; + const parametersPasswordlessToken = { + pwdlessLoginToken: '123456', + dnt: '1', + }; + await expect( + slasHelper.getPasswordLessAccessToken( + mockSlasClient, + credentialsPrivate, + parametersPasswordlessToken + ) + ).rejects.toThrow( + 'Required argument organizationId is not provided through clientConfig.parameters.organizationId' + ); + }); +}); + describe('Refresh Token', () => { test('refreshes the token with slas public client', () => { const expectedBody = { diff --git a/src/static/helpers/slasHelper.ts b/src/static/helpers/slasHelper.ts index fe12b1a2..931b0807 100644 --- a/src/static/helpers/slasHelper.ts +++ b/src/static/helpers/slasHelper.ts @@ -359,6 +359,145 @@ export async function loginRegisteredUserB2C( return slasClient.getAccessToken({body: tokenBody}); } +/** + * 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; + }, + parameters: { + callbackURI?: string; + usid?: string; + userid: string; + locale?: string; + mode: string; + } +): Promise { + if (!credentials.clientSecret) { + throw new Error('Required argument client secret is not provided'); + } + if (!slasClient.clientConfig.parameters.siteId) { + throw new Error( + 'Required argument channel_id is not provided through clientConfig.parameters.siteId' + ); + } + if (!parameters.mode) { + throw new Error( + 'Required argument mode is not provided through parameters' + ); + } + if (!parameters.userid) { + throw new Error( + 'Required argument userid is not provided through parameters' + ); + } + const authHeaderIdSecret = `Basic ${stringToBase64( + `${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` + )}`; + const tokenBody = { + user_id: parameters.userid, + mode: parameters.mode, + ...(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 { + if (!credentials.clientSecret) { + throw new Error('Required argument client secret is not provided'); + } + if (!slasClient.clientConfig.parameters.organizationId) { + throw new Error( + 'Required argument organizationId is not provided through clientConfig.parameters.organizationId' + ); + } + if (!parameters.pwdlessLoginToken) { + throw new Error( + 'Required argument pwdlessLoginToken is not provided through parameters' + ); + } + const codeVerifier = createCodeVerifier(); + const authHeaderIdSecret = `Basic ${stringToBase64( + `${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` + )}`; + + const tokenBody = { + grant_type: 'client_credentials', + hint: 'pwdless_login', + 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, + }); +} + /** * Exchange a refresh token for a new access token. * **Note**: this func can run on client side. Only use private slas when the slas client secret is secured.