From 31dd0a6082abf003d427ac1affc2107565308444 Mon Sep 17 00:00:00 2001 From: yunakim714 Date: Thu, 12 Sep 2024 16:12:48 -0400 Subject: [PATCH] separate idplogin helper into separate file --- src/static/helpers/IDPLoginHelper.test.ts | 87 +++++++++++++++++++++++ src/static/helpers/IDPLoginHelper.ts | 81 +++++++++++++++++++++ src/static/helpers/slasHelper.test.ts | 18 ----- src/static/helpers/slasHelper.ts | 67 ----------------- 4 files changed, 168 insertions(+), 85 deletions(-) create mode 100644 src/static/helpers/IDPLoginHelper.test.ts create mode 100644 src/static/helpers/IDPLoginHelper.ts diff --git a/src/static/helpers/IDPLoginHelper.test.ts b/src/static/helpers/IDPLoginHelper.test.ts new file mode 100644 index 0000000..8b1e6e7 --- /dev/null +++ b/src/static/helpers/IDPLoginHelper.test.ts @@ -0,0 +1,87 @@ +/** + * @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, TokenResponse} from '../../lib/shopperLogin'; +import loginIDPUser from './IDPLoginHelper'; +import ResponseError from '../responseError'; + +const credentialsPublic = {}; + +const expectedTokenResponse: TokenResponse = { + access_token: 'access_token', + id_token: 'id_token', + refresh_token: 'refresh_token', + expires_in: 0, + refresh_token_expires_in: 0, + token_type: 'token_type', + usid: 'usid', + customer_id: 'customer_id', + enc_user_id: 'enc_user_id', + idp_access_token: 'idp', +}; + +const parameters = { + accessToken: 'access_token', + redirectURI: 'redirect_uri', + refreshToken: 'refresh_token', + usid: 'usid', + hint: 'hint', + dnt: false, +}; + +const url = + 'https://localhost:3000/callback?usid=048adcfb-aa93-4978-be9e-09cb569fdcb9&code=J2lHm0cgXmnXpwDhjhLoyLJBoUAlBfxDY-AhjqGMC-o'; + +const authenticateCustomerMock = jest.fn(() => ({url})); + +const getAccessTokenMock = jest.fn(() => expectedTokenResponse); +const logoutCustomerMock = jest.fn(() => expectedTokenResponse); +const generateCodeChallengeMock = jest.fn(() => 'code_challenge'); + +const createMockSlasClient = () => + ({ + clientConfig: { + parameters: { + shortCode: 'short_code', + organizationId: 'organization_id', + clientId: 'client_id', + siteId: 'site_id', + }, + }, + authenticateCustomer: authenticateCustomerMock, + getAccessToken: getAccessTokenMock, + logoutCustomer: logoutCustomerMock, + generateCodeChallenge: generateCodeChallengeMock, + } as unknown as ShopperLogin<{ + shortCode: string; + organizationId: string; + clientId: string; + siteId: string; + }>); + +describe('Social login user flow', () => { + test('loginIDPUser does not stop when authorizeCustomer returns 303', async () => { + // slasClient is copied and tries to make an actual API call + const mockSlasClient = createMockSlasClient(); + const {shortCode, organizationId} = mockSlasClient.clientConfig.parameters; + + // Mock authorizeCustomer + nock(`https://${shortCode}.api.commercecloud.salesforce.com`) + .get(`/shopper/auth/v1/organizations/${organizationId}/oauth2/authorize`) + .query(true) + .reply(303, {message: 'Oh yes!'}); + + await expect( + loginIDPUser(mockSlasClient, credentialsPublic, parameters) + ).resolves.not.toThrow(ResponseError); + }); +}); diff --git a/src/static/helpers/IDPLoginHelper.ts b/src/static/helpers/IDPLoginHelper.ts new file mode 100644 index 0000000..7d88b88 --- /dev/null +++ b/src/static/helpers/IDPLoginHelper.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022, 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 {stringToBase64, createCodeVerifier, authorize} from './slasHelper'; +import { + ShopperLogin, + TokenRequest, + TokenResponse, +} from '../../lib/shopperLogin'; + +/** + * A single function to execute the ShopperLogin External IDP Login with proof key for code exchange flow as described in the [API documentation](https://developer.salesforce.com/docs/commerce/commerce-api/references?meta=shopper-login:Summary). + * **Note**: this func can run on client side. Only use private slas when the slas client secret is secured. + * @param slasClient a configured instance of the ShopperLogin SDK client. + * @param credentials - the 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.redirectURI - Per OAuth standard, a valid app route. Must be listed in your SLAS configuration. On server, this will not be actually called. On browser, this will be called, but ignored. + * @param parameters.hint - Name of an identity provider (IDP) to redirect to + * @param parameters.usid? - Unique Shopper Identifier to enable personalization. + * @param parameters.dnt? - Optional parameter to enable Do Not Track (DNT) for the user. + * @returns TokenResponse + */ +async function loginIDPUser( + slasClient: ShopperLogin<{ + shortCode: string; + organizationId: string; + clientId: string; + siteId: string; + }>, + credentials: { + clientSecret?: string; + }, + parameters: { + redirectURI: string; + hint: string; + usid?: string; + dnt?: boolean; + } +): Promise { + const codeVerifier = createCodeVerifier(); + + const authResponse = await authorize(slasClient, codeVerifier, { + redirectURI: parameters.redirectURI, + hint: parameters.hint, + ...(parameters.usid && {usid: parameters.usid}), + }); + + const tokenBody: TokenRequest = { + client_id: slasClient.clientConfig.parameters.clientId, + channel_id: slasClient.clientConfig.parameters.siteId, + code: authResponse.code, + code_verifier: codeVerifier, + grant_type: 'authorization_code_pkce', + redirect_uri: parameters.redirectURI, + usid: authResponse.usid, + ...(parameters.dnt !== undefined && {dnt: parameters.dnt.toString()}), + }; + + // Using private client + if (credentials.clientSecret) { + const authHeaderIdSecret = `Basic ${stringToBase64( + `${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` + )}`; + + const optionsToken = { + headers: { + Authorization: authHeaderIdSecret, + }, + body: tokenBody, + }; + return slasClient.getAccessToken(optionsToken); + } + + return slasClient.getAccessToken({body: tokenBody}); +} + +export default loginIDPUser; diff --git a/src/static/helpers/slasHelper.test.ts b/src/static/helpers/slasHelper.test.ts index 7e9128b..e84251b 100644 --- a/src/static/helpers/slasHelper.test.ts +++ b/src/static/helpers/slasHelper.test.ts @@ -435,24 +435,6 @@ describe('Registered B2C user flow', () => { }); }); -describe('Social login user flow', () => { - test('loginIDPUser does not stop when authorizeCustomer returns 303', async () => { - // slasClient is copied and tries to make an actual API call - const mockSlasClient = createMockSlasClient(); - const {shortCode, organizationId} = mockSlasClient.clientConfig.parameters; - - // Mock authorizeCustomer - nock(`https://${shortCode}.api.commercecloud.salesforce.com`) - .get(`/shopper/auth/v1/organizations/${organizationId}/oauth2/authorize`) - .query(true) - .reply(303, {message: 'Oh yes!'}); - - await expect( - slasHelper.loginIDPUser(mockSlasClient, parameters) - ).resolves.not.toThrow(ResponseError); - }); -}); - 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 13e600c..fe12b1a 100644 --- a/src/static/helpers/slasHelper.ts +++ b/src/static/helpers/slasHelper.ts @@ -359,73 +359,6 @@ export async function loginRegisteredUserB2C( return slasClient.getAccessToken({body: tokenBody}); } -/** - * A single function to execute the ShopperLogin External IDP Login with proof key for code exchange flow as described in the [API documentation](https://developer.salesforce.com/docs/commerce/commerce-api/references?meta=shopper-login:Summary). - * **Note**: this func can run on client side. Only use private slas when the slas client secret is secured. - * @param slasClient a configured instance of the ShopperLogin SDK client. - * @param credentials - the 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.redirectURI - Per OAuth standard, a valid app route. Must be listed in your SLAS configuration. On server, this will not be actually called. On browser, this will be called, but ignored. - * @param parameters.hint - Name of an identity provider (IDP) to redirect to - * @param parameters.usid? - Unique Shopper Identifier to enable personalization. - * @param parameters.dnt? - Optional parameter to enable Do Not Track (DNT) for the user. - * @returns TokenResponse - */ -export async function loginIDPUser( - slasClient: ShopperLogin<{ - shortCode: string; - organizationId: string; - clientId: string; - siteId: string; - }>, - credentials: { - clientSecret?: string; - }, - parameters: { - redirectURI: string; - hint: string; - usid?: string; - dnt?: boolean; - } -): Promise { - const codeVerifier = createCodeVerifier(); - - const authResponse = await authorize(slasClient, codeVerifier, { - redirectURI: parameters.redirectURI, - hint: parameters.hint, - ...(parameters.usid && {usid: parameters.usid}), - }); - - const tokenBody: TokenRequest = { - client_id: slasClient.clientConfig.parameters.clientId, - channel_id: slasClient.clientConfig.parameters.siteId, - code: authResponse.code, - code_verifier: codeVerifier, - grant_type: 'authorization_code_pkce', - redirect_uri: parameters.redirectURI, - usid: authResponse.usid, - ...(parameters.dnt !== undefined && {dnt: parameters.dnt.toString()}), - }; - - // Using private client - if (credentials.clientSecret) { - const authHeaderIdSecret = `Basic ${stringToBase64( - `${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` - )}`; - - const optionsToken = { - headers: { - Authorization: authHeaderIdSecret, - }, - body: tokenBody, - }; - return slasClient.getAccessToken(optionsToken); - } - - return slasClient.getAccessToken({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.