From 22ff065d530a1f8608d74e5cf088e6c773554b27 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Fri, 19 Jan 2024 17:36:09 -0800 Subject: [PATCH 1/7] allow slas private client --- src/static/helpers/slasHelper.ts | 103 ++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/src/static/helpers/slasHelper.ts b/src/static/helpers/slasHelper.ts index 6ca7d19..f0ae1ee 100644 --- a/src/static/helpers/slasHelper.ts +++ b/src/static/helpers/slasHelper.ts @@ -16,6 +16,10 @@ import { } from '../../lib/shopperLogin'; import ResponseError from '../responseError'; +const warningSlasSecretMsg = + 'This function can run on client-side. You are potentially exposing SLAS secret on browser. Make sure to keep it safe and secure!'; + +const onClient = typeof window !== 'undefined'; export const stringToBase64 = isBrowser ? btoa : (unencoded: string): string => Buffer.from(unencoded).toString('base64'); @@ -147,6 +151,51 @@ export async function authorize( return {url: redirectUrlString, ...getCodeAndUsidFromUrl(redirectUrlString)}; } +/** + * A single function to execute the ShopperLogin Private Client Guest Login as described in the [API documentation](https://developer.salesforce.com/docs/commerce/commerce-api/guide/slas-private-client.html). + * + * @param slasClient - a configured instance of the ShopperLogin SDK client + * @param credentials - client secret used for authentication + * @param credentials.clientSecret - secret associated with client ID + * @param usid? - optional Unique Shopper Identifier to enable personalization + * @returns TokenResponse + */ +export async function loginGuestUserPrivate( + slasClient: ShopperLogin<{ + shortCode: string; + organizationId: string; + clientId: string; + siteId: string; + }>, + parameters: { + redirectURI: string; + usid?: string; + }, + credentials: { + clientSecret: string; + } +): Promise { + if (onClient) { + // eslint-ignore-next-line we intentionally have warning here + console.warn(warningSlasSecretMsg); + } + const authorization = `Basic ${stringToBase64( + `${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` + )}`; + + const options = { + headers: { + Authorization: authorization, + }, + body: { + grant_type: 'client_credentials', + ...(parameters.usid && {usid: parameters.usid}), + }, + }; + + return slasClient.getAccessToken(options); +} + /** * A single function to execute the ShopperLogin Public Client Guest 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). * @param slasClient a configured instance of the ShopperLogin SDK client. @@ -194,6 +243,7 @@ export async function loginGuestUser( * @param credentials - the id and password to login with. * @param credentials.username - the id of the user to login with. * @param credentials.password - the password of the user 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.usid? - Unique Shopper Identifier to enable personalization. @@ -209,6 +259,7 @@ export async function loginRegisteredUserB2C( credentials: { username: string; password: string; + clientSecret: string; }, parameters: { redirectURI: string; @@ -260,7 +311,34 @@ export async function loginRegisteredUserB2C( } const authResponse = getCodeAndUsidFromUrl(redirectUrlString); + // using slas private client + if (credentials.clientSecret) { + if (onClient) { + // eslint-ignore-next-line we intentionally have warning here + console.warn(warningSlasSecretMsg); + } + + const authHeaderIdSecret = `Basic ${stringToBase64( + `${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` + )}`; + + const optionsToken = { + headers: { + Authorization: authHeaderIdSecret, + }, + body: { + grant_type: 'authorization_code_pkce', + code_verifier: codeVerifier, + code: authResponse.code, + client_id: slasClient.clientConfig.parameters.clientId, + redirect_uri: parameters.redirectURI, + ...(parameters.usid && {usid: parameters.usid}), + }, + }; + return slasClient.getAccessToken(optionsToken); + } + // default is to use slas public client const tokenBody = { client_id: slasClient.clientConfig.parameters.clientId, channel_id: slasClient.clientConfig.parameters.siteId, @@ -280,6 +358,8 @@ export async function loginRegisteredUserB2C( * @param slasClient a configured instance of the ShopperLogin SDK client. * @param parameters - parameters to pass in the API calls. * @param parameters.refreshToken - a valid refresh token to exchange for a new access token (and refresh token). + * @param parameters.refreshToken - a valid refresh token to exchange for a new access token (and refresh token). + * @param credentials.clientSecret - secret associated with client ID * @returns TokenResponse */ export function refreshAccessToken( @@ -289,8 +369,29 @@ export function refreshAccessToken( clientId: string; siteId: string; }>, - parameters: {refreshToken: string} + parameters: {refreshToken: string}, + credentials: {clientSecret: string} ): Promise { + if (credentials.clientSecret) { + if (onClient) { + // eslint-ignore-next-line we intentionally have warning here + console.warn(warningSlasSecretMsg); + } + const authorization = `Basic ${stringToBase64( + `${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` + )}`; + const options = { + headers: { + Authorization: authorization, + }, + body: { + grant_type: 'refresh_token', + refresh_token: parameters.refreshToken, + }, + }; + return slasClient.getAccessToken(options); + } + const body = { grant_type: 'refresh_token', refresh_token: parameters.refreshToken, From 59763ffdffbf752c8d294226f4b5c1a157a552bd Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Fri, 19 Jan 2024 18:07:58 -0800 Subject: [PATCH 2/7] allow slas private client --- src/static/helpers/slasHelper.test.ts | 52 +++++++++++++++++++++------ src/static/helpers/slasHelper.ts | 18 ++++++---- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/static/helpers/slasHelper.test.ts b/src/static/helpers/slasHelper.test.ts index c630e34..11ed48c 100644 --- a/src/static/helpers/slasHelper.test.ts +++ b/src/static/helpers/slasHelper.test.ts @@ -22,6 +22,12 @@ const credentials = { password: 'shopper_password', }; +const credentialsPrivate = { + username: 'shopper_user_id', + password: 'shopper_password', + clientSecret: 'slas_private_secret', +}; + const expectedTokenResponse: TokenResponse = { access_token: 'access_token', id_token: 'id_token', @@ -204,18 +210,18 @@ test('throws error on 400 response', async () => { }); describe('Guest user flow', () => { - const expectedTokenBody = { - body: { - client_id: 'client_id', - channel_id: 'site_id', - code: 'J2lHm0cgXmnXpwDhjhLoyLJBoUAlBfxDY-AhjqGMC-o', - code_verifier: expect.stringMatching(/./) as string, - grant_type: 'authorization_code_pkce', - redirect_uri: 'redirect_uri', - usid: '048adcfb-aa93-4978-be9e-09cb569fdcb9', - }, - }; test('retrieves usid and code from location header and generates an access token', async () => { + const expectedTokenBody = { + body: { + client_id: 'client_id', + channel_id: 'site_id', + code: 'J2lHm0cgXmnXpwDhjhLoyLJBoUAlBfxDY-AhjqGMC-o', + code_verifier: expect.stringMatching(/./) as string, + grant_type: 'authorization_code_pkce', + redirect_uri: 'redirect_uri', + usid: '048adcfb-aa93-4978-be9e-09cb569fdcb9', + }, + }; const mockSlasClient = createMockSlasClient(); const {shortCode, organizationId} = mockSlasClient.clientConfig.parameters; @@ -231,6 +237,30 @@ describe('Guest user flow', () => { expect(getAccessTokenMock).toBeCalledWith(expectedTokenBody); expect(accessToken).toBe(expectedTokenResponse); }); + + test('generates an access token using slas private client', async () => { + const mockSlasClient = createMockSlasClient(); + + const accessToken = await slasHelper.loginGuestUserPrivate( + mockSlasClient, + parameters, + credentialsPrivate + ); + + const expectedReqOptions = { + headers: { + Authorization: `Basic ${stringToBase64( + `client_id:${credentialsPrivate.clientSecret}` + )}`, + }, + body: { + grant_type: 'client_credentials', + usid: 'usid', + }, + }; + expect(getAccessTokenMock).toBeCalledWith(expectedReqOptions); + expect(accessToken).toBe(expectedTokenResponse); + }); }); describe('Registered B2C user flow', () => { const expectedTokenBody = { diff --git a/src/static/helpers/slasHelper.ts b/src/static/helpers/slasHelper.ts index f0ae1ee..f2724ef 100644 --- a/src/static/helpers/slasHelper.ts +++ b/src/static/helpers/slasHelper.ts @@ -176,7 +176,8 @@ export async function loginGuestUserPrivate( } ): Promise { if (onClient) { - // eslint-ignore-next-line we intentionally have warning here + // we intentionally have warning here + // eslint-disable-next-line console.warn(warningSlasSecretMsg); } const authorization = `Basic ${stringToBase64( @@ -240,7 +241,7 @@ export async function loginGuestUser( /** * A single function to execute the ShopperLogin Public Client Registered User B2C 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). * @param slasClient a configured instance of the ShopperLogin SDK client. - * @param credentials - the id and password to login with. + * @param credentials - the id and password and clientSecret (if applicable) to login with. * @param credentials.username - the id of the user to login with. * @param credentials.password - the password of the user to login with. * @param credentials.clientSecret - secret associated with client ID @@ -259,7 +260,7 @@ export async function loginRegisteredUserB2C( credentials: { username: string; password: string; - clientSecret: string; + clientSecret?: string; }, parameters: { redirectURI: string; @@ -314,7 +315,8 @@ export async function loginRegisteredUserB2C( // using slas private client if (credentials.clientSecret) { if (onClient) { - // eslint-ignore-next-line we intentionally have warning here + // we intentionally have warning here + // eslint-disable-next-line console.warn(warningSlasSecretMsg); } @@ -359,6 +361,7 @@ export async function loginRegisteredUserB2C( * @param parameters - parameters to pass in the API calls. * @param parameters.refreshToken - a valid refresh token to exchange for a new access token (and refresh token). * @param parameters.refreshToken - a valid refresh token to exchange for a new access token (and refresh token). + * @param credentials - the clientSecret (if applicable) to login with. * @param credentials.clientSecret - secret associated with client ID * @returns TokenResponse */ @@ -370,11 +373,12 @@ export function refreshAccessToken( siteId: string; }>, parameters: {refreshToken: string}, - credentials: {clientSecret: string} + credentials?: {clientSecret?: string} ): Promise { - if (credentials.clientSecret) { + if (credentials && credentials.clientSecret) { if (onClient) { - // eslint-ignore-next-line we intentionally have warning here + // we intentionally have warning here + // eslint-disable-next-line console.warn(warningSlasSecretMsg); } const authorization = `Basic ${stringToBase64( From a9d6dbef8c55c253631bddc4af17581a85a618f1 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 22 Jan 2024 14:00:06 -0800 Subject: [PATCH 3/7] add unit tests --- src/static/helpers/slasHelper.test.ts | 103 +++++++++++++++++++++++--- src/static/helpers/slasHelper.ts | 16 ++-- 2 files changed, 101 insertions(+), 18 deletions(-) diff --git a/src/static/helpers/slasHelper.test.ts b/src/static/helpers/slasHelper.test.ts index 11ed48c..c6f520a 100644 --- a/src/static/helpers/slasHelper.test.ts +++ b/src/static/helpers/slasHelper.test.ts @@ -276,7 +276,7 @@ describe('Registered B2C user flow', () => { }, }; - test('uses code challenge and authorization header to generate auth code', async () => { + test('uses code challenge and authorization header to generate auth code with slas public client', async () => { // slasClient is copied and tries to make an actual API call const mockSlasClient = createMockSlasClient(); const {shortCode, organizationId} = mockSlasClient.clientConfig.parameters; @@ -295,6 +295,40 @@ describe('Registered B2C user flow', () => { expect(getAccessTokenMock).toBeCalledWith(expectedTokenBody); }); + test('uses code challenge and authorization header to generate auth code with slas private client', async () => { + const expectedReqOptions = { + headers: { + Authorization: `Basic ${stringToBase64( + `client_id:${credentialsPrivate.clientSecret}` + )}`, + }, + body: { + client_id: 'client_id', + code: 'J2lHm0cgXmnXpwDhjhLoyLJBoUAlBfxDY-AhjqGMC-o', + code_verifier: expect.stringMatching(/./) as string, + grant_type: 'authorization_code_pkce', + redirect_uri: 'redirect_uri', + usid: '048adcfb-aa93-4978-be9e-09cb569fdcb9', + }, + }; + // slasClient is copied and tries to make an actual API call + const mockSlasClient = createMockSlasClient(); + const {shortCode, organizationId} = mockSlasClient.clientConfig.parameters; + + // Mocking slasCopy.authenticateCustomer + nock(`https://${shortCode}.api.commercecloud.salesforce.com`) + .post(`/shopper/auth/v1/organizations/${organizationId}/oauth2/login`) + .reply(303, {response_body: 'response_body'}, {location: url}); + + await slasHelper.loginRegisteredUserB2C( + mockSlasClient, + credentialsPrivate, + parameters + ); + + expect(getAccessTokenMock).toBeCalledWith(expectedReqOptions); + }); + test('loginRegisteredUserB2C stops when authenticateCustomer returns 400', async () => { // slasClient is copied and tries to make an actual API call const mockSlasClient = createMockSlasClient(); @@ -351,7 +385,7 @@ describe('Registered B2C user flow', () => { ).resolves.not.toThrow(ResponseError); }); - test('uses auth code and code verifier to generate JWT', async () => { + test('uses auth code and code verifier to generate JWT with public client', async () => { // slasClient is copied and tries to make an actual API call const mockSlasClient = createMockSlasClient(); const {shortCode, organizationId} = mockSlasClient.clientConfig.parameters; @@ -370,16 +404,15 @@ describe('Registered B2C user flow', () => { }); describe('Refresh Token', () => { - const expectedBody = { - body: { - client_id: 'client_id', - channel_id: 'site_id', - grant_type: 'refresh_token', - refresh_token: 'refresh_token', - }, - }; - - test('refreshes the token', () => { + test('refreshes the token with slas public client', () => { + const expectedBody = { + body: { + client_id: 'client_id', + channel_id: 'site_id', + grant_type: 'refresh_token', + refresh_token: 'refresh_token', + }, + }; const token = slasHelper.refreshAccessToken( createMockSlasClient(), parameters @@ -387,6 +420,52 @@ describe('Refresh Token', () => { expect(getAccessTokenMock).toBeCalledWith(expectedBody); expect(token).toStrictEqual(expectedTokenResponse); }); + + test('refreshes the token with slas private client', () => { + const expectedReqOpts = { + headers: { + Authorization: `Basic ${stringToBase64( + `client_id:${credentialsPrivate.clientSecret}` + )}`, + }, + body: { + grant_type: 'refresh_token', + refresh_token: parameters.refreshToken, + }, + }; + const token = slasHelper.refreshAccessToken( + createMockSlasClient(), + parameters, + { + clientSecret: credentialsPrivate.clientSecret, + } + ); + expect(getAccessTokenMock).toBeCalledWith(expectedReqOpts); + expect(token).toStrictEqual(expectedTokenResponse); + }); + + test('refreshes the token warn users about using slas private client on', () => { + const expectedReqOpts = { + headers: { + Authorization: `Basic ${stringToBase64( + `client_id:${credentialsPrivate.clientSecret}` + )}`, + }, + body: { + grant_type: 'refresh_token', + refresh_token: parameters.refreshToken, + }, + }; + const token = slasHelper.refreshAccessToken( + createMockSlasClient(), + parameters, + { + clientSecret: credentialsPrivate.clientSecret, + } + ); + expect(getAccessTokenMock).toBeCalledWith(expectedReqOpts); + expect(token).toStrictEqual(expectedTokenResponse); + }); }); describe('Logout', () => { diff --git a/src/static/helpers/slasHelper.ts b/src/static/helpers/slasHelper.ts index f2724ef..622ec91 100644 --- a/src/static/helpers/slasHelper.ts +++ b/src/static/helpers/slasHelper.ts @@ -19,7 +19,6 @@ import ResponseError from '../responseError'; const warningSlasSecretMsg = 'This function can run on client-side. You are potentially exposing SLAS secret on browser. Make sure to keep it safe and secure!'; -const onClient = typeof window !== 'undefined'; export const stringToBase64 = isBrowser ? btoa : (unencoded: string): string => Buffer.from(unencoded).toString('base64'); @@ -157,7 +156,9 @@ export async function authorize( * @param slasClient - a configured instance of the ShopperLogin SDK client * @param credentials - client secret used for authentication * @param credentials.clientSecret - secret associated with client ID - * @param usid? - optional Unique Shopper Identifier to enable personalization + * @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.usid? - Unique Shopper Identifier to enable personalization. * @returns TokenResponse */ export async function loginGuestUserPrivate( @@ -175,7 +176,8 @@ export async function loginGuestUserPrivate( clientSecret: string; } ): Promise { - if (onClient) { + /* istanbul ignore next */ + if (isBrowser) { // we intentionally have warning here // eslint-disable-next-line console.warn(warningSlasSecretMsg); @@ -314,7 +316,8 @@ export async function loginRegisteredUserB2C( const authResponse = getCodeAndUsidFromUrl(redirectUrlString); // using slas private client if (credentials.clientSecret) { - if (onClient) { + /* istanbul ignore next */ + if (isBrowser) { // we intentionally have warning here // eslint-disable-next-line console.warn(warningSlasSecretMsg); @@ -334,7 +337,7 @@ export async function loginRegisteredUserB2C( code: authResponse.code, client_id: slasClient.clientConfig.parameters.clientId, redirect_uri: parameters.redirectURI, - ...(parameters.usid && {usid: parameters.usid}), + usid: authResponse.usid, }, }; return slasClient.getAccessToken(optionsToken); @@ -376,7 +379,8 @@ export function refreshAccessToken( credentials?: {clientSecret?: string} ): Promise { if (credentials && credentials.clientSecret) { - if (onClient) { + /* istanbul ignore next */ + if (isBrowser) { // we intentionally have warning here // eslint-disable-next-line console.warn(warningSlasSecretMsg); From ceb7e5d87400e9a4e1eefd3dc56309155000d355 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Wed, 24 Jan 2024 12:30:36 -0800 Subject: [PATCH 4/7] PR feedback --- README.md | 11 ++++++ src/static/helpers/slasHelper.test.ts | 27 ++----------- src/static/helpers/slasHelper.ts | 55 ++++++++++----------------- 3 files changed, 35 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index dd704c3..dfa3bba 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,11 @@ npm install commerce-sdk-isomorphic ### Configure the Isomorphic SDK +**Note** +You will see warning about using private helpers like this +'This function can run on client-side. You are potentially exposing SLAS secret on browser. Make sure to keep it safe and secure!';. +The reason is because commerce-sdk-isomophic runs on both server and client. This warning is to remind developers to always keep their secret safe +and avoid expose it to client side ```javascript /** @@ -59,6 +64,12 @@ const {access_token, refresh_token} = await helpers.loginGuestUser( {redirectURI: `${config.proxy}/callback`} // Callback URL must be configured in SLAS Admin ); +// Execute Private Client OAuth with PKCE to acquire guest tokens +const {access_token, refresh_token} = await helpers.loginGuestUserPrivate( + shopperLogin, + {}, {clientSecret: 'slas-client-secret'} +); + const shopperSearch = new ShopperSearch({ ...config, headers: {authorization: `Bearer ${access_token}`}, diff --git a/src/static/helpers/slasHelper.test.ts b/src/static/helpers/slasHelper.test.ts index c6f520a..4027166 100644 --- a/src/static/helpers/slasHelper.test.ts +++ b/src/static/helpers/slasHelper.test.ts @@ -308,6 +308,8 @@ describe('Registered B2C user flow', () => { code_verifier: expect.stringMatching(/./) as string, grant_type: 'authorization_code_pkce', redirect_uri: 'redirect_uri', + channel_id: 'site_id', + organizationId: 'organization_id', usid: '048adcfb-aa93-4978-be9e-09cb569fdcb9', }, }; @@ -430,29 +432,8 @@ describe('Refresh Token', () => { }, body: { grant_type: 'refresh_token', - refresh_token: parameters.refreshToken, - }, - }; - const token = slasHelper.refreshAccessToken( - createMockSlasClient(), - parameters, - { - clientSecret: credentialsPrivate.clientSecret, - } - ); - expect(getAccessTokenMock).toBeCalledWith(expectedReqOpts); - expect(token).toStrictEqual(expectedTokenResponse); - }); - - test('refreshes the token warn users about using slas private client on', () => { - const expectedReqOpts = { - headers: { - Authorization: `Basic ${stringToBase64( - `client_id:${credentialsPrivate.clientSecret}` - )}`, - }, - body: { - grant_type: 'refresh_token', + client_id: 'client_id', + channel_id: 'site_id', refresh_token: parameters.refreshToken, }, }; diff --git a/src/static/helpers/slasHelper.ts b/src/static/helpers/slasHelper.ts index 622ec91..051390b 100644 --- a/src/static/helpers/slasHelper.ts +++ b/src/static/helpers/slasHelper.ts @@ -157,7 +157,6 @@ export async function authorize( * @param credentials - client secret used for authentication * @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.usid? - Unique Shopper Identifier to enable personalization. * @returns TokenResponse */ @@ -169,7 +168,6 @@ export async function loginGuestUserPrivate( siteId: string; }>, parameters: { - redirectURI: string; usid?: string; }, credentials: { @@ -246,7 +244,7 @@ export async function loginGuestUser( * @param credentials - the id and password and clientSecret (if applicable) to login with. * @param credentials.username - the id of the user to login with. * @param credentials.password - the password of the user to login with. - * @param credentials.clientSecret - secret associated with client ID + * @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.usid? - Unique Shopper Identifier to enable personalization. @@ -314,6 +312,16 @@ export async function loginRegisteredUserB2C( } const authResponse = getCodeAndUsidFromUrl(redirectUrlString); + const tokenBody = { + client_id: slasClient.clientConfig.parameters.clientId, + channel_id: slasClient.clientConfig.parameters.siteId, + code: authResponse.code, + code_verifier: codeVerifier, + grant_type: 'authorization_code_pkce', + organizationId: slasClient.clientConfig.parameters.organizationId, + redirect_uri: parameters.redirectURI, + usid: authResponse.usid, + }; // using slas private client if (credentials.clientSecret) { /* istanbul ignore next */ @@ -331,30 +339,11 @@ export async function loginRegisteredUserB2C( headers: { Authorization: authHeaderIdSecret, }, - body: { - grant_type: 'authorization_code_pkce', - code_verifier: codeVerifier, - code: authResponse.code, - client_id: slasClient.clientConfig.parameters.clientId, - redirect_uri: parameters.redirectURI, - usid: authResponse.usid, - }, + body: tokenBody, }; return slasClient.getAccessToken(optionsToken); } - // default is to use slas public client - const tokenBody = { - client_id: slasClient.clientConfig.parameters.clientId, - channel_id: slasClient.clientConfig.parameters.siteId, - code: authResponse.code, - code_verifier: codeVerifier, - grant_type: 'authorization_code_pkce', - organizationId: slasClient.clientConfig.parameters.organizationId, - redirect_uri: parameters.redirectURI, - usid: authResponse.usid, - }; - return slasClient.getAccessToken({body: tokenBody}); } @@ -363,7 +352,6 @@ export async function loginRegisteredUserB2C( * @param slasClient a configured instance of the ShopperLogin SDK client. * @param parameters - parameters to pass in the API calls. * @param parameters.refreshToken - a valid refresh token to exchange for a new access token (and refresh token). - * @param parameters.refreshToken - a valid refresh token to exchange for a new access token (and refresh token). * @param credentials - the clientSecret (if applicable) to login with. * @param credentials.clientSecret - secret associated with client ID * @returns TokenResponse @@ -378,6 +366,13 @@ export function refreshAccessToken( parameters: {refreshToken: string}, credentials?: {clientSecret?: string} ): Promise { + const body = { + grant_type: 'refresh_token', + refresh_token: parameters.refreshToken, + client_id: slasClient.clientConfig.parameters.clientId, + channel_id: slasClient.clientConfig.parameters.siteId, + }; + if (credentials && credentials.clientSecret) { /* istanbul ignore next */ if (isBrowser) { @@ -392,21 +387,11 @@ export function refreshAccessToken( headers: { Authorization: authorization, }, - body: { - grant_type: 'refresh_token', - refresh_token: parameters.refreshToken, - }, + body, }; return slasClient.getAccessToken(options); } - const body = { - grant_type: 'refresh_token', - refresh_token: parameters.refreshToken, - client_id: slasClient.clientConfig.parameters.clientId, - channel_id: slasClient.clientConfig.parameters.siteId, - }; - return slasClient.getAccessToken({body}); } From 9665ffea0ea1df0fc20ae0f82b73d067c18c3baf Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Thu, 25 Jan 2024 13:59:24 -0800 Subject: [PATCH 5/7] PR feedback --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index dfa3bba..4be393c 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,7 @@ npm install commerce-sdk-isomorphic ### Configure the Isomorphic SDK -**Note** -You will see warning about using private helpers like this -'This function can run on client-side. You are potentially exposing SLAS secret on browser. Make sure to keep it safe and secure!';. -The reason is because commerce-sdk-isomophic runs on both server and client. This warning is to remind developers to always keep their secret safe -and avoid expose it to client side + ```javascript /** @@ -65,10 +61,11 @@ const {access_token, refresh_token} = await helpers.loginGuestUser( ); // Execute Private Client OAuth with PKCE to acquire guest tokens -const {access_token, refresh_token} = await helpers.loginGuestUserPrivate( - shopperLogin, - {}, {clientSecret: 'slas-client-secret'} -); +// ***WARNING*** Be cautious about using this function in the browser as you may end up exposing your client secret +// const {access_token, refresh_token} = await helpers.loginGuestUserPrivate( +// shopperLogin, +// {}, {clientSecret: 'slas-client-secret'} +// ); const shopperSearch = new ShopperSearch({ ...config, @@ -123,12 +120,17 @@ _headers:_ A collection of key/value string pairs representing additional header _throwOnBadResponse:_ Default value is false. When set to true, the SDK throws an Error on responses with statuses that are not 2xx or 304. -### Public Client Shopper Login helpers +### Client Shopper Login helpers A collection of helper functions are available in this SDK to simplify [Public Client Shopper Login OAuth flows](https://developer.salesforce.com/docs/commerce/commerce-api/references?meta=shopper-login:Summary). See sample code above for guest login. +**Note** +If you use the SLAS private client helper functions in the browser, a warning will be displayed: +`This function can run on client-side. You are potentially exposing SLAS secret on browser. Make sure to keep it safe and secure!` +The reason is because commerce-sdk-isomophic runs on both server and client. This warning is to remind developers to always keep their secret safe and avoid expose it to client side + ## License Information The Commerce SDK Isomorphic is licensed under BSD-3-Clause license. See the [license](./LICENSE.txt) for details. From e9bd0bbb42c06007e8712c38067eda2d30170392 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Fri, 26 Jan 2024 12:29:41 -0800 Subject: [PATCH 6/7] PR feedback --- src/static/helpers/slasHelper.ts | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/static/helpers/slasHelper.ts b/src/static/helpers/slasHelper.ts index 051390b..3b78924 100644 --- a/src/static/helpers/slasHelper.ts +++ b/src/static/helpers/slasHelper.ts @@ -16,9 +16,6 @@ import { } from '../../lib/shopperLogin'; import ResponseError from '../responseError'; -const warningSlasSecretMsg = - 'This function can run on client-side. You are potentially exposing SLAS secret on browser. Make sure to keep it safe and secure!'; - export const stringToBase64 = isBrowser ? btoa : (unencoded: string): string => Buffer.from(unencoded).toString('base64'); @@ -152,7 +149,7 @@ export async function authorize( /** * A single function to execute the ShopperLogin Private Client Guest Login as described in the [API documentation](https://developer.salesforce.com/docs/commerce/commerce-api/guide/slas-private-client.html). - * + * **Note**: this func can run on client side. Only use this one when the slas client secret is secured. * @param slasClient - a configured instance of the ShopperLogin SDK client * @param credentials - client secret used for authentication * @param credentials.clientSecret - secret associated with client ID @@ -174,12 +171,6 @@ export async function loginGuestUserPrivate( clientSecret: string; } ): Promise { - /* istanbul ignore next */ - if (isBrowser) { - // we intentionally have warning here - // eslint-disable-next-line - console.warn(warningSlasSecretMsg); - } const authorization = `Basic ${stringToBase64( `${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` )}`; @@ -240,6 +231,7 @@ export async function loginGuestUser( /** * A single function to execute the ShopperLogin Public Client Registered User B2C 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 id and password and clientSecret (if applicable) to login with. * @param credentials.username - the id of the user to login with. @@ -324,13 +316,6 @@ export async function loginRegisteredUserB2C( }; // using slas private client if (credentials.clientSecret) { - /* istanbul ignore next */ - if (isBrowser) { - // we intentionally have warning here - // eslint-disable-next-line - console.warn(warningSlasSecretMsg); - } - const authHeaderIdSecret = `Basic ${stringToBase64( `${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` )}`; @@ -349,6 +334,7 @@ export async function loginRegisteredUserB2C( /** * 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. * @param slasClient a configured instance of the ShopperLogin SDK client. * @param parameters - parameters to pass in the API calls. * @param parameters.refreshToken - a valid refresh token to exchange for a new access token (and refresh token). @@ -374,12 +360,6 @@ export function refreshAccessToken( }; if (credentials && credentials.clientSecret) { - /* istanbul ignore next */ - if (isBrowser) { - // we intentionally have warning here - // eslint-disable-next-line - console.warn(warningSlasSecretMsg); - } const authorization = `Basic ${stringToBase64( `${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}` )}`; From 0eb80c4f39ca0d79f7c2cd13956b1605a050f4f8 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Fri, 26 Jan 2024 14:08:28 -0800 Subject: [PATCH 7/7] adjust readme --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4be393c..415032f 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ const {access_token, refresh_token} = await helpers.loginGuestUser( ); // Execute Private Client OAuth with PKCE to acquire guest tokens -// ***WARNING*** Be cautious about using this function in the browser as you may end up exposing your client secret +// ***WARNING*** Be cautious about using this function in the browser as you may end up exposing your client secret, only use it when you know your slas client secret is secured // const {access_token, refresh_token} = await helpers.loginGuestUserPrivate( // shopperLogin, // {}, {clientSecret: 'slas-client-secret'} @@ -127,9 +127,7 @@ Client Shopper Login OAuth flows](https://developer.salesforce.com/docs/commerce/commerce-api/references?meta=shopper-login:Summary). See sample code above for guest login. **Note** -If you use the SLAS private client helper functions in the browser, a warning will be displayed: -`This function can run on client-side. You are potentially exposing SLAS secret on browser. Make sure to keep it safe and secure!` -The reason is because commerce-sdk-isomophic runs on both server and client. This warning is to remind developers to always keep their secret safe and avoid expose it to client side +If you use the SLAS private client helper functions in the browser, making sure that your slas client secret are secured since funcs can run in client-side. ## License Information