Skip to content

Commit

Permalink
chore: client certificates api review (#31826)
Browse files Browse the repository at this point in the history
  • Loading branch information
mxschmitt authored Jul 23, 2024
1 parent 383e4b3 commit c4862c0
Show file tree
Hide file tree
Showing 14 changed files with 284 additions and 376 deletions.
19 changes: 11 additions & 8 deletions docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,14 +523,17 @@ Does not enforce fixed viewport, allows resizing window in the headed mode.

## context-option-clientCertificates
- `clientCertificates` <[Array]<[Object]>>
- `url` <[string]> Glob pattern to match the URLs that the certificate is valid for.
- `certs` <[Array]<[Object]>> List of client certificates to be used.
- `certPath` ?<[string]> Path to the file with the certificate in PEM format.
- `keyPath` ?<[string]> Path to the file with the private key in PEM format.
- `pfxPath` ?<[string]> Path to the PFX or PKCS12 encoded private key and certificate chain.
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).

An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided with a glob pattern to match the URLs that the certificate is valid for.
- `origin` <[string]> Glob pattern to match against the request origin that the certificate is valid for.
- `certPath` ?<[string]> Path to the file with the certificate in PEM format.
- `keyPath` ?<[string]> Path to the file with the private key in PEM format.
- `pfxPath` ?<[string]> Path to the PFX or PKCS12 encoded private key and certificate chain.
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).

TLS Client Authentication allows the server to request a client certificate and verify it.

**Details**

An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the certficiate is encrypted. If the certificate is valid only for specific origins, the `origin` property should be provided with a glob pattern to match the origins that the certificate is valid for.

:::note
Using Client Certificates in combination with Proxy Servers is not supported.
Expand Down
24 changes: 8 additions & 16 deletions docs/src/test-api/class-testoptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,22 +148,14 @@ export default defineConfig({
import { defineConfig } from '@playwright/test';

export default defineConfig({
projects: [
{
name: 'Microsoft Edge',
use: {
...devices['Desktop Edge'],
clientCertificates: [{
url: 'https://example.com/**',
certs: [{
certPath: './cert.pem',
keyPath: './key.pem',
passphrase: 'mysecretpassword',
}],
}],
},
},
]
use: {
clientCertificates: [{
origin: 'https://example.com',
certPath: './cert.pem',
keyPath: './key.pem',
passphrase: 'mysecretpassword',
}],
},
});
```

Expand Down
20 changes: 8 additions & 12 deletions packages/playwright-core/src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,20 +550,16 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
return 'deny';
}

export async function toClientCertificatesProtocol(clientCertificates?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
if (!clientCertificates)
export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
if (!certs)
return undefined;
return await Promise.all(clientCertificates.map(async clientCertificate => {
return await Promise.all(certs.map(async cert => {
return {
url: clientCertificate.url,
certs: await Promise.all(clientCertificate.certs.map(async cert => {
return {
cert: cert.certPath ? await fs.promises.readFile(cert.certPath) : undefined,
key: cert.keyPath ? await fs.promises.readFile(cert.keyPath) : undefined,
pfx: cert.pfxPath ? await fs.promises.readFile(cert.pfxPath) : undefined,
passphrase: cert.passphrase,
};
}))
origin: cert.origin,
cert: cert.certPath ? await fs.promises.readFile(cert.certPath) : undefined,
key: cert.keyPath ? await fs.promises.readFile(cert.keyPath) : undefined,
pfx: cert.pfxPath ? await fs.promises.readFile(cert.pfxPath) : undefined,
passphrase: cert.passphrase,
};
}));
}
12 changes: 5 additions & 7 deletions packages/playwright-core/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,11 @@ export type LifecycleEvent = channels.LifecycleEvent;
export const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle', 'commit']);

export type ClientCertificate = {
url: string;
certs: {
certPath?: string;
keyPath?: string;
pfxPath?: string;
passphrase?: string;
}[];
origin: string;
certPath?: string;
keyPath?: string;
pfxPath?: string;
passphrase?: string;
};

export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'viewport' | 'noDefaultViewport' | 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'recordHar' | 'colorScheme' | 'reducedMotion' | 'forcedColors' | 'acceptDownloads'> & {
Expand Down
60 changes: 25 additions & 35 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,13 +337,11 @@ scheme.PlaywrightNewRequestParams = tObject({
ignoreHTTPSErrors: tOptional(tBoolean),
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
clientCertificates: tOptional(tArray(tObject({
url: tString,
certs: tArray(tObject({
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
})),
origin: tString,
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
}))),
httpCredentials: tOptional(tObject({
username: tString,
Expand Down Expand Up @@ -547,13 +545,11 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
})),
ignoreHTTPSErrors: tOptional(tBoolean),
clientCertificates: tOptional(tArray(tObject({
url: tString,
certs: tArray(tObject({
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
})),
origin: tString,
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
}))),
javaScriptEnabled: tOptional(tBoolean),
bypassCSP: tOptional(tBoolean),
Expand Down Expand Up @@ -635,13 +631,11 @@ scheme.BrowserNewContextParams = tObject({
})),
ignoreHTTPSErrors: tOptional(tBoolean),
clientCertificates: tOptional(tArray(tObject({
url: tString,
certs: tArray(tObject({
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
})),
origin: tString,
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
}))),
javaScriptEnabled: tOptional(tBoolean),
bypassCSP: tOptional(tBoolean),
Expand Down Expand Up @@ -706,13 +700,11 @@ scheme.BrowserNewContextForReuseParams = tObject({
})),
ignoreHTTPSErrors: tOptional(tBoolean),
clientCertificates: tOptional(tArray(tObject({
url: tString,
certs: tArray(tObject({
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
})),
origin: tString,
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
}))),
javaScriptEnabled: tOptional(tBoolean),
bypassCSP: tOptional(tBoolean),
Expand Down Expand Up @@ -2526,13 +2518,11 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
})),
ignoreHTTPSErrors: tOptional(tBoolean),
clientCertificates: tOptional(tArray(tObject({
url: tString,
certs: tArray(tObject({
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
})),
origin: tString,
cert: tOptional(tBinary),
key: tOptional(tBinary),
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
}))),
javaScriptEnabled: tOptional(tBoolean),
bypassCSP: tOptional(tBoolean),
Expand Down
26 changes: 11 additions & 15 deletions packages/playwright-core/src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,21 +725,17 @@ export function verifyGeolocation(geolocation?: types.Geolocation) {
export function verifyClientCertificates(clientCertificates?: channels.BrowserNewContextParams['clientCertificates']) {
if (!clientCertificates)
return;
for (const { url, certs } of clientCertificates) {
if (!url)
throw new Error(`clientCertificates.url is required`);
if (!certs.length)
throw new Error('No certs specified for url: ' + url);
for (const cert of certs) {
if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx)
throw new Error('None of cert, key, passphrase or pfx is specified');
if (cert.cert && !cert.key)
throw new Error('cert is specified without key');
if (!cert.cert && cert.key)
throw new Error('key is specified without cert');
if (cert.pfx && (cert.cert || cert.key))
throw new Error('pfx is specified together with cert, key or passphrase');
}
for (const cert of clientCertificates) {
if (!cert.origin)
throw new Error(`clientCertificates.origin is required`);
if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx)
throw new Error('None of cert, key, passphrase or pfx is specified');
if (cert.cert && !cert.key)
throw new Error('cert is specified without key');
if (!cert.cert && cert.key)
throw new Error('key is specified without cert');
if (cert.pfx && (cert.cert || cert.key))
throw new Error('pfx is specified together with cert, key or passphrase');
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export abstract class APIRequestContext extends SdkObject {
maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects,
timeout,
deadline,
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.toString()),
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.origin),
__testHookLookup: (params as any).__testHookLookup,
};
if (process.env.PWTEST_UNSUPPORTED_CUSTOM_CA && isUnderTest())
Expand Down Expand Up @@ -357,7 +357,7 @@ export abstract class APIRequestContext extends SdkObject {
maxRedirects: options.maxRedirects - 1,
timeout: options.timeout,
deadline: options.deadline,
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, url.toString()),
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, url.origin),
__testHookLookup: options.__testHookLookup,
};
// rejectUnauthorized = undefined is treated as true in node 12.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class SocksProxyConnection {
host: this.host,
port: this.port,
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
...clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, `https://${this.host}:${this.port}/`),
...clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, `https://${this.host}:${this.port}`),
};
if (!net.isIP(this.host))
tlsOptions.servername = this.host;
Expand Down Expand Up @@ -183,7 +183,7 @@ export function clientCertificatesToTLSOptions(
const matchingCerts = clientCertificates?.filter(c => {
let regex: RegExp | undefined = (c as any)[kClientCertificatesGlobRegex];
if (!regex) {
regex = globToRegex(c.url);
regex = globToRegex(c.origin);
(c as any)[kClientCertificatesGlobRegex] = regex;
}
regex.lastIndex = 0;
Expand All @@ -196,15 +196,13 @@ export function clientCertificatesToTLSOptions(
key: [] as { pem: Buffer, passphrase?: string }[],
cert: [] as Buffer[],
};
for (const { certs } of matchingCerts) {
for (const cert of certs) {
if (cert.cert)
tlsOptions.cert.push(cert.cert);
if (cert.key)
tlsOptions.key.push({ pem: cert.key, passphrase: cert.passphrase });
if (cert.pfx)
tlsOptions.pfx.push({ buf: cert.pfx, passphrase: cert.passphrase });
}
for (const cert of matchingCerts) {
if (cert.cert)
tlsOptions.cert.push(cert.cert);
if (cert.key)
tlsOptions.key.push({ pem: cert.key, passphrase: cert.passphrase });
if (cert.pfx)
tlsOptions.pfx.push({ buf: cert.pfx, passphrase: cert.passphrase });
}
return tlsOptions;
}
Loading

0 comments on commit c4862c0

Please sign in to comment.