From 83224737beb42e50aba4b6e6e5157e41757d8908 Mon Sep 17 00:00:00 2001 From: yunakim714 Date: Tue, 24 Sep 2024 20:12:41 -0400 Subject: [PATCH] move helpers toslashelper file --- .eslintrc.json | 6 + src/static/helpers/IDPLoginHelper.test.ts | 136 ---------------------- src/static/helpers/IDPLoginHelper.ts | 90 -------------- src/static/helpers/index.ts | 1 - src/static/helpers/slasHelper.test.ts | 55 +++++++++ src/static/helpers/slasHelper.ts | 76 ++++++++++++ 6 files changed, 137 insertions(+), 227 deletions(-) delete mode 100644 src/static/helpers/IDPLoginHelper.test.ts delete mode 100644 src/static/helpers/IDPLoginHelper.ts 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/IDPLoginHelper.test.ts b/src/static/helpers/IDPLoginHelper.test.ts deleted file mode 100644 index f4ccd171..00000000 --- a/src/static/helpers/IDPLoginHelper.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -/** - * @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 {stringToBase64} from './slasHelper'; -import ResponseError from '../responseError'; - -const credentialsPublic = {}; - -const credentialsPrivate = { - username: 'shopper_user_id', - password: 'shopper_password', - clientSecret: 'slas_private_secret', -}; - -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; - }>); - -beforeEach(() => { - jest.clearAllMocks(); - nock.cleanAll(); -}); - -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); - }); - - test('generates an access token using slas private client', async () => { - 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, {response_body: 'response_body'}, {location: url}); - - const accessToken = await loginIDPUser( - mockSlasClient, - credentialsPrivate, - parameters - ); - - const expectedReqOptions = { - headers: { - Authorization: `Basic ${stringToBase64( - `client_id:${credentialsPrivate.clientSecret}` - )}`, - }, - body: { - grant_type: 'authorization_code', - redirect_uri: 'redirect_uri', - client_id: 'client_id', - channel_id: 'site_id', - usid: '048adcfb-aa93-4978-be9e-09cb569fdcb9', - code_verifier: expect.stringMatching(/./) as string, - code: 'J2lHm0cgXmnXpwDhjhLoyLJBoUAlBfxDY-AhjqGMC-o', - dnt: 'false', - }, - }; - expect(getAccessTokenMock).toBeCalledWith(expectedReqOptions); - expect(accessToken).toBe(expectedTokenResponse); - }); -}); diff --git a/src/static/helpers/IDPLoginHelper.ts b/src/static/helpers/IDPLoginHelper.ts deleted file mode 100644 index 4479de89..00000000 --- a/src/static/helpers/IDPLoginHelper.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 privateClient = !!credentials.clientSecret; - - const authResponse = await authorize( - slasClient, - codeVerifier, - { - redirectURI: parameters.redirectURI, - hint: parameters.hint, - ...(parameters.usid && {usid: parameters.usid}), - }, - privateClient - ); - - const tokenBody: TokenRequest = { - client_id: slasClient.clientConfig.parameters.clientId, - channel_id: slasClient.clientConfig.parameters.siteId, - code: authResponse.code, - code_verifier: codeVerifier, - grant_type: privateClient - ? 'authorization_code' - : '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/index.ts b/src/static/helpers/index.ts index 5b65e933..f72986bb 100644 --- a/src/static/helpers/index.ts +++ b/src/static/helpers/index.ts @@ -8,7 +8,6 @@ // Doing so may lead to circular dependencies or duplicate exports (due to rollup mangling the types) export * from './environment'; export * from './slasHelper'; -export * from './IDPLoginHelper'; export * from './types'; export * from './customApi'; export * from './fetchHelper'; diff --git a/src/static/helpers/slasHelper.test.ts b/src/static/helpers/slasHelper.test.ts index e84251b2..990702fc 100644 --- a/src/static/helpers/slasHelper.test.ts +++ b/src/static/helpers/slasHelper.test.ts @@ -435,6 +435,61 @@ 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); + }); + + test('generates an access token using slas private client', async () => { + 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, {response_body: 'response_body'}, {location: url}); + + const accessToken = await slasHelper.loginIDPUser( + mockSlasClient, + credentialsPrivate, + parameters + ); + + const expectedReqOptions = { + headers: { + Authorization: `Basic ${stringToBase64( + `client_id:${credentialsPrivate.clientSecret}` + )}`, + }, + body: { + grant_type: 'authorization_code', + redirect_uri: 'redirect_uri', + client_id: 'client_id', + channel_id: 'site_id', + usid: '048adcfb-aa93-4978-be9e-09cb569fdcb9', + code_verifier: expect.stringMatching(/./) as string, + code: 'J2lHm0cgXmnXpwDhjhLoyLJBoUAlBfxDY-AhjqGMC-o', + dnt: 'false', + }, + }; + expect(getAccessTokenMock).toBeCalledWith(expectedReqOptions); + expect(accessToken).toBe(expectedTokenResponse); + }); +}); + 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 400ad62e..246c5f3f 100644 --- a/src/static/helpers/slasHelper.ts +++ b/src/static/helpers/slasHelper.ts @@ -360,6 +360,82 @@ 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 privateClient = !!credentials.clientSecret; + + const authResponse = await authorize( + slasClient, + codeVerifier, + { + redirectURI: parameters.redirectURI, + hint: parameters.hint, + ...(parameters.usid && {usid: parameters.usid}), + }, + privateClient + ); + + const tokenBody: TokenRequest = { + client_id: slasClient.clientConfig.parameters.clientId, + channel_id: slasClient.clientConfig.parameters.siteId, + code: authResponse.code, + code_verifier: codeVerifier, + grant_type: privateClient + ? 'authorization_code' + : '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.