Skip to content
137 changes: 136 additions & 1 deletion packages/commerce-sdk-react/src/auth/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,14 @@ jest.mock('commerce-sdk-isomorphic', () => {
fetchOptions: {
credentials: config?.fetchOptions?.credentials || 'same-origin'
}
}
},
authorizeWebauthnRegistration: jest.fn().mockResolvedValue({}),
startWebauthnUserRegistration: jest.fn().mockResolvedValue({}),
finishWebauthnUserRegistration: jest.fn().mockResolvedValue({}),
startWebauthnAuthentication: jest.fn().mockResolvedValue({}),
finishWebauthnAuthentication: jest
.fn()
.mockResolvedValue({tokenResponse: TOKEN_RESPONSE})
}))
}
})
Expand Down Expand Up @@ -1280,3 +1287,131 @@ describe('hybridAuthEnabled property toggles clearECOMSession', () => {
expect(auth.get('dwsid')).toBe('test-dwsid-value')
})
})

describe('Webauthn', () => {
beforeEach(() => {
jest.clearAllMocks()
})

const PUBLIC_KEY_CREDENTIAL_JSON: ShopperLoginTypes.PublicKeyCredentialJson = {
id: 'credential-id',
rawId: 'raw-credential-id',
type: 'public-key',
response: {
authenticatorData: [],
clientDataJSON: [],
signature: [],
userHandle: null
} as ShopperLoginTypes.AuthenticatorAssertionResponseJson
}

test('authorizeWebauthnRegistration', async () => {
const auth = new Auth(config)
await auth.authorizeWebauthnRegistration({
user_id: 'test-user-id',
mode: 'test-mode',
channel_id: 'test-channel-id'
})

expect((auth as any).client.authorizeWebauthnRegistration).toHaveBeenCalledWith({
headers: {
Authorization: ''
},
body: {
user_id: 'test-user-id',
mode: 'test-mode',
channel_id: 'test-channel-id'
}
})
})

test('startWebauthnUserRegistration', async () => {
const auth = new Auth(config)
await auth.startWebauthnUserRegistration({
channel_id: 'test-channel-id',
display_name: 'test-display-name',
nick_name: 'test-nick-name',
client_id: 'test-client-id',
pwd_action_token: 'test-pwd-action-token',
user_id: 'test-user-id'
})

expect((auth as any).client.startWebauthnUserRegistration).toHaveBeenCalledWith({
headers: {
Authorization: ''
},
body: {
display_name: 'test-display-name',
nick_name: 'test-nick-name',
client_id: 'test-client-id',
channel_id: 'test-channel-id',
pwd_action_token: 'test-pwd-action-token',
user_id: 'test-user-id'
}
})
})

test('finishWebauthnUserRegistration', async () => {
const auth = new Auth(config)
await auth.finishWebauthnUserRegistration({
client_id: 'test-client-id',
username: 'test-username',
credential: PUBLIC_KEY_CREDENTIAL_JSON,
channel_id: 'test-channel-id',
pwd_action_token: 'test-pwd-action-token'
})

expect((auth as any).client.finishWebauthnUserRegistration).toHaveBeenCalledWith({
headers: {
Authorization: ''
},
body: {
client_id: 'test-client-id',
username: 'test-username',
credential: PUBLIC_KEY_CREDENTIAL_JSON,
channel_id: 'test-channel-id',
pwd_action_token: 'test-pwd-action-token'
}
})
})

test('startWebauthnAuthentication', async () => {
const auth = new Auth(config)
await auth.startWebauthnAuthentication({
user_id: 'test-user-id',
channel_id: 'test-channel-id',
client_id: 'test-client-id'
})

expect((auth as any).client.startWebauthnAuthentication).toHaveBeenCalledWith({
headers: {
Authorization: ''
},
body: {
user_id: 'test-user-id',
channel_id: 'test-channel-id',
client_id: 'test-client-id'
}
})
})

test('finishWebauthnAuthentication', async () => {
const auth = new Auth(config)
await auth.finishWebauthnAuthentication({
client_id: 'test-client-id',
channel_id: 'test-channel-id',
credential: PUBLIC_KEY_CREDENTIAL_JSON
})

expect((auth as any).client.finishWebauthnAuthentication).toHaveBeenCalledWith({
headers: {
Authorization: ''
},
body: {
client_id: 'test-client-id',
channel_id: 'test-channel-id',
credential: PUBLIC_KEY_CREDENTIAL_JSON
}
})
})
})
169 changes: 160 additions & 9 deletions packages/commerce-sdk-react/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,19 @@ class Auth {
return token
}

/**
* Get Basic auth header for private client requests.
* Returns undefined if not using a private client.
*/
private getBasicAuthHeader(client: ShopperLogin<ApiClientConfigParams>): string | undefined {
return (
this.clientSecret &&
`Basic ${stringToBase64(
`${client.clientConfig.parameters.clientId}:${this.clientSecret}`
)}`
)
}

/**
* A wrapper method for the SLAS endpoint: getPasswordResetToken.
*
Expand All @@ -1340,10 +1353,9 @@ class Auth {
}

// Only set authorization header if using private client
if (this.clientSecret) {
options.headers.Authorization = `Basic ${stringToBase64(
`${slasClient.clientConfig.parameters.clientId}:${this.clientSecret}`
)}`
const authHeader = this.getBasicAuthHeader(slasClient)
if (authHeader) {
options.headers.Authorization = authHeader
}

const res = await slasClient.getPasswordResetToken(options)
Expand Down Expand Up @@ -1371,10 +1383,9 @@ class Auth {
}

// Only set authorization header if using private client
if (this.clientSecret) {
options.headers.Authorization = `Basic ${stringToBase64(
`${slasClient.clientConfig.parameters.clientId}:${this.clientSecret}`
)}`
const authHeader = this.getBasicAuthHeader(slasClient)
if (authHeader) {
options.headers.Authorization = authHeader
}
// TODO: no code verifier needed with the fix blair has made, delete this when the fix has been merged to production
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down Expand Up @@ -1423,6 +1434,146 @@ class Auth {
uido
}
}
}

/**
* A wrapper method for the SLAS endpoint: authorizeWebauthnRegistration.
*/
async authorizeWebauthnRegistration(
parameters: ShopperLoginTypes.authorizeWebauthnRegistrationBodyType
) {
const slasClient = this.client
const authHeader = this.getBasicAuthHeader(slasClient)
const options = {
headers: {
Authorization: authHeader ?? ''
},
body: {
// Required params
user_id: parameters.user_id,
mode: parameters.mode,
channel_id: parameters.channel_id || slasClient.clientConfig.parameters.siteId,
// Optional params
...(parameters.locale && {locale: parameters.locale}),
...(parameters.client_id && {client_id: parameters.client_id}),
...(parameters.code_challenge && {code_challenge: parameters.code_challenge}),
...(parameters.callback_uri && {callback_uri: parameters.callback_uri}),
...(parameters.idp_name && {idp_name: parameters.idp_name}),
...(parameters.hint && {hint: parameters.hint})
}
}

return await slasClient.authorizeWebauthnRegistration(options)
}

/**
* A wrapper method for the SLAS endpoint: startWebauthnUserRegistration.
*/
async startWebauthnUserRegistration(
parameters: ShopperLoginTypes.startWebauthnUserRegistrationBodyType
) {
const slasClient = this.client
const authHeader = this.getBasicAuthHeader(slasClient)
const options = {
headers: {
Authorization: authHeader ?? ''
},
body: {
// Required params
channel_id: parameters.channel_id || slasClient.clientConfig.parameters.siteId,
pwd_action_token: parameters.pwd_action_token,
user_id: parameters.user_id,
// Optional params
...(parameters.display_name && {display_name: parameters.display_name}),
...(parameters.nick_name && {nick_name: parameters.nick_name}),
...(parameters.client_id && {client_id: parameters.client_id})
}
}

return await slasClient.startWebauthnUserRegistration(options)
}

/**
* A wrapper method for the SLAS endpoint: finishWebauthnUserRegistration.
*/
async finishWebauthnUserRegistration(parameters: ShopperLoginTypes.RegistrationFinishRequest) {
const slasClient = this.client
const authHeader = this.getBasicAuthHeader(slasClient)

const options = {
headers: {
Authorization: authHeader ?? ''
},
body: {
// Required params
client_id: parameters.client_id || slasClient.clientConfig.parameters.clientId,
username: parameters.username,
credential: parameters.credential,
channel_id: parameters.channel_id || slasClient.clientConfig.parameters.siteId,
pwd_action_token: parameters.pwd_action_token
}
}

return await slasClient.finishWebauthnUserRegistration(options)
}

/**
* A wrapper method for the SLAS endpoint: startWebauthnAuthentication.
*/
async startWebauthnAuthentication(
parameters: ShopperLoginTypes.startWebauthnAuthenticationBodyType
) {
const slasClient = this.client
const authHeader = this.getBasicAuthHeader(slasClient)
const options = {
headers: {
Authorization: authHeader ?? ''
},
body: {
// Required params
client_id: parameters.client_id || slasClient.clientConfig.parameters.clientId,
channel_id: parameters.channel_id || slasClient.clientConfig.parameters.siteId,
user_id: parameters.user_id,
// Optional params
...(parameters.tenant_id && {tenant_id: parameters.tenant_id})
}
}

return await slasClient.startWebauthnAuthentication(options)
}

/**
* A wrapper method for the SLAS endpoint: finishWebauthnAuthentication.
*/
async finishWebauthnAuthentication(parameters: ShopperLoginTypes.AuthenticateFinishRequest) {
const slasClient = this.client
const authHeader = this.getBasicAuthHeader(slasClient)
const options = {
headers: {
Authorization: authHeader ?? ''
},
body: {
// Required params
client_id: parameters.client_id || slasClient.clientConfig.parameters.clientId,
channel_id: parameters.channel_id || slasClient.clientConfig.parameters.siteId,
credential: parameters.credential,
// Optional params
...(parameters.user_id && {user_id: parameters.user_id}),
...(parameters.email && {email: parameters.email}),
...(parameters.tenant_id && {tenant_id: parameters.tenant_id}),
...(parameters.usid && {usid: parameters.usid})
}
}

const res = await slasClient.finishWebauthnAuthentication(options)

const tokenResponse = res.tokenResponse
if (!tokenResponse) {
throw new Error('finishWebauthnAuthentication did not return a tokenResponse.')
}

this.handleTokenResponse(tokenResponse, false)

return tokenResponse
}
}
export default Auth
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,11 @@ export const cacheUpdateMatrix: CacheUpdateMatrix<Client> = {
resetPassword: noop,
getPasswordLessAccessToken: noop,
revokeToken: noop,
introspectToken: noop
introspectToken: noop,
// WebAuthn methods - these will be available when commerce-sdk-isomorphic is updated
startWebauthnUserRegistration: noop,
finishWebauthnUserRegistration: noop,
authorizeWebauthnRegistration: noop,
startWebauthnAuthentication: noop,
finishWebauthnAuthentication: noop
}
Loading
Loading