Dec 8th, 2023: [EN] Authentication in Synthetic Monitoring with Playwright and Elastic Synthetics

Introduction

Christmas is a time of coming together. Last Christmas we could add RUM and APM to keep an eye on things. But it will not necessarily notify you if a particular user feature, such as the 'Buy Now!' button is failing, or what exactly they see. It also doesn't allow SREs to work with developers later to fix the problem..

Synthetic Monitoring has proven to be a great way for developers and SREs to embody the Christmas spirit and come together to perform pre-production E2E tests as well as production monitoring. But one of the most common questions we get is how to handle authentication in user journeys or leverage test services such test cards for payment services.

For today's exciting entry we'll show you via a set of examples how to perform manual and token-based authentication using Playwright and Elastic Synthetics, leveraging environment and global variables.

Prerequisites

To embrace the Christmas spirit and work together, we need a code project to house our monitors. This can be done by installing the @elastic/synthetics library and creating a new project with the init command:

npm install -g @elastic/synthetics
npx @elastic/synthetics init synthetics-replicator-tests

Running the above command and adding your Elastic cluster settings and API key via the generated command line wizard will give you a sample project containing:

  1. Basic configuration, such as the URL of the site we need to monitor, within the synthetics.config.ts configuration file. Check out a basic example, including use of local and production application URLS, here.
  2. Sample lightweight heartbeat monitors that can be used to ping service endpoints, under the lightweight folder.
  3. Example journeys, which we shall focus on today, that perform user actions using Playwright JS which are generated in the journeys folder.

Manual Authentication Journey

Using Playwright in conjunction with the @elastic/synthetics wrapper allows us to interact with our webpage and perform user actions such as button clicks and text entry. The below example shows how we can enter the username and password using the Playwright locator fill method and submit the entries using the click method on the submit button perform a human click:

import { journey, step, monitor, expect } from '@elastic/synthetics';

journey('My Manual Example Authentication Journey', ({ page, params }) => {

  // Only relevant for the push command to create
  // monitors in Kibana
  monitor.use({
    id: 'example-manual-monitor',
    schedule: 10,
  });

  step('Go to cloud page', async () => {
    await page.goto(params.url);

    const header = await page.locator('h1.euiTitle');
    expect(await header.textContent()).toContain('Log in');
  });

  step('Manual login', async () => {
    // Check submit button is disabled before entry
    const submitButton = await page.getByTestId('login-button');
    expect(submitButton).toBeDisabled();

    // Add credentials
    const username = params.username;
    expect(username).toBeDefined();
    await page.getByTestId('login-username').fill(username);

    const password = params.password;
    expect(password).toBeDefined();
    await page.getByTestId('login-password').fill(password);

    // Check login button is now enabled
    expect(submitButton).toBeEnabled();
    await submitButton.click();

    // Wait for login
    await page.waitForURL('**/home');

    // Validate we have logged in successfully
    const header = await page.locator('h1.euiTitle');
    expect(await header.textContent()).toContain('Welcome to Elastic');
  });

});

The above code emulates the workflow of logging into the site specified in params.url, pulling the username and password from the params variable injected into the journey from our project configuration located in synthetics.config.ts:

import type { SyntheticsConfig } from '@elastic/synthetics';

export default env => {
  const config: SyntheticsConfig = {
    params: {
      url: 'https://cloud.elastic.co/',
      username: process.env.ELASTIC_USERNAME,
      password: process.env.ELASTIC_PASSWORD
      /// Other variables omitted
    },
    playwrightOptions: {
      ignoreHTTPSErrors: false,
      testIdAttribute: 'data-test-id'
    },
    /**
     * Configure global monitor settings
     */
    monitor: {
      schedule: 5,
      locations: ['united_kingdom'],
      privateLocations: [],
    },
    /**
     * Project monitors settings
     */
    project: {
      id: 'synthetic-monitoring-authentication',
      url: 'https://my-elastic-deployment.com:443',
      space: 'default',
    },
  };
  }
  return config;
};

Notice that we are pulling attributes from environment variables. It's important to obfuscate the password information in particular to prevent leaking of the password. These variables can be configured for locally and stored as secrets for CI-based execution. Nevertheless, it's important to limit the roles of any test accounts to the minimum required operations for synthetic monitoring purposes to prevent malicious activity.

Token-based Authentication Journey

While the above is a simple login case, there are more complex scenarios out there where we may wish to leverage token-based authentication from SSO tools, via protocols such as OAuth2.

The request parameter is perfect for pulling tokens or required credentials independently of the browser interactions for the page you are testing. The below example shows a precursor step to obtain a token that is included in the webpage request in a subsequent step:

import { journey, step, monitor, expect } from '@elastic/synthetics';

journey('My Example Auto Authentication Journey', ({ page, params, request }) => {
  
  // Only relevant for the push command to create
  // monitors in Kibana
  monitor.use({
    id: 'example-token-based-monitor',
    schedule: 10,
  });
  
  // We will populate these variable from example HTTP API responses
  let apiKey;

  // Lets simulate getting an authentication token back from an auth service
  const apiBaseUrl = params.example_oauth2_endpoint;
  const clientId = params.example_client_id;

  // Let's say we expect the auth API to return a token
  const exampleAccessTokenKey = 'access_token';
  const exampleAccessToken = params.example_access_token;
  
  // Get headers for use by the page
  step('Get token from auth service', async() => {
    const resp = await request.get(`${apiBaseUrl}?client_id=${clientId}&${exampleAccessTokenKey}=${exampleAccessToken}`);
    apiKey = resp.headers()[exampleAccessTokenKey];
    expect(apiKey).toEqual(exampleAccessToken);
  });

  // Use headers in other page access
  step('Use API Key in page request', async() => {
    await page.setExtraHTTPHeaders({ [exampleAccessTokenKey]: apiKey });

    await page.goto(`${params.redirectUrl}?${exampleAccessTokenKey}=${apiKey}`);
    await page.waitForSelector(`text=${apiKey}`);
  });
});

In a real-word example, we obviously wouldn't specify the generated token as a params. But it is possible we would want to access a dedicated endpoint to obtain our token, which again would need to be stored as a secret and accessed via the config params:

import type { SyntheticsConfig } from '@elastic/synthetics';

export default env => {
  const config: SyntheticsConfig = {
    params: {
      redirectUrl: 'https://httpbin.org/response-headers',
      example_oauth2_endpoint: process.env.EXAMPLE_OAUTH2_ENDPOINT
      // Other params omitted
    },
    /**
     * Playwright, monitor and project options omitted
     * See manual example above
     */
  }
  return config;
};

One further case we may encounter is the need to send the token as headers to another API for data. If you're interested in this case I recommend taking a look at this example in the synthetics-demo GitHub repo.

Monitor Execution

There are two key points where you want to execute these monitors before they hit production.

Local Development

Developers, this is your moment to do a happy "It works!" dance as you see the journey test turn green. The wizard-generated npm run test command runs the test suite for you:

CI Pipeline

Next, both developers and SREs may sit crossing their fingers and toes as we check to make sure the monitors pass as they are run as E2E tests within our GitHub ActionsCI pipeline:

For ease, I created an additional npm script ci-test in our package.json specifically for use in our pipeline:

"ci-test": "SYNTHETICS_JUNIT_FILE='junit-synthetics.xml' npx @elastic/synthetics journeys --reporter=junit"

The above command specifies the test result file path, generated in JUnit format using the --reporter=junit option, to allow for the results to published in a friendly format.

Push Monitors

SREs, now is your time to shine as on a passing suite of tests, these monitors can be pushed alongside an application deployment to Elastic for use against the production website:

npm run push

The push command will add the new monitor to your cluster, and allow you to see whether the given user journey is working as expected based on the most recent scheduled execution. However, remember those environment variables we used previously? Elastic has no knowledge of these, so we need to configure them using the Global parameters tab under Settings at the top right of the Synthetics application in Kibana:

You'll then find all monitors will pass with the next execution:

Add Alerting

But of course, you'll be enjoying the festivities and time with family, rather than keeping your eyes permanently glued to the dashboard. So lets add a alert to notify us after the monitor status changes:

With the above alert configuration, an email alert will be triggered to notify you that it's time to log on and investigate a potential issue.

Happy Holidays!

In this post we've covered how to test and monitor authentication workflows in your application using Playwright and Elastic Synthetic Monitoring in both user-entry and token-based authentication workflows. Combining these with a dash of RUM in your application will give you confidence that all is well as you enjoy festivities with your family.

Image source: Unsplash

To setup your own monitors, check out the GitHub repository with the code and configuration and the Elastic Synthetics documentation.

Seasons Greetings!

4 Likes

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.