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
}
})
})
})
183 changes: 174 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 {
if (!this.clientSecret) {
return undefined
}
return `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,160 @@ class Auth {
uido
}
}
}

/**
* A wrapper method for the SLAS endpoint: authorizeWebauthnRegistration.
*/
async authorizeWebauthnRegistration(
parameters: ShopperLoginTypes.authorizeWebauthnRegistrationBodyType
) {
const slasClient = this.client

const options = {
headers: {
Authorization: ''
},
body: {
// Required params
user_id: parameters.user_id,
mode: parameters.mode,
channel_id: parameters.channel_id || slasClient.clientConfig.parameters.siteId
}
}

const authHeader = this.getBasicAuthHeader(slasClient)
if (authHeader) {
options.headers.Authorization = authHeader
}

const res = await slasClient.authorizeWebauthnRegistration(options)

return res
}

/**
* A wrapper method for the SLAS endpoint: startWebauthnUserRegistration.
*/
async startWebauthnUserRegistration(
parameters: ShopperLoginTypes.startWebauthnUserRegistrationBodyType
) {
const slasClient = this.client

const options = {
headers: {
Authorization: ''
},
body: {
display_name: parameters.display_name,
nick_name: parameters.nick_name,
client_id: parameters.client_id || slasClient.clientConfig.parameters.clientId,
// Required params
channel_id: parameters.channel_id || slasClient.clientConfig.parameters.siteId,
pwd_action_token: parameters.pwd_action_token,
user_id: parameters.user_id
}
}

const authHeader = this.getBasicAuthHeader(slasClient)
if (authHeader) {
options.headers.Authorization = authHeader
}

const res = await slasClient.startWebauthnUserRegistration(options)
return res
}

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

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

const authHeader = this.getBasicAuthHeader(slasClient)
if (authHeader) {
options.headers.Authorization = authHeader
}

const res = await slasClient.finishWebauthnUserRegistration(options)
return res
}

/**
* A wrapper method for the SLAS endpoint: startWebauthnAuthentication.
*/
async startWebauthnAuthentication(
parameters: ShopperLoginTypes.startWebauthnAuthenticationBodyType
) {
const slasClient = this.client

const options = {
headers: {
Authorization: ''
},
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
}
}

const authHeader = this.getBasicAuthHeader(slasClient)
if (authHeader) {
options.headers.Authorization = authHeader
}

const res = await slasClient.startWebauthnAuthentication(options)
return res
}

/**
* A wrapper method for the SLAS endpoint: finishWebauthnAuthentication.
*/
async finishWebauthnAuthentication(parameters: ShopperLoginTypes.AuthenticateFinishRequest) {
const slasClient = this.client

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

const authHeader = this.getBasicAuthHeader(slasClient)
if (authHeader) {
options.headers.Authorization = authHeader
}

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