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:
- 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. - Sample lightweight heartbeat monitors that can be used to ping service endpoints, under the
lightweight
folder. - 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!