Skip to content

Commit cc2264e

Browse files
committed
Merge branch 'feature/webauthn-login' of github.com:SalesforceCommerceCloud/pwa-kit into feature/webauthn-login
2 parents 2961538 + 6c26eba commit cc2264e

File tree

11 files changed

+792
-131
lines changed

11 files changed

+792
-131
lines changed

packages/commerce-sdk-react/src/auth/index.test.ts

Lines changed: 242 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,113 +1527,268 @@ describe('Webauthn', () => {
15271527
} as ShopperLoginTypes.AuthenticatorAssertionResponseJson
15281528
}
15291529

1530-
test('authorizeWebauthnRegistration', async () => {
1531-
const auth = new Auth(config)
1532-
await auth.authorizeWebauthnRegistration({
1533-
user_id: 'test-user-id',
1534-
mode: 'test-mode',
1535-
channel_id: 'test-channel-id'
1536-
})
1537-
1538-
expect((auth as any).client.authorizeWebauthnRegistration).toHaveBeenCalledWith({
1539-
headers: {
1540-
Authorization: ''
1530+
test.each([
1531+
[
1532+
'with all parameters specified',
1533+
{
1534+
user_id: 'user@example.com',
1535+
mode: 'email',
1536+
channel_id: 'custom-channel-id',
1537+
client_id: 'custom-client-id',
1538+
locale: 'en-GB',
1539+
code_challenge: 'test-code-challenge',
1540+
callback_uri: 'https://example.com/callback',
1541+
idp_name: 'customIdp',
1542+
hint: 'custom_hint'
15411543
},
1542-
body: {
1543-
user_id: 'test-user-id',
1544-
mode: 'test-mode',
1545-
channel_id: 'test-channel-id'
1544+
{
1545+
user_id: 'user@example.com',
1546+
mode: 'email',
1547+
channel_id: 'custom-channel-id',
1548+
client_id: 'custom-client-id',
1549+
locale: 'en-GB',
1550+
code_challenge: 'test-code-challenge',
1551+
callback_uri: 'https://example.com/callback',
1552+
idp_name: 'customIdp',
1553+
hint: 'custom_hint'
15461554
}
1547-
})
1548-
})
1555+
],
1556+
[
1557+
'defaults optional parameters when only required parameters are specified',
1558+
{
1559+
user_id: 'user@example.com',
1560+
mode: 'email'
1561+
},
1562+
{
1563+
user_id: 'user@example.com',
1564+
mode: 'email',
1565+
channel_id: config.siteId
1566+
}
1567+
]
1568+
])(
1569+
'authorizeWebauthnRegistration %s',
1570+
async (
1571+
_,
1572+
input: Partial<ShopperLoginTypes.authorizeWebauthnRegistrationBodyType>,
1573+
expectedBody: Partial<ShopperLoginTypes.authorizeWebauthnRegistrationBodyType>
1574+
) => {
1575+
const auth = new Auth(config)
1576+
await auth.authorizeWebauthnRegistration(
1577+
input as ShopperLoginTypes.authorizeWebauthnRegistrationBodyType
1578+
)
15491579

1550-
test('startWebauthnUserRegistration', async () => {
1551-
const auth = new Auth(config)
1552-
await auth.startWebauthnUserRegistration({
1553-
channel_id: 'test-channel-id',
1554-
display_name: 'test-display-name',
1555-
nick_name: 'test-nick-name',
1556-
client_id: 'test-client-id',
1557-
pwd_action_token: 'test-pwd-action-token',
1558-
user_id: 'test-user-id'
1559-
})
1580+
expect((auth as any).client.authorizeWebauthnRegistration).toHaveBeenCalledWith({
1581+
headers: {
1582+
Authorization: ''
1583+
},
1584+
body: expectedBody
1585+
})
1586+
}
1587+
)
15601588

1561-
expect((auth as any).client.startWebauthnUserRegistration).toHaveBeenCalledWith({
1562-
headers: {
1563-
Authorization: ''
1589+
test.each([
1590+
[
1591+
'with all parameters specified',
1592+
{
1593+
user_id: 'user@example.com',
1594+
pwd_action_token: 'test-pwd-action-token',
1595+
channel_id: 'custom-channel-id',
1596+
client_id: 'custom-client-id',
1597+
display_name: 'Test Display Name',
1598+
nick_name: 'Test Nick Name'
15641599
},
1565-
body: {
1566-
display_name: 'test-display-name',
1567-
nick_name: 'test-nick-name',
1568-
client_id: 'test-client-id',
1569-
channel_id: 'test-channel-id',
1600+
{
1601+
user_id: 'user@example.com',
15701602
pwd_action_token: 'test-pwd-action-token',
1571-
user_id: 'test-user-id'
1603+
channel_id: 'custom-channel-id',
1604+
client_id: 'custom-client-id',
1605+
display_name: 'Test Display Name',
1606+
nick_name: 'Test Nick Name'
15721607
}
1573-
})
1574-
})
1608+
],
1609+
[
1610+
'defaults optional parameters when only required parameters are specified',
1611+
{
1612+
user_id: 'user@example.com',
1613+
pwd_action_token: 'test-pwd-action-token'
1614+
},
1615+
{
1616+
user_id: 'user@example.com',
1617+
pwd_action_token: 'test-pwd-action-token',
1618+
channel_id: config.siteId
1619+
}
1620+
]
1621+
])(
1622+
'startWebauthnUserRegistration %s',
1623+
async (
1624+
_,
1625+
input: Partial<ShopperLoginTypes.startWebauthnUserRegistrationBodyType>,
1626+
expectedBody: Partial<ShopperLoginTypes.startWebauthnUserRegistrationBodyType>
1627+
) => {
1628+
const auth = new Auth(config)
1629+
await auth.startWebauthnUserRegistration(
1630+
input as ShopperLoginTypes.startWebauthnUserRegistrationBodyType
1631+
)
15751632

1576-
test('finishWebauthnUserRegistration', async () => {
1577-
const auth = new Auth(config)
1578-
await auth.finishWebauthnUserRegistration({
1579-
client_id: 'test-client-id',
1580-
username: 'test-username',
1581-
credential: PUBLIC_KEY_CREDENTIAL_JSON,
1582-
channel_id: 'test-channel-id',
1583-
pwd_action_token: 'test-pwd-action-token'
1584-
})
1633+
expect((auth as any).client.startWebauthnUserRegistration).toHaveBeenCalledWith({
1634+
headers: {
1635+
Authorization: ''
1636+
},
1637+
body: expectedBody
1638+
})
1639+
}
1640+
)
15851641

1586-
expect((auth as any).client.finishWebauthnUserRegistration).toHaveBeenCalledWith({
1587-
headers: {
1588-
Authorization: ''
1642+
test.each([
1643+
[
1644+
'with all parameters specified',
1645+
{
1646+
username: 'user@example.com',
1647+
credential: PUBLIC_KEY_CREDENTIAL_JSON,
1648+
pwd_action_token: 'test-pwd-action-token',
1649+
channel_id: 'custom-channel-id',
1650+
client_id: 'custom-client-id'
15891651
},
1590-
body: {
1591-
client_id: 'test-client-id',
1592-
username: 'test-username',
1652+
{
1653+
username: 'user@example.com',
1654+
credential: PUBLIC_KEY_CREDENTIAL_JSON,
1655+
pwd_action_token: 'test-pwd-action-token',
1656+
channel_id: 'custom-channel-id',
1657+
client_id: 'custom-client-id'
1658+
}
1659+
],
1660+
[
1661+
'defaults optional parameters when only required parameters are specified',
1662+
{
1663+
username: 'user@example.com',
15931664
credential: PUBLIC_KEY_CREDENTIAL_JSON,
1594-
channel_id: 'test-channel-id',
15951665
pwd_action_token: 'test-pwd-action-token'
1666+
},
1667+
{
1668+
username: 'user@example.com',
1669+
credential: PUBLIC_KEY_CREDENTIAL_JSON,
1670+
pwd_action_token: 'test-pwd-action-token',
1671+
channel_id: config.siteId,
1672+
client_id: config.clientId
15961673
}
1597-
})
1598-
})
1674+
]
1675+
])(
1676+
'finishWebauthnUserRegistration %s',
1677+
async (
1678+
_,
1679+
input: Partial<ShopperLoginTypes.RegistrationFinishRequest>,
1680+
expectedBody: Partial<ShopperLoginTypes.RegistrationFinishRequest>
1681+
) => {
1682+
const auth = new Auth(config)
1683+
await auth.finishWebauthnUserRegistration(
1684+
input as ShopperLoginTypes.RegistrationFinishRequest
1685+
)
15991686

1600-
test('startWebauthnAuthentication', async () => {
1601-
const auth = new Auth(config)
1602-
await auth.startWebauthnAuthentication({
1603-
user_id: 'test-user-id',
1604-
channel_id: 'test-channel-id',
1605-
client_id: 'test-client-id'
1606-
})
1687+
expect((auth as any).client.finishWebauthnUserRegistration).toHaveBeenCalledWith({
1688+
headers: {
1689+
Authorization: ''
1690+
},
1691+
body: expectedBody
1692+
})
1693+
}
1694+
)
16071695

1608-
expect((auth as any).client.startWebauthnAuthentication).toHaveBeenCalledWith({
1609-
headers: {
1610-
Authorization: ''
1696+
test.each([
1697+
[
1698+
'with all parameters specified',
1699+
{
1700+
user_id: 'user@example.com',
1701+
tenant_id: 'tenant-123',
1702+
channel_id: 'custom-channel-id',
1703+
client_id: 'custom-client-id'
16111704
},
1612-
body: {
1613-
user_id: 'test-user-id',
1614-
channel_id: 'test-channel-id',
1615-
client_id: 'test-client-id'
1705+
{
1706+
user_id: 'user@example.com',
1707+
tenant_id: 'tenant-123',
1708+
channel_id: 'custom-channel-id',
1709+
client_id: 'custom-client-id'
16161710
}
1617-
})
1618-
})
1711+
],
1712+
[
1713+
'defaults optional parameters when empty object is provided',
1714+
{},
1715+
{
1716+
channel_id: config.siteId,
1717+
client_id: config.clientId
1718+
}
1719+
]
1720+
])(
1721+
'startWebauthnAuthentication %s',
1722+
async (
1723+
_,
1724+
input: Partial<ShopperLoginTypes.startWebauthnAuthenticationBodyType>,
1725+
expectedBody: Partial<ShopperLoginTypes.startWebauthnAuthenticationBodyType>
1726+
) => {
1727+
const auth = new Auth(config)
1728+
await auth.startWebauthnAuthentication(
1729+
input as ShopperLoginTypes.startWebauthnAuthenticationBodyType
1730+
)
16191731

1620-
test('finishWebauthnAuthentication', async () => {
1621-
const auth = new Auth(config)
1622-
await auth.finishWebauthnAuthentication({
1623-
client_id: 'test-client-id',
1624-
channel_id: 'test-channel-id',
1625-
credential: PUBLIC_KEY_CREDENTIAL_JSON
1626-
})
1732+
expect((auth as any).client.startWebauthnAuthentication).toHaveBeenCalledWith({
1733+
headers: {
1734+
Authorization: ''
1735+
},
1736+
body: expectedBody
1737+
})
1738+
}
1739+
)
16271740

1628-
expect((auth as any).client.finishWebauthnAuthentication).toHaveBeenCalledWith({
1629-
headers: {
1630-
Authorization: ''
1741+
test.each([
1742+
[
1743+
'with all parameters specified',
1744+
{
1745+
user_id: 'user@example.com',
1746+
email: 'user@example.com',
1747+
tenant_id: 'tenant-123',
1748+
usid: 'usid-123',
1749+
channel_id: 'custom-channel-id',
1750+
client_id: 'custom-client-id',
1751+
credential: PUBLIC_KEY_CREDENTIAL_JSON
16311752
},
1632-
body: {
1633-
client_id: 'test-client-id',
1634-
channel_id: 'test-channel-id',
1753+
{
1754+
user_id: 'user@example.com',
1755+
email: 'user@example.com',
1756+
tenant_id: 'tenant-123',
1757+
usid: 'usid-123',
1758+
channel_id: 'custom-channel-id',
1759+
client_id: 'custom-client-id',
16351760
credential: PUBLIC_KEY_CREDENTIAL_JSON
16361761
}
1637-
})
1638-
})
1762+
],
1763+
[
1764+
'defaults optional parameters when only required parameters are specified',
1765+
{
1766+
credential: PUBLIC_KEY_CREDENTIAL_JSON
1767+
},
1768+
{
1769+
channel_id: config.siteId,
1770+
client_id: config.clientId,
1771+
credential: PUBLIC_KEY_CREDENTIAL_JSON
1772+
}
1773+
]
1774+
])(
1775+
'finishWebauthnAuthentication %s',
1776+
async (
1777+
_,
1778+
input: Partial<ShopperLoginTypes.AuthenticateFinishRequest>,
1779+
expectedBody: Partial<ShopperLoginTypes.AuthenticateFinishRequest>
1780+
) => {
1781+
const auth = new Auth(config)
1782+
await auth.finishWebauthnAuthentication(
1783+
input as ShopperLoginTypes.AuthenticateFinishRequest
1784+
)
1785+
1786+
expect((auth as any).client.finishWebauthnAuthentication).toHaveBeenCalledWith({
1787+
headers: {
1788+
Authorization: ''
1789+
},
1790+
body: expectedBody
1791+
})
1792+
}
1793+
)
16391794
})

packages/commerce-sdk-react/src/auth/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1542,12 +1542,14 @@ class Auth {
15421542
// Required params
15431543
client_id: parameters.client_id || slasClient.clientConfig.parameters.clientId,
15441544
channel_id: parameters.channel_id || slasClient.clientConfig.parameters.siteId,
1545-
user_id: parameters.user_id,
15461545
// Optional params
1546+
...(parameters.user_id && {user_id: parameters.user_id}),
15471547
...(parameters.tenant_id && {tenant_id: parameters.tenant_id})
15481548
}
15491549
}
15501550

1551+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1552+
// @ts-ignore TODO: user_id is optional, but commerce-sdk-isomorphic expects it to be required. Remove this comment after commerce-sdk-isomorphic is updated.
15511553
return await slasClient.startWebauthnAuthentication(options)
15521554
}
15531555

packages/template-retail-react-app/app/components/passkey-registration-modal/index.jsx

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-cur
3232

3333
// Utils
3434
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
35+
import {arrayBufferToBase64Url} from '@salesforce/retail-react-app/app/utils/utils'
3536

3637
// SDK
3738
import {AuthHelpers, useAuthHelper} from '@salesforce/commerce-sdk-react'
@@ -84,19 +85,6 @@ const PasskeyRegistrationModal = ({isOpen, onClose}) => {
8485
}
8586
}
8687

87-
/**
88-
* Convert ArrayBuffer to base64url string
89-
*/
90-
const arrayBufferToBase64Url = (buffer) => {
91-
const bytes = new Uint8Array(buffer)
92-
let binary = ''
93-
for (let i = 0; i < bytes.length; i++) {
94-
binary += String.fromCharCode(bytes[i])
95-
}
96-
const base64 = btoa(binary)
97-
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
98-
}
99-
10088
const handleOtpVerification = async (code) => {
10189
setIsLoading(true)
10290
setError(null)

0 commit comments

Comments
 (0)