Skip to content

Commit fc893f8

Browse files
authored
feat(webauthn): make rpId the first argument of credentials.create() (#41259)
1 parent 0573573 commit fc893f8

6 files changed

Lines changed: 60 additions & 51 deletions

File tree

docs/src/api/class-credentials.md

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ There are two common ways to use it:
2020
const context = await browser.newContext();
2121

2222
// A passkey your backend already provisioned for a test user.
23-
await context.credentials.create({
24-
rpId: 'example.com',
23+
await context.credentials.create('example.com', {
2524
id: knownCredentialId, // base64url
2625
userHandle: knownUserHandle, // base64url
2726
privateKey: knownPrivateKey, // base64url PKCS#8 (DER)
@@ -54,7 +53,7 @@ fs.writeFileSync('playwright/.auth/passkey.json', JSON.stringify(credential));
5453
// later test: seed the captured passkey so the app starts already enrolled.
5554
const credential = JSON.parse(fs.readFileSync('playwright/.auth/passkey.json', 'utf8'));
5655
const context = await browser.newContext();
57-
await context.credentials.create(credential);
56+
await context.credentials.create(credential.rpId, credential);
5857
await context.credentials.install();
5958

6059
const page = await context.newPage();
@@ -106,14 +105,35 @@ To **import a known credential**, supply all four of `id`, `userHandle`, `privat
106105

107106
Call [`method: Credentials.install`] before navigating to a page that uses WebAuthn.
108107

109-
### param: Credentials.create.options
108+
### param: Credentials.create.rpId
110109
* since: v1.61
111-
- `options` <[Object]>
112-
- `rpId` <[string]> Relying party id (typically the site's effective domain).
113-
- `id` ?<[string]> Base64url-encoded credential id. Auto-generated if omitted.
114-
- `userHandle` ?<[string]> Base64url-encoded user handle. Auto-generated if omitted.
115-
- `privateKey` ?<[string]> Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted.
116-
- `publicKey` ?<[string]> Base64url-encoded SPKI (DER) public key. Auto-generated if omitted.
110+
- `rpId` <[string]>
111+
112+
Relying party id (typically the site's effective domain).
113+
114+
### option: Credentials.create.id
115+
* since: v1.61
116+
- `id` <[string]>
117+
118+
Base64url-encoded credential id. Auto-generated if omitted.
119+
120+
### option: Credentials.create.userHandle
121+
* since: v1.61
122+
- `userHandle` <[string]>
123+
124+
Base64url-encoded user handle. Auto-generated if omitted.
125+
126+
### option: Credentials.create.privateKey
127+
* since: v1.61
128+
- `privateKey` <[string]>
129+
130+
Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted.
131+
132+
### option: Credentials.create.publicKey
133+
* since: v1.61
134+
- `publicKey` <[string]>
135+
136+
Base64url-encoded SPKI (DER) public key. Auto-generated if omitted.
117137

118138
## async method: Credentials.delete
119139
* since: v1.61

docs/src/auth.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,7 @@ export * from '@playwright/test';
416416
export const test = baseTest.extend({
417417
context: async ({ context }, use) => {
418418
// A passkey your backend provisioned for the test user.
419-
await context.credentials.create({
420-
rpId: 'example.com',
419+
await context.credentials.create('example.com', {
421420
id: process.env.PASSKEY_ID,
422421
userHandle: process.env.PASSKEY_USER_HANDLE,
423422
privateKey: process.env.PASSKEY_PRIVATE_KEY,
@@ -457,7 +456,7 @@ export * from '@playwright/test';
457456
export const test = baseTest.extend({
458457
context: async ({ context }, use) => {
459458
const credential = JSON.parse(fs.readFileSync('playwright/.auth/passkey.json', 'utf8'));
460-
await context.credentials.create(credential);
459+
await context.credentials.create(credential.rpId, credential);
461460
await context.credentials.install();
462461
await use(context);
463462
},

packages/playwright-client/types/types.d.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18820,8 +18820,7 @@ export interface Coverage {
1882018820
* const context = await browser.newContext();
1882118821
*
1882218822
* // A passkey your backend already provisioned for a test user.
18823-
* await context.credentials.create({
18824-
* rpId: 'example.com',
18823+
* await context.credentials.create('example.com', {
1882518824
* id: knownCredentialId, // base64url
1882618825
* userHandle: knownUserHandle, // base64url
1882718826
* privateKey: knownPrivateKey, // base64url PKCS#8 (DER)
@@ -18854,7 +18853,7 @@ export interface Coverage {
1885418853
* // later test: seed the captured passkey so the app starts already enrolled.
1885518854
* const credential = JSON.parse(fs.readFileSync('playwright/.auth/passkey.json', 'utf8'));
1885618855
* const context = await browser.newContext();
18857-
* await context.credentials.create(credential);
18856+
* await context.credentials.create(credential.rpId, credential);
1885818857
* await context.credentials.install();
1885918858
*
1886018859
* const page = await context.newPage();
@@ -18877,9 +18876,10 @@ export interface Credentials {
1887718876
*
1887818877
* Call [credentials.install()](https://playwright.dev/docs/api/class-credentials#credentials-install) before
1887918878
* navigating to a page that uses WebAuthn.
18879+
* @param rpId Relying party id (typically the site's effective domain).
1888018880
* @param options
1888118881
*/
18882-
create(options: {
18882+
create(rpId: string, options?: {
1888318883
/**
1888418884
* Base64url-encoded credential id. Auto-generated if omitted.
1888518885
*/
@@ -18895,11 +18895,6 @@ export interface Credentials {
1889518895
*/
1889618896
publicKey?: string;
1889718897

18898-
/**
18899-
* Relying party id (typically the site's effective domain).
18900-
*/
18901-
rpId: string;
18902-
1890318898
/**
1890418899
* Base64url-encoded user handle. Auto-generated if omitted.
1890518900
*/
@@ -18933,22 +18928,22 @@ export interface Credentials {
1893318928

1893418929
/**
1893518930
* Removes a credential from the authenticator by its id. Works for any credential currently held — both those seeded
18936-
* with [credentials.create(options)](https://playwright.dev/docs/api/class-credentials#credentials-create) and those
18937-
* the page registered itself by calling `navigator.credentials.create()`.
18931+
* with [credentials.create(rpId[, options])](https://playwright.dev/docs/api/class-credentials#credentials-create)
18932+
* and those the page registered itself by calling `navigator.credentials.create()`.
1893818933
* @param id Base64url-encoded credential id.
1893918934
*/
1894018935
delete(id: string): Promise<void>;
1894118936

1894218937
/**
1894318938
* Returns every credential currently held by the authenticator, optionally filtered by `rpId` or `id`. This includes
1894418939
* both credentials seeded with
18945-
* [credentials.create(options)](https://playwright.dev/docs/api/class-credentials#credentials-create) and credentials
18946-
* the page registered itself by calling `navigator.credentials.create()`.
18940+
* [credentials.create(rpId[, options])](https://playwright.dev/docs/api/class-credentials#credentials-create) and
18941+
* credentials the page registered itself by calling `navigator.credentials.create()`.
1894718942
*
1894818943
* Each returned credential includes its `privateKey` and `publicKey`, so a passkey the app just registered can be
1894918944
* saved and re-seeded into a later test with
18950-
* [credentials.create(options)](https://playwright.dev/docs/api/class-credentials#credentials-create) — see the
18951-
* second example in the class overview.
18945+
* [credentials.create(rpId[, options])](https://playwright.dev/docs/api/class-credentials#credentials-create) — see
18946+
* the second example in the class overview.
1895218947
* @param options
1895318948
*/
1895418949
get(options?: {
@@ -18980,7 +18975,7 @@ export interface Credentials {
1898018975
*
1898118976
* Required: until `install()` is called, no interception is in place and the page sees the platform's native (or
1898218977
* absent) WebAuthn behaviour. Seeding credentials with
18983-
* [credentials.create(options)](https://playwright.dev/docs/api/class-credentials#credentials-create) without
18978+
* [credentials.create(rpId[, options])](https://playwright.dev/docs/api/class-credentials#credentials-create) without
1898418979
* `install()` populates the authenticator, but the page will never see those credentials.
1898518980
*/
1898618981
install(): Promise<void>;

packages/playwright-core/src/client/credentials.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ export class Credentials implements api.Credentials {
2929
await this._browserContext._channel.credentialsInstall({});
3030
}
3131

32-
async create(options: channels.BrowserContextCredentialsCreateParams): Promise<channels.VirtualCredential> {
33-
const { credential } = await this._browserContext._channel.credentialsCreate(options);
32+
async create(rpId: string, options: Omit<channels.BrowserContextCredentialsCreateParams, 'rpId'> = {}): Promise<channels.VirtualCredential> {
33+
const { credential } = await this._browserContext._channel.credentialsCreate({ ...options, rpId });
3434
return credential;
3535
}
3636

packages/playwright-core/types/types.d.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18820,8 +18820,7 @@ export interface Coverage {
1882018820
* const context = await browser.newContext();
1882118821
*
1882218822
* // A passkey your backend already provisioned for a test user.
18823-
* await context.credentials.create({
18824-
* rpId: 'example.com',
18823+
* await context.credentials.create('example.com', {
1882518824
* id: knownCredentialId, // base64url
1882618825
* userHandle: knownUserHandle, // base64url
1882718826
* privateKey: knownPrivateKey, // base64url PKCS#8 (DER)
@@ -18854,7 +18853,7 @@ export interface Coverage {
1885418853
* // later test: seed the captured passkey so the app starts already enrolled.
1885518854
* const credential = JSON.parse(fs.readFileSync('playwright/.auth/passkey.json', 'utf8'));
1885618855
* const context = await browser.newContext();
18857-
* await context.credentials.create(credential);
18856+
* await context.credentials.create(credential.rpId, credential);
1885818857
* await context.credentials.install();
1885918858
*
1886018859
* const page = await context.newPage();
@@ -18877,9 +18876,10 @@ export interface Credentials {
1887718876
*
1887818877
* Call [credentials.install()](https://playwright.dev/docs/api/class-credentials#credentials-install) before
1887918878
* navigating to a page that uses WebAuthn.
18879+
* @param rpId Relying party id (typically the site's effective domain).
1888018880
* @param options
1888118881
*/
18882-
create(options: {
18882+
create(rpId: string, options?: {
1888318883
/**
1888418884
* Base64url-encoded credential id. Auto-generated if omitted.
1888518885
*/
@@ -18895,11 +18895,6 @@ export interface Credentials {
1889518895
*/
1889618896
publicKey?: string;
1889718897

18898-
/**
18899-
* Relying party id (typically the site's effective domain).
18900-
*/
18901-
rpId: string;
18902-
1890318898
/**
1890418899
* Base64url-encoded user handle. Auto-generated if omitted.
1890518900
*/
@@ -18933,22 +18928,22 @@ export interface Credentials {
1893318928

1893418929
/**
1893518930
* Removes a credential from the authenticator by its id. Works for any credential currently held — both those seeded
18936-
* with [credentials.create(options)](https://playwright.dev/docs/api/class-credentials#credentials-create) and those
18937-
* the page registered itself by calling `navigator.credentials.create()`.
18931+
* with [credentials.create(rpId[, options])](https://playwright.dev/docs/api/class-credentials#credentials-create)
18932+
* and those the page registered itself by calling `navigator.credentials.create()`.
1893818933
* @param id Base64url-encoded credential id.
1893918934
*/
1894018935
delete(id: string): Promise<void>;
1894118936

1894218937
/**
1894318938
* Returns every credential currently held by the authenticator, optionally filtered by `rpId` or `id`. This includes
1894418939
* both credentials seeded with
18945-
* [credentials.create(options)](https://playwright.dev/docs/api/class-credentials#credentials-create) and credentials
18946-
* the page registered itself by calling `navigator.credentials.create()`.
18940+
* [credentials.create(rpId[, options])](https://playwright.dev/docs/api/class-credentials#credentials-create) and
18941+
* credentials the page registered itself by calling `navigator.credentials.create()`.
1894718942
*
1894818943
* Each returned credential includes its `privateKey` and `publicKey`, so a passkey the app just registered can be
1894918944
* saved and re-seeded into a later test with
18950-
* [credentials.create(options)](https://playwright.dev/docs/api/class-credentials#credentials-create) — see the
18951-
* second example in the class overview.
18945+
* [credentials.create(rpId[, options])](https://playwright.dev/docs/api/class-credentials#credentials-create) — see
18946+
* the second example in the class overview.
1895218947
* @param options
1895318948
*/
1895418949
get(options?: {
@@ -18980,7 +18975,7 @@ export interface Credentials {
1898018975
*
1898118976
* Required: until `install()` is called, no interception is in place and the page sees the platform's native (or
1898218977
* absent) WebAuthn behaviour. Seeding credentials with
18983-
* [credentials.create(options)](https://playwright.dev/docs/api/class-credentials#credentials-create) without
18978+
* [credentials.create(rpId[, options])](https://playwright.dev/docs/api/class-credentials#credentials-create) without
1898418979
* `install()` populates the authenticator, but the page will never see those credentials.
1898518980
*/
1898618981
install(): Promise<void>;

tests/library/browsercontext-webauthn.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { browserTest as it, expect } from '../config/browserTest';
1919
it('should not intercept navigator.credentials without install()', async ({ contextFactory, server }) => {
2020
const context = await contextFactory();
2121
// Seed a credential, but do not install the interceptor.
22-
await context.credentials.create({ rpId: server.HOSTNAME });
22+
await context.credentials.create(server.HOSTNAME);
2323
const page = await context.newPage();
2424
await page.goto(server.EMPTY_PAGE);
2525

@@ -31,11 +31,11 @@ it('should seed a known credential and authenticate', async ({ contextFactory, s
3131
// This is the easiest way to create credentials. In practice, this
3232
// probably comes from environment.
3333
const source = await contextFactory();
34-
const known = await source.credentials.create({ rpId: server.HOSTNAME });
34+
const known = await source.credentials.create(server.HOSTNAME);
3535

3636
// A fresh context imports the known credential and signs in with it.
3737
const context = await contextFactory();
38-
await context.credentials.create(known);
38+
await context.credentials.create(known.rpId, known);
3939
await context.credentials.install();
4040
const page = await context.newPage();
4141
await page.goto(server.EMPTY_PAGE);
@@ -139,7 +139,7 @@ it('should capture a page-created credential and reuse it in another context', a
139139

140140
// Reuse the captured passkey in a fresh context and sign in with it.
141141
const context = await contextFactory();
142-
await context.credentials.create(captured);
142+
await context.credentials.create(captured.rpId, captured);
143143
await context.credentials.install();
144144
const page = await context.newPage();
145145
await page.goto(server.EMPTY_PAGE);

0 commit comments

Comments
 (0)