diff --git a/packages/passkey-controller/CHANGELOG.md b/packages/passkey-controller/CHANGELOG.md index cbf1c3c755..bb68b38f9d 100644 --- a/packages/passkey-controller/CHANGELOG.md +++ b/packages/passkey-controller/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- `PasskeyController` verifies registration and authentication responses with `requireUserVerification: true`, so the WebAuthn user verification (UV) flag must be set; assertions with user presence only no longer pass verification ([#8696](https://github.com/MetaMask/core/pull/8696)) + +### Fixed + +- `generateAuthenticationOptions` now sets `userVerification: 'required'` so client WebAuthn requests align with server-side verification requirements and do not fail on authenticators that skip UV when set to `'preferred'` ([#8696](https://github.com/MetaMask/core/pull/8696)) + ## [2.0.0] ### Added diff --git a/packages/passkey-controller/src/PasskeyController.test.ts b/packages/passkey-controller/src/PasskeyController.test.ts index de37013629..75d6698b5a 100644 --- a/packages/passkey-controller/src/PasskeyController.test.ts +++ b/packages/passkey-controller/src/PasskeyController.test.ts @@ -284,6 +284,11 @@ describe('PasskeyController', () => { ]); expect(options.attestation).toBe('none'); expect(options.timeout).toBe(WEBAUTHN_TIMEOUT_MS); + expect(options.authenticatorSelection).toStrictEqual({ + userVerification: 'required', + authenticatorAttachment: 'platform', + residentKey: 'preferred', + }); expect( (options.extensions as Record)?.prf, ).toBeDefined(); @@ -405,6 +410,21 @@ describe('PasskeyController', () => { }), ).toThrow(PasskeyControllerErrorMessage.NoRegistrationCeremony); }); + + it('returns options with userVerification required', () => { + const controller = createController(); + const regOpts = controller.generateRegistrationOptions(); + const authOpts = controller.generatePostRegistrationAuthenticationOptions( + { + registrationResponse: minimalRegistrationResponse( + undefined, + regOpts.challenge, + ), + }, + ); + + expect(authOpts.userVerification).toBe('required'); + }); }); describe('generateAuthenticationOptions', () => { @@ -447,6 +467,26 @@ describe('PasskeyController', () => { (authOpts.extensions as Record)?.prf, ).toBeDefined(); }); + + it('returns options with userVerification required', async () => { + setupRegistrationMocks(); + setupAuthenticationMocks(); + const controller = createController(); + const regOpts = controller.generateRegistrationOptions(); + + await enrollWithPostRegistrationAuth(controller, { + registrationResponse: minimalRegistrationResponse( + undefined, + regOpts.challenge, + ), + vaultKey: 'vault-key', + userHandle: regOpts.user.id, + }); + + const authOpts = controller.generateAuthenticationOptions(); + + expect(authOpts.userVerification).toBe('required'); + }); }); describe('protectVaultKeyWithPasskey', () => { @@ -1673,7 +1713,7 @@ describe('PasskeyController', () => { expect.objectContaining({ expectedOrigin: 'chrome-extension://abc123', expectedRPIDs: ['custom-rp.com'], - requireUserVerification: false, + requireUserVerification: true, }), ); }); @@ -1723,7 +1763,7 @@ describe('PasskeyController', () => { id: TEST_CREDENTIAL_ID, counter: 0, }), - requireUserVerification: false, + requireUserVerification: true, }), ); }); diff --git a/packages/passkey-controller/src/PasskeyController.ts b/packages/passkey-controller/src/PasskeyController.ts index f8f0298e50..7b5bc2b8b1 100644 --- a/packages/passkey-controller/src/PasskeyController.ts +++ b/packages/passkey-controller/src/PasskeyController.ts @@ -253,7 +253,7 @@ export class PasskeyController extends BaseController< ], timeout: WEBAUTHN_TIMEOUT_MS, authenticatorSelection: { - userVerification: 'preferred', + userVerification: 'required', authenticatorAttachment: 'platform', residentKey: 'preferred', }, @@ -316,7 +316,7 @@ export class PasskeyController extends BaseController< | undefined, }, ], - userVerification: 'preferred', + userVerification: 'required', hints: ['client-device', 'hybrid'], timeout: WEBAUTHN_TIMEOUT_MS, extensions, @@ -356,7 +356,7 @@ export class PasskeyController extends BaseController< transports: record.credential.transports, }, ], - userVerification: 'preferred', + userVerification: 'required', hints: ['client-device', 'hybrid'], timeout: WEBAUTHN_TIMEOUT_MS, extensions, @@ -413,7 +413,7 @@ export class PasskeyController extends BaseController< expectedChallenge: registrationCeremony.challenge, expectedOrigin: this.#expectedOrigin, expectedRPIDs: this.#expectedRPIDs, - requireUserVerification: false, + requireUserVerification: true, }).catch((error) => { log('Error verifying passkey registration response', error); throw new PasskeyControllerError( @@ -724,8 +724,7 @@ export class PasskeyController extends BaseController< counter: credential.counter, transports: credential.transports, }, - // UV optional for device compatibility; vault key remains password-gated. - requireUserVerification: false, + requireUserVerification: true, }).catch((error) => { log( 'Error verifying passkey authentication response',