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-16553557 Add passwordless login helpers #173

Merged
merged 5 commits into from
Sep 25, 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
6 changes: 6 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "error"
}
},
{
"files": ["./src/static/helpers/slasHelper.ts"],
"rules": {
"max-lines": "off"
}
}
],
"settings": {
Expand Down
148 changes: 148 additions & 0 deletions src/static/helpers/slasHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () =>
({
Expand All @@ -73,6 +75,8 @@ const createMockSlasClient = () =>
getAccessToken: getAccessTokenMock,
logoutCustomer: logoutCustomerMock,
generateCodeChallenge: generateCodeChallengeMock,
authorizePasswordlessCustomer: authorizePasswordlessCustomerMock,
getPasswordLessAccessToken: getPasswordLessAccessTokenMock,
} as unknown as ShopperLogin<{
shortCode: string;
organizationId: string;
Expand Down Expand Up @@ -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 = {
Expand Down
139 changes: 139 additions & 0 deletions src/static/helpers/slasHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response> {
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<TokenResponse> {
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.
Expand Down
Loading