Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/passkey-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 42 additions & 2 deletions packages/passkey-controller/src/PasskeyController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>)?.prf,
).toBeDefined();
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -447,6 +467,26 @@ describe('PasskeyController', () => {
(authOpts.extensions as Record<string, unknown>)?.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', () => {
Expand Down Expand Up @@ -1673,7 +1713,7 @@ describe('PasskeyController', () => {
expect.objectContaining({
expectedOrigin: 'chrome-extension://abc123',
expectedRPIDs: ['custom-rp.com'],
requireUserVerification: false,
requireUserVerification: true,
}),
);
});
Expand Down Expand Up @@ -1723,7 +1763,7 @@ describe('PasskeyController', () => {
id: TEST_CREDENTIAL_ID,
counter: 0,
}),
requireUserVerification: false,
requireUserVerification: true,
}),
);
});
Expand Down
11 changes: 5 additions & 6 deletions packages/passkey-controller/src/PasskeyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export class PasskeyController extends BaseController<
],
timeout: WEBAUTHN_TIMEOUT_MS,
authenticatorSelection: {
userVerification: 'preferred',
userVerification: 'required',
authenticatorAttachment: 'platform',
residentKey: 'preferred',
},
Expand Down Expand Up @@ -316,7 +316,7 @@ export class PasskeyController extends BaseController<
| undefined,
},
],
userVerification: 'preferred',
userVerification: 'required',
hints: ['client-device', 'hybrid'],
timeout: WEBAUTHN_TIMEOUT_MS,
extensions,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -413,7 +413,7 @@ export class PasskeyController extends BaseController<
expectedChallenge: registrationCeremony.challenge,
expectedOrigin: this.#expectedOrigin,
expectedRPIDs: this.#expectedRPIDs,
requireUserVerification: false,
requireUserVerification: true,
Comment thread
cursor[bot] marked this conversation as resolved.
}).catch((error) => {
log('Error verifying passkey registration response', error);
throw new PasskeyControllerError(
Expand Down Expand Up @@ -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',
Expand Down
Loading