Introduction to Certificate-Based Authentication (CBA)
Microsoft recently announced (July 2024) support for certificate-based authentication (CBA) for Microsoft Entra. CBA is a phishing-resistant, passwordless, and convenient way to authenticate users with X.509 certificates without relying on passwords. With the recent v1.46 release, Playwright now supports the CBA method for authenticating with Entra.
TLS Client Certificates and Their Role in CBA
TLS client certificates are digital credentials that authenticate clients to servers in secure connections. They contain the client's public key, are signed by trusted authorities, and enable mutual authentication. These certificates enhance security by verifying client identities, preventing unauthorized access to sensitive resources in various IT environments.
If no client certificate is available, your browser will display a prompt like this:
Implementing CBA with Playwright
Step 1: Obtain the certificates
If the certificate files are not yet available on the client, we recommend defining base fixtures that will fetch the certificates in every worker process. The certificates will then remain in memory. See here for all available options for clientCertificates
(e.g., if you are using PEM instead of PKCS#12).
import { DefaultAzureCredential } from '@azure/identity';
import { SecretClient } from '@azure/keyvault-secrets';
import { test as base } from '@playwright/test'
// DefaultAzureCredential automatically detects and uses the most appropriate authentication method,
// including environment variables, managed identities, and OIDC tokens from GitHub Actions.
// We recommend OIDC logins via e.g. azure/login:
// https://github.com/Azure/login/?tab=readme-ov-file#login-with-openid-connect-oidc-recommended
const credential = new DefaultAzureCredential();
const vaultName = '<YOUR KEYVAULT NAME>';
const KEYVAULT_URI = `https://${vaultName}.vault.azure.net`;
const secretName = '<YOUR SECRET>';
const secretClient = new SecretClient(KEYVAULT_URI, credential);
export const test = base.extend({
clientCertificates: async ({ }, use) => {
const certificateSecret = await secretClient.getSecret(secretName);
await use([
{
origin: 'https://certauth.login.microsoftonline.com',
// Alternatively, if you use e.g. PPE, it's: https://certauth.login.windows-ppe.net
pfx: Buffer.from(certificateSecret.value!, 'base64'),
// You might need to provide your passphrase here:
// passphrase: process.env.SECRET_PASSPHRASE
}
]);
}
});
export { expect } from '@playwright/test'
Step 2: Update your test @playwright/test
imports
-import { test, expect } from '@playwright/test';
+import { test, expect } from './baseTest';
Step 3: Log in inside a test
import { test, expect } from './baseTest';
test('is able to login', async ({ page }) => {
await page.goto('https://your-application.com/login');
// Your login logic here
await page.getByRole('textbox', { name: 'Email' }).fill('example@foo.com');
await page.getByRole('button', { name: 'Sign in' }).click();
});
You can also provide client certificates as a parameter of browser.newContext() and apiRequest.newContext().
Troubleshooting
To debug which network requests are being made, you can set the DEBUG=pw:client-certificates
environment variable. This will print all the connections that are being established.
It's a known issue that the authentication does not work if the --disable-web-security
argument is passed to Chromium.