Skip to content
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-14758586: Support slas client #140

Merged
merged 7 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ npm install commerce-sdk-isomorphic

### Configure the Isomorphic SDK


```javascript
/**
* Configure required parameters
Expand Down Expand Up @@ -59,6 +60,13 @@ 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
alexvuong marked this conversation as resolved.
Show resolved Hide resolved
// ***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'}
// );

alexvuong marked this conversation as resolved.
Show resolved Hide resolved
const shopperSearch = new ShopperSearch({
...config,
headers: {authorization: `Bearer ${access_token}`},
Expand Down Expand Up @@ -112,12 +120,15 @@ _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, making sure that your slas client secret are secured since funcs can run in client-side.

## License Information

The Commerce SDK Isomorphic is licensed under BSD-3-Clause license. See the [license](./LICENSE.txt) for details.
Expand Down
136 changes: 113 additions & 23 deletions src/static/helpers/slasHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;

Expand All @@ -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 = {
Expand All @@ -246,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;
Expand All @@ -265,6 +295,42 @@ 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',
channel_id: 'site_id',
organizationId: 'organization_id',
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();
Expand Down Expand Up @@ -321,7 +387,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;
Expand All @@ -340,23 +406,47 @@ 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
);
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',
client_id: 'client_id',
channel_id: 'site_id',
refresh_token: parameters.refreshToken,
},
};
const token = slasHelper.refreshAccessToken(
createMockSlasClient(),
parameters,
{
clientSecret: credentialsPrivate.clientSecret,
}
);
expect(getAccessTokenMock).toBeCalledWith(expectedReqOpts);
expect(token).toStrictEqual(expectedTokenResponse);
});
});

describe('Logout', () => {
Expand Down
82 changes: 78 additions & 4 deletions src/static/helpers/slasHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,47 @@ 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).
* **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
* @param parameters - parameters to pass in the API calls.
* @param parameters.usid? - Unique Shopper Identifier to enable personalization.
* @returns TokenResponse
*/
export async function loginGuestUserPrivate(
slasClient: ShopperLogin<{
shortCode: string;
organizationId: string;
clientId: string;
siteId: string;
}>,
parameters: {
usid?: string;
},
credentials: {
clientSecret: string;
}
): Promise<TokenResponse> {
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}),
joeluong-sfcc marked this conversation as resolved.
Show resolved Hide resolved
},
};

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.
Expand Down Expand Up @@ -190,10 +231,12 @@ 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 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
* @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.
Expand All @@ -209,6 +252,7 @@ export async function loginRegisteredUserB2C(
credentials: {
username: string;
password: string;
clientSecret?: string;
},
parameters: {
redirectURI: string;
Expand Down Expand Up @@ -260,7 +304,6 @@ export async function loginRegisteredUserB2C(
}

const authResponse = getCodeAndUsidFromUrl(redirectUrlString);

const tokenBody = {
client_id: slasClient.clientConfig.parameters.clientId,
channel_id: slasClient.clientConfig.parameters.siteId,
Expand All @@ -271,15 +314,32 @@ export async function loginRegisteredUserB2C(
redirect_uri: parameters.redirectURI,
usid: authResponse.usid,
};

// using slas 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);
}
vcua-mobify marked this conversation as resolved.
Show resolved Hide resolved
// default is to use slas public client
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.
* @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 credentials - the clientSecret (if applicable) to login with.
* @param credentials.clientSecret - secret associated with client ID
* @returns TokenResponse
*/
export function refreshAccessToken(
Expand All @@ -289,7 +349,8 @@ export function refreshAccessToken(
clientId: string;
siteId: string;
}>,
parameters: {refreshToken: string}
parameters: {refreshToken: string},
credentials?: {clientSecret?: string}
): Promise<TokenResponse> {
const body = {
grant_type: 'refresh_token',
Expand All @@ -298,6 +359,19 @@ export function refreshAccessToken(
channel_id: slasClient.clientConfig.parameters.siteId,
};

if (credentials && credentials.clientSecret) {
const authorization = `Basic ${stringToBase64(
`${slasClient.clientConfig.parameters.clientId}:${credentials.clientSecret}`
)}`;
const options = {
headers: {
Authorization: authorization,
},
body,
};
return slasClient.getAccessToken(options);
}

return slasClient.getAccessToken({body});
}

Expand Down
Loading