playwright/tests/library/client-certificates.spec.ts

253 lines
10 KiB
TypeScript
Raw Normal View History

2024-07-04 11:05:47 +02:00
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import path from 'path';
import { expect, playwrightTest as base } from '../config/browserTest';
import https from 'https';
import type net from 'net';
const test = base.extend<{ serverURL: string, serverURLRewrittenToLocalhost: string }>({
serverURL: async ({ }, use) => {
const server = https.createServer({
key: fs.readFileSync(path.join(kClientCertificatesDir, 'server/server_key.pem')),
cert: fs.readFileSync(path.join(kClientCertificatesDir, 'server/server_cert.pem')),
ca: [
fs.readFileSync(path.join(kClientCertificatesDir, 'server/server_cert.pem')),
],
requestCert: true,
rejectUnauthorized: false,
}, (req, res) => {
const cert = (req.socket as import('tls').TLSSocket).getPeerCertificate();
if ((req as any).client.authorized) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`Hello ${cert.subject.CN}, your certificate was issued by ${cert.issuer.CN}!`);
} else if (cert.subject) {
res.writeHead(403, { 'Content-Type': 'text/html' });
res.end(`Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`);
} else {
res.writeHead(401, { 'Content-Type': 'text/html' });
res.end(`Sorry, but you need to provide a client certificate to continue.`);
}
});
await new Promise<void>(f => server.listen(0, 'localhost', () => f()));
await use(`https://localhost:${(server.address() as net.AddressInfo).port}/`);
await new Promise<void>(resolve => server.close(() => resolve()));
},
serverURLRewrittenToLocalhost: async ({ serverURL, browserName }, use) => {
const parsed = new URL(serverURL);
parsed.hostname = 'i-get-rewritten-to-localhost-on-the-server-side';
const shouldRewriteToLocalhost = browserName === 'webkit' && process.platform === 'darwin';
await use(shouldRewriteToLocalhost ? parsed.toString() : serverURL);
}
});
test.skip(({ mode }) => mode !== 'default');
const kClientCertificatesDir = path.join(__dirname, '../config/testserver/client-certificates');
const kValidationSubTests: any[] = [
[[{ url: 'test', certs: [] }], 'No certs specified for url: test'],
[[{ url: 'test', certs: [{}] }]], 'None of cert, key, passphrase or pfx is specified',
[[{
url: 'test', certs: [{
cert: 'foo',
key: 'foo',
passphrase: 'foo',
pfx: 'foo',
}]
}], 'pfx is specified together with cert, key or passphrase']
];
test.describe('fetch', () => {
test('validate input', async ({ playwright }) => {
for (const [clientCertificates, expected] of kValidationSubTests)
await expect(playwright.request.newContext({ clientCertificates })).rejects.toThrow(expected);
});
test('should fail with no client certificates provided', async ({ playwright, serverURL }) => {
const request = await playwright.request.newContext({ ignoreHTTPSErrors: true });
const response = await request.get(serverURL);
expect(response.status()).toBe(401);
expect(await response.text()).toBe('Sorry, but you need to provide a client certificate to continue.');
await request.dispose();
});
test('should keep supporting http', async ({ playwright, server }) => {
const request = await playwright.request.newContext({
clientCertificates: [{
url: server.PREFIX,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
const response = await request.get(server.PREFIX + '/one-style.html');
expect(response.url()).toBe(server.PREFIX + '/one-style.html');
expect(response.status()).toBe(200);
expect(await response.text()).toContain('<div>hello, world!</div>');
await request.dispose();
});
test('should throw with a untrusted CA', async ({ playwright, serverURL }) => {
const request = await playwright.request.newContext({
clientCertificates: [{
url: serverURL,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
try {
const response = await request.get(serverURL);
expect(response.status()).toBe(503);
expect(await response.text()).toBe('Self-signed certificate error: self-signed certificate');
} catch (error) {
expect(error.message).toContain('self-signed certificate');
}
});
test('should throw with matching CA but untrusted client certs', async ({ playwright, serverURL }) => {
const request = await playwright.request.newContext({
ignoreHTTPSErrors: true,
clientCertificates: [{
url: serverURL,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/bob_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/bob_key.pem'),
}],
}],
});
const response = await request.get(serverURL);
expect(response.url()).toBe(serverURL);
expect(response.status()).toBe(403);
expect(await response.text()).toBe('Sorry Bob, certificates from Bob are not welcome here.');
await request.dispose();
});
test('should work without CA', async ({ playwright, serverURL }) => {
const request = await playwright.request.newContext({
clientCertificates: [{
url: serverURL,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
ignoreHTTPSErrors: true,
});
const response = await request.get(serverURL);
expect(response.url()).toBe(serverURL);
expect(response.status()).toBe(200);
expect(await response.text()).toBe('Hello Alice, your certificate was issued by localhost!');
await request.dispose();
});
test('should work in the browser with request interception', async ({ browser, playwright, serverURL }) => {
const request = await playwright.request.newContext({
ignoreHTTPSErrors: true,
clientCertificates: [{
url: serverURL,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
const page = await browser.newPage({ ignoreHTTPSErrors: true });
await page.route('**/*', async route => {
const response = await request.fetch(route.request());
await route.fulfill({ response });
});
await page.goto(serverURL);
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
await page.close();
await request.dispose();
});
});
test.describe('browser', () => {
test('validate input', async ({ browser }) => {
for (const [clientCertificates, expected] of kValidationSubTests)
await expect(browser.newContext({ clientCertificates })).rejects.toThrow(expected);
});
test('should keep supporting http', async ({ browser, server }) => {
const page = await browser.newPage({
clientCertificates: [{
url: server.PREFIX,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
await page.goto(server.PREFIX + '/one-style.html');
await expect(page.getByText('hello, world!')).toBeVisible();
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
await page.close();
});
test('should fail with no client certificates', async ({ browser, serverURLRewrittenToLocalhost }) => {
const page = await browser.newPage({
ignoreHTTPSErrors: true,
clientCertificates: [{
url: 'https://not-matching.com',
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
await page.goto(serverURLRewrittenToLocalhost);
await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible();
await page.close();
});
test('should fail with untrusted client certs', async ({ browser, serverURLRewrittenToLocalhost }) => {
const page = await browser.newPage({
clientCertificates: [{
url: serverURLRewrittenToLocalhost,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/bob_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/bob_key.pem'),
}],
}],
});
await page.goto(serverURLRewrittenToLocalhost);
await expect(page.getByText('Sorry Bob, certificates from Bob are not welcome here')).toBeVisible();
await page.close();
});
test('should pass with matching certificates', async ({ browser, serverURLRewrittenToLocalhost }) => {
const page = await browser.newPage({
ignoreHTTPSErrors: true,
clientCertificates: [{
url: serverURLRewrittenToLocalhost,
certs: [{
cert: path.join(kClientCertificatesDir, 'client/alice_cert.pem'),
key: path.join(kClientCertificatesDir, 'client/alice_key.pem'),
}],
}],
});
await page.goto(serverURLRewrittenToLocalhost);
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
await page.close();
});
});