chore(credentials): remove setUserVerified (#41195)

This commit is contained in:
Dmitry Gozman
2026-06-08 09:45:34 -07:00
committed by GitHub
parent 26e3dc2b8d
commit 3ba155ee28
12 changed files with 10 additions and 217 deletions
-40
View File
@@ -71,8 +71,6 @@ await page.goto('https://example.com/login');
usernameless passkey flows resolve them.
- Fresh keys are ECDSA P-256 (COSE algorithm `-7`). An omitted `id` or `userHandle` is
filled with 16 random bytes.
- User verification is **on** by default — every assertion and attestation reports the user as
verified. Toggle this with [`method: Credentials.setUserVerified`].
## async method: Credentials.install
* since: v1.61
@@ -158,41 +156,3 @@ Only return credentials for this relying party id.
- `id` <[string]>
Only return the credential with this base64url-encoded id.
## async method: Credentials.setUserVerified
* since: v1.61
Controls whether the virtual authenticator reports the user as **verified**. This is a
context-wide setting (default `true`) that toggles the user-verified (UV) flag in the
`authenticatorData` of every subsequent `navigator.credentials.create()` and
`navigator.credentials.get()` ceremony.
When set to `false`, ceremonies still **succeed**, but the resulting assertion/attestation reports
that user verification was *not* performed — the user-present (UP) flag stays set. It does **not**
simulate a cancelled or denied prompt, and it does not reject the call. Use it to test how your
relying party or app handles an assertion that lacks user verification, for example requiring
step-up authentication.
**Usage**
```js
await context.credentials.install();
await context.credentials.create({ rpId: 'example.com' });
// Report assertions as NOT user-verified, e.g. a presence-only tap.
await context.credentials.setUserVerified(false);
const page = await context.newPage();
await page.goto('https://example.com/login');
// Assert the app requires step-up auth or rejects the unverified sign-in.
// Restore verified assertions for later steps.
await context.credentials.setUserVerified(true);
```
### param: Credentials.setUserVerified.value
* since: v1.61
- `value` <[boolean]>
`true` to report assertions and attestations as user-verified (default), `false` to report them as
not user-verified.
-41
View File
@@ -1,41 +0,0 @@
// Demonstrates `context.credentials.setUserVerified()` — simulating a user
// refusing biometric verification (e.g. a wrong fingerprint).
//
// We configure webauthn.io to require user verification, then flip the
// authenticator's UV flag off and try to log in. The relying party sees
// UV=0 in the assertion flags and rejects the login. Flipping UV back on
// makes the next attempt succeed.
import { chromium } from 'playwright';
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 250 });
const context = await browser.newContext();
await context.credentials.install();
const page = await context.newPage();
await page.goto('https://webauthn.io/');
// Force the relying party to require user verification at both ends.
await page.locator('button:has-text("Advanced Settings")').click();
await page.locator('#optRegUserVerification').selectOption('required');
await page.locator('#optAuthUserVerification').selectOption('required');
await page.locator('#input-email').fill(`pw-demo-${Date.now()}`);
await page.locator('#register-button').click();
await page.getByText(/success!.*try to authenticate/i).waitFor();
console.log(` ✓ registered (UV=required)`);
// Simulate failed biometric — UV bit will be 0 in the next assertion.
await context.credentials.setUserVerified(false);
await page.locator('#login-button').click();
await page.getByText(/authentication failed/i).waitFor();
console.log(` ✓ login rejected: server got UV=0 but required UV=1`);
// Recovery: biometric succeeds, UV=1 in the assertion.
await context.credentials.setUserVerified(true);
await page.locator('#login-button').click();
await page.getByText(/you're logged in/i).waitFor();
console.log(` ✓ login succeeded after UV restored`);
await browser.close();
})();
+4 -5
View File
@@ -107,11 +107,10 @@ export const methodMetainfo = new Map<string, MethodMetainfo>([
['BrowserContext.clockRunFor', { title: 'Run clock "{ticksNumber|ticksString}"', }],
['BrowserContext.clockSetFixedTime', { title: 'Set fixed time "{timeNumber|timeString}"', }],
['BrowserContext.clockSetSystemTime', { title: 'Set system time "{timeNumber|timeString}"', }],
['BrowserContext.credentialsInstall', { title: 'Install virtual WebAuthn authenticator', }],
['BrowserContext.credentialsCreate', { title: 'Create virtual credential for "{rpId}"', }],
['BrowserContext.credentialsGet', { title: 'Get virtual credentials', }],
['BrowserContext.credentialsDelete', { title: 'Delete virtual credential', }],
['BrowserContext.credentialsSetUserVerified', { title: 'Set virtual authenticator user verified', }],
['BrowserContext.credentialsInstall', { title: 'Install virtual WebAuthn authenticator', group: 'configuration', }],
['BrowserContext.credentialsCreate', { title: 'Create virtual credential for "{rpId}"', group: 'configuration', }],
['BrowserContext.credentialsGet', { title: 'Get virtual credentials', group: 'configuration', }],
['BrowserContext.credentialsDelete', { title: 'Delete virtual credential', group: 'configuration', }],
['BrowserType.launch', { title: 'Launch browser', }],
['BrowserType.launchPersistentContext', { title: 'Launch persistent context', }],
['BrowserType.connectOverCDP', { title: 'Connect over CDP', }],
-32
View File
@@ -19596,38 +19596,6 @@ export interface Credentials {
* `install()` populates the authenticator, but the page will never see those credentials.
*/
install(): Promise<void>;
/**
* Controls whether the virtual authenticator reports the user as **verified**. This is a context-wide setting
* (default `true`) that toggles the user-verified (UV) flag in the `authenticatorData` of every subsequent
* `navigator.credentials.create()` and `navigator.credentials.get()` ceremony.
*
* When set to `false`, ceremonies still **succeed**, but the resulting assertion/attestation reports that user
* verification was *not* performed — the user-present (UP) flag stays set. It does **not** simulate a cancelled or
* denied prompt, and it does not reject the call. Use it to test how your relying party or app handles an assertion
* that lacks user verification, for example requiring step-up authentication.
*
* **Usage**
*
* ```js
* await context.credentials.install();
* await context.credentials.create({ rpId: 'example.com' });
*
* // Report assertions as NOT user-verified, e.g. a presence-only tap.
* await context.credentials.setUserVerified(false);
*
* const page = await context.newPage();
* await page.goto('https://example.com/login');
* // Assert the app requires step-up auth or rejects the unverified sign-in.
*
* // Restore verified assertions for later steps.
* await context.credentials.setUserVerified(true);
* ```
*
* @param value `true` to report assertions and attestations as user-verified (default), `false` to report them as not
* user-verified.
*/
setUserVerified(value: boolean): Promise<void>;
}
/**
@@ -42,8 +42,4 @@ export class Credentials implements api.Credentials {
async delete(id: string): Promise<void> {
await this._browserContext._channel.credentialsDelete({ id });
}
async setUserVerified(value: boolean): Promise<void> {
await this._browserContext._channel.credentialsSetUserVerified({ value });
}
}
@@ -956,10 +956,6 @@ scheme.BrowserContextCredentialsDeleteParams = tObject({
id: tString,
});
scheme.BrowserContextCredentialsDeleteResult = tOptional(tObject({}));
scheme.BrowserContextCredentialsSetUserVerifiedParams = tObject({
value: tBoolean,
});
scheme.BrowserContextCredentialsSetUserVerifiedResult = tOptional(tObject({}));
scheme.BrowserTypeInitializer = tObject({
executablePath: tString,
name: tString,
@@ -44,7 +44,6 @@ export class Credentials {
private _initScripts: InitScript[] = [];
private _installed = false;
private _registry = new Map<string, CredentialRecord>();
private _userVerified = true;
constructor(browserContext: BrowserContext) {
this._browserContext = browserContext;
@@ -91,10 +90,6 @@ export class Credentials {
this._registry.delete(id);
}
setUserVerified(value: boolean) {
this._userVerified = value;
}
async dispose(progress: Progress) {
await progress.race(Promise.all(this._initScripts.map(s => s.dispose())));
this._initScripts = [];
@@ -160,7 +155,7 @@ export class Credentials {
crossOrigin: false,
}));
const rpIdHash = crypto.createHash('sha256').update(rpId).digest();
const flags = 0x01 | (this._userVerified ? 0x04 : 0) | 0x40; // UP | UV? | AT
const flags = 0x01 | 0x04 | 0x40; // UP | UV | AT
const signCountBuf = u32ToBytes(record.signCount);
const cosePublicKey = encodeCoseEs256PublicKey(pair.publicKey);
const credIdLenBuf = Buffer.from([(credentialId.length >> 8) & 0xff, credentialId.length & 0xff]);
@@ -205,7 +200,7 @@ export class Credentials {
crossOrigin: false,
}));
const rpIdHash = crypto.createHash('sha256').update(rpId).digest();
const flags = 0x01 | (this._userVerified ? 0x04 : 0); // UP | UV?
const flags = 0x01 | 0x04; // UP | UV
candidate.signCount += 1;
const signCountBuf = u32ToBytes(candidate.signCount);
const authData = Buffer.concat([rpIdHash, Buffer.from([flags]), signCountBuf]);
@@ -422,10 +422,6 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
await progress.race(this._context.credentials.delete(params.id));
}
async credentialsSetUserVerified(params: channels.BrowserContextCredentialsSetUserVerifiedParams, progress: Progress): Promise<channels.BrowserContextCredentialsSetUserVerifiedResult> {
this._context.credentials.setUserVerified(params.value);
}
async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams, progress: Progress): Promise<void> {
if (params.enabled)
this._subscriptions.add(params.event);
-32
View File
@@ -19596,38 +19596,6 @@ export interface Credentials {
* `install()` populates the authenticator, but the page will never see those credentials.
*/
install(): Promise<void>;
/**
* Controls whether the virtual authenticator reports the user as **verified**. This is a context-wide setting
* (default `true`) that toggles the user-verified (UV) flag in the `authenticatorData` of every subsequent
* `navigator.credentials.create()` and `navigator.credentials.get()` ceremony.
*
* When set to `false`, ceremonies still **succeed**, but the resulting assertion/attestation reports that user
* verification was *not* performed — the user-present (UP) flag stays set. It does **not** simulate a cancelled or
* denied prompt, and it does not reject the call. Use it to test how your relying party or app handles an assertion
* that lacks user verification, for example requiring step-up authentication.
*
* **Usage**
*
* ```js
* await context.credentials.install();
* await context.credentials.create({ rpId: 'example.com' });
*
* // Report assertions as NOT user-verified, e.g. a presence-only tap.
* await context.credentials.setUserVerified(false);
*
* const page = await context.newPage();
* await page.goto('https://example.com/login');
* // Assert the app requires step-up auth or rejects the unverified sign-in.
*
* // Restore verified assertions for later steps.
* await context.credentials.setUserVerified(true);
* ```
*
* @param value `true` to report assertions and attestations as user-verified (default), `false` to report them as not
* user-verified.
*/
setUserVerified(value: boolean): Promise<void>;
}
/**
+4 -5
View File
@@ -316,9 +316,11 @@ BrowserContext:
credentialsInstall:
title: Install virtual WebAuthn authenticator
group: configuration
credentialsCreate:
title: Create virtual credential for "{rpId}"
group: configuration
parameters:
rpId: string
id: string?
@@ -330,6 +332,7 @@ BrowserContext:
credentialsGet:
title: Get virtual credentials
group: configuration
parameters:
rpId: string?
id: string?
@@ -340,14 +343,10 @@ BrowserContext:
credentialsDelete:
title: Delete virtual credential
group: configuration
parameters:
id: string
credentialsSetUserVerified:
title: Set virtual authenticator user verified
parameters:
value: boolean
events:
bindingCall:
-8
View File
@@ -1366,7 +1366,6 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, Channe
credentialsCreate(params: BrowserContextCredentialsCreateParams, progress?: Progress): Promise<BrowserContextCredentialsCreateResult>;
credentialsGet(params: BrowserContextCredentialsGetParams, progress?: Progress): Promise<BrowserContextCredentialsGetResult>;
credentialsDelete(params: BrowserContextCredentialsDeleteParams, progress?: Progress): Promise<BrowserContextCredentialsDeleteResult>;
credentialsSetUserVerified(params: BrowserContextCredentialsSetUserVerifiedParams, progress?: Progress): Promise<BrowserContextCredentialsSetUserVerifiedResult>;
}
export type BrowserContextBindingCallEvent = {
binding: BindingCallChannel,
@@ -1785,13 +1784,6 @@ export type BrowserContextCredentialsDeleteOptions = {
};
export type BrowserContextCredentialsDeleteResult = void;
export type BrowserContextCredentialsSetUserVerifiedParams = {
value: boolean,
};
export type BrowserContextCredentialsSetUserVerifiedOptions = {
};
export type BrowserContextCredentialsSetUserVerifiedResult = void;
export interface BrowserContextEvents {
'bindingCall': BrowserContextBindingCallEvent;
@@ -29,41 +29,6 @@ it('should not intercept navigator.credentials without install()', async ({ cont
expect(intercepted).toBe(false);
});
it('should toggle user-verified flag', async ({ contextFactory, server }) => {
const context = await contextFactory();
await context.credentials.install();
const seeded = await context.credentials.create({ rpId: server.HOSTNAME });
await context.credentials.setUserVerified(false);
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
const flagsByte = await page.evaluate(async ({ rpId, credentialId }) => {
const b64UrlToBytes = (s: string) => {
let str = s.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4)
str += '=';
const bin = atob(str);
const u8 = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++)
u8[i] = bin.charCodeAt(i);
return u8;
};
const cred = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array(32),
rpId,
allowCredentials: [{ type: 'public-key', id: b64UrlToBytes(credentialId) }],
},
}) as PublicKeyCredential;
const resp = cred.response as AuthenticatorAssertionResponse;
return new Uint8Array(resp.authenticatorData)[32];
}, { rpId: server.HOSTNAME, credentialId: seeded.id });
// UV bit (0x04) should be unset; UP bit (0x01) still set.
expect(flagsByte & 0x04).toBe(0);
expect(flagsByte & 0x01).toBe(0x01);
});
it('should seed a known credential and authenticate', async ({ contextFactory, server }) => {
// This is the easiest way to create credentials. In practice, this
// probably comes from environment.