mirror of
https://github.com/hanzoai/playwright.git
synced 2026-09-24 17:01:53 +00:00
chore(credentials): remove setUserVerified (#41195)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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
@@ -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
@@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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:
|
||||
|
||||
Vendored
-8
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user