diff --git a/modules/passkey-crypto/package.json b/modules/passkey-crypto/package.json index 0010782ef2..e492b18060 100644 --- a/modules/passkey-crypto/package.json +++ b/modules/passkey-crypto/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@bitgo/public-types": "6.1.0", + "@bitgo/sdk-core": "^36.42.0", "@bitgo/sjcl": "^1.1.0" }, "devDependencies": { diff --git a/modules/passkey-crypto/src/index.ts b/modules/passkey-crypto/src/index.ts index 787f130bd7..efa340ce92 100644 --- a/modules/passkey-crypto/src/index.ts +++ b/modules/passkey-crypto/src/index.ts @@ -1,4 +1,5 @@ export { derivePassword } from './derivePassword'; export { deriveEnterpriseSalt } from './deriveEnterpriseSalt'; export { buildEvalByCredential, matchDeviceByCredentialId } from './prfHelpers'; +export { removePasskeyFromAccount } from './removePasskeyFromAccount'; export type { WebAuthnOtpDevice, PasskeyAuthResult, PasskeyGetOptions, WebAuthnProvider } from './webAuthnTypes'; diff --git a/modules/passkey-crypto/src/removePasskeyFromAccount.ts b/modules/passkey-crypto/src/removePasskeyFromAccount.ts new file mode 100644 index 0000000000..0afe539b2c --- /dev/null +++ b/modules/passkey-crypto/src/removePasskeyFromAccount.ts @@ -0,0 +1,14 @@ +import { BitGoBase } from '@bitgo/sdk-core'; +import type { WebAuthnOtpDevice } from '@bitgo/public-types'; + +/** + * Permanently removes a passkey credential from the user's account. + * Call removePasskeyFromWallet() for all affected wallets before calling this. + */ +export async function removePasskeyFromAccount(params: { bitgo: BitGoBase; device: WebAuthnOtpDevice }): Promise { + const { bitgo, device } = params; + if (!device.id) { + throw new Error('device.id is required to remove a passkey from the account'); + } + await bitgo.del(bitgo.url(`/user/otp/${device.id}`)).result(); +} diff --git a/modules/passkey-crypto/test/unit/removePasskeyFromAccount.test.ts b/modules/passkey-crypto/test/unit/removePasskeyFromAccount.test.ts new file mode 100644 index 0000000000..7b3dfb02bc --- /dev/null +++ b/modules/passkey-crypto/test/unit/removePasskeyFromAccount.test.ts @@ -0,0 +1,67 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { removePasskeyFromAccount } from '../../src/removePasskeyFromAccount'; +import type { WebAuthnOtpDevice } from '@bitgo/public-types'; + +describe('removePasskeyFromAccount', function () { + let mockBitGo: { + url: sinon.SinonStub; + del: sinon.SinonStub; + }; + + const device: WebAuthnOtpDevice = { + id: 'mongo-object-id-123', + credentialId: 'cred-id-should-not-be-used', + prfSalt: 'some-salt', + isPasskey: true, + }; + + beforeEach(function () { + mockBitGo = { + url: sinon.stub().callsFake((path: string) => `https://app.bitgo.com/api/v1${path}`), + del: sinon.stub().returns({ + result: sinon.stub().resolves(undefined), + }), + }; + }); + + afterEach(function () { + sinon.restore(); + }); + + it('should DELETE /user/otp/{device.id} using device.id', async function () { + await removePasskeyFromAccount({ bitgo: mockBitGo as any, device }); + + assert.strictEqual(mockBitGo.url.calledOnce, true); + assert.strictEqual(mockBitGo.url.firstCall.args[0], `/user/otp/${device.id}`); + assert.strictEqual(mockBitGo.del.calledOnce, true); + }); + + it('should not use credentialId', async function () { + await removePasskeyFromAccount({ bitgo: mockBitGo as any, device }); + + const urlArg: string = mockBitGo.url.firstCall.args[0]; + assert.ok(!urlArg.includes(device.credentialId), 'URL should not contain credentialId'); + }); + + it('should resolve without returning a value', async function () { + const result = await removePasskeyFromAccount({ bitgo: mockBitGo as any, device }); + assert.strictEqual(result, undefined); + }); + + it('should throw if device.id is empty', async function () { + const badDevice: WebAuthnOtpDevice = { ...device, id: '' }; + await assert.rejects(() => removePasskeyFromAccount({ bitgo: mockBitGo as any, device: badDevice }), { + message: 'device.id is required to remove a passkey from the account', + }); + assert.strictEqual(mockBitGo.del.called, false); + }); + + it('should throw if device.id is undefined', async function () { + const badDevice = { ...device, id: undefined } as unknown as WebAuthnOtpDevice; + await assert.rejects(() => removePasskeyFromAccount({ bitgo: mockBitGo as any, device: badDevice }), { + message: 'device.id is required to remove a passkey from the account', + }); + assert.strictEqual(mockBitGo.del.called, false); + }); +}); diff --git a/modules/passkey-crypto/tsconfig.json b/modules/passkey-crypto/tsconfig.json index 5c6d1e6385..e150f979f8 100644 --- a/modules/passkey-crypto/tsconfig.json +++ b/modules/passkey-crypto/tsconfig.json @@ -9,5 +9,10 @@ "typeRoots": ["../../types", "./node_modules/@types", "../../node_modules/@types"] }, "include": ["src/**/*", "test/**/*"], - "exclude": ["node_modules"] + "exclude": ["node_modules"], + "references": [ + { + "path": "../sdk-core" + } + ] }