Implementing Microsoft Entra Certificate-Based Authentication with Playwright

Max Schmitt - Sep 18 - - Dev Community

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:

Image description

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'


Enter fullscreen mode Exit fullscreen mode

Step 2: Update your test @playwright/test imports



-import { test, expect } from '@playwright/test';
+import { test, expect } from './baseTest';


Enter fullscreen mode Exit fullscreen mode

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();
});


Enter fullscreen mode Exit fullscreen mode

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.

Useful links

. . . . . . . . . . . . . . . . . . . .