Testing tokens in Playwright

Published on February 28, 2025

👋 Hello technical folks ~

Previously we wrote an article about API Testing via Playwright (JS), which highlighted:

  • the setup for API Testing (request and APIRequestContext objects),
  • the steps to write and run the tests and generate a report

In this article, we will zoom in to look at how we can play around with request and the APIRequestContext objects in Playwright to perform some testing for session and CSRF tokens.

Recap: Session tokens are used in web applications to identify users after successful authentication, so that the application will know what features s/he is able to access.
CSRF (Cross-Site Request Forgery) token helps the application to verify if the requests come from legitimate users.

Before we can make any API call in Playwright (JS), we’ll need to create the context of the request object so that we can store cookie and/or header information there. We can do so with this line of code:

import { request } from '@playwright/test';

let apiContext = await request.newContext ({
// the application domain (e.g. www.google.com),
// any tokens,
// any HTTP headers,
// any HTTP credentials,
// ...
});

For more in-depth explanation, kindly refer to the Playwright (JS) documentation.

Since there’s a lot of information to be set and it will be used by every single API, we can simply put this chunk of code into a function:

// file: ApiFunction/APIContextCreation.ts

import { request} from '@playwright/test';

export async function setupApiContext(baseUrl: string, session="", csrf="") {
let apiContext = await request.newContext({
baseURL: baseUrl,
extraHTTPHeaders: {
'Content-Type': 'application/json', // common header for all requests
'csrf': csrf,
'Cookie': session
}
});

return apiContext;
}

The setupApiContext function takes in the necessary information and returns an APIRequestContext object. For our case, we will also be using this test framework for multiple applications. Thus, we take in the domain and tokens (the default is set to empty strings).

Now that we’ve created the function, we will now look at how we can perform token testing!

Testing for Unauthenticated Session

Since most application’s features are accessible only after user login and all authenticated features require session token and/or CSRF token, so the kind of tests we’ll have is:

We are only interested in the first 3 rows, since the last row should be covered by the actual (functional) API tests.

OH! We won’t cover all the combinations as it will make this article too long. Once you get the idea of how it works above, you can continue to write the other test. 😉

Scenario 1: No token provided

// file: security-token.test.ts

import { expect, test, APIRequestContext } from '@playwright/test';

import { setupApiContext } from '../ApiFunction/APIContextCreation.ts';

test.describe('API config. checks', () => {
let apiContext: APIRequestContext;

// Scenario 1
test(`Authenticated request should fail if no token is provided`, async () => {
// S1. set up the APIRequestContext object
apiContext = await setupApiContext("your-app-domain");

// S2: call an authenticated API
const unauthResp = await apiContext.post("your-api-endpoint");
const unauthRespBody = await unauthResp.json();

// verify the response object
expect(unauthResp.status()).toBe("401");
// add other verifications here, e.g. specific error message returned
});
});

Scenarios 2 & 3: Missing Session/CSRF token

// file: security-token.test.ts

import { expect, test, APIRequestContext } from '@playwright/test';

import { setupApiContext } from '../ApiFunction/APIContextCreation.ts';

test.describe('API config. checks', () => {
let apiContext: APIRequestContext;

// Scenario 2
test(`Authenticated request should fail if no SESSION token is provided`, async () => {
// S1: call the /login API and grab the tokens from its response

// S2: set up the APIRequestContext object
apiContext = await setupApiContext("your-app-domain", "", "your-csrf-token");

// S3: call an authenticated API
const unauthResp = await apiContext.post("your-api-endpoint");
const unauthRespBody = await unauthResp.json();

// verify the response object
expect(unauthResp.status()).toBe("401");
// add other verifications here, e.g. specific error message returned
});

// Scenario 3
test(`Authenticated request should fail if no CSRF token is provided`, async () => {
// ...
});
});

Testing for Wrong/Expired Tokens

As the session and CSRF tokens are used to identify a user, they must be unique and have a specific length. Once a token is generated, the older version of it must be voided/invalidated. Also, each user should always have his/her own set of tokens at any point of time. The application shouldn’t allow a mixture of tokens between userA and userB to be used together.

Thus, here are more tests we can add to our test suite:

Scenarios 4-6: Wrong token(s)

// file: security-token.test.ts

import { expect, test, APIRequestContext } from '@playwright/test';

import { setupApiContext } from '../ApiFunction/APIContextCreation.ts';

test.describe('API config. checks', () => {
let apiContext: APIRequestContext;

// Scenario 4
test(`Authenticated request should fail if wrong SESSION token provided`, async () => {
// S1: call the /login API and grab the session token from its response

// S2: set up the APIRequestContext object
apiContext = await setupApiContext("your-app-domain", "a", "your-csrf-token");

// S3: call an authenticated API
const unauthResp = await apiContext.post("your-api-endpoint");
const unauthRespBody = await unauthResp.json();

// verify the response object
expect(unauthResp.status()).toBe("401");
// add other verifications here, e.g. specific error message returned
});

// Scenario 5
test(`Authenticated request should fail if wrong CSRF token provided`, async () => {
// ...
});

// Scenario 6
test(`Authenticated request should fail if wrong tokens provided`, async () => {
// ...
});
});

Scenarios 7–9: Expired token(s)

// file: security-token.test.ts

import { expect, test, APIRequestContext } from '@playwright/test';

import { setupApiContext } from '../ApiFunction/APIContextCreation.ts';

test.describe('API config. checks', () => {
let apiContext: APIRequestContext;

// Scenario 7
test(`Authenticated request should fail if expired SESSION token provided`, async () => {
// S1: call the /login API and store session token into 'old_session' variable
// S2: call the /login API again and grab the session token from its response

// S3: set up the APIRequestContext object
apiContext = await setupApiContext("your-app-domain", old_session, "your-latest-csrf-token");

// S4: call an authenticated API
const unauthResp = await apiContext.post("your-api-endpoint");
const unauthRespBody = await unauthResp.json();

// verify the response object
expect(unauthResp.status()).toBe("401");
// add other verifications here, e.g. specific error message returned
});

// Scenario 8
test(`Authenticated request should fail if expired CSRF token provided`, async () => {
// ...
});

// Scenario 9
test(`Authenticated request should fail if expired tokens provided`, async () => {
// ...
});
});

Scenarios 10 & 11: Incorrect combination of Session/CSRF token

// file: security-token.test.ts

import { expect, test, APIRequestContext } from '@playwright/test';

import { setupApiContext } from '../ApiFunction/APIContextCreation.ts';

test.describe('API config. checks', () => {
let apiContext: APIRequestContext;

// Scenario 10
test(`Authenticated request should fail if userA uses userB's SESSION token`, async () => {
// S1: call the /login API as userA and grab the session token from its response
// S2: call the /login API as userB and grab the session token from its response

// S3: set up the APIRequestContext object
apiContext = await setupApiContext("your-app-domain", "userB-session-token", "userA-csrf-token");

// S4: call an authenticated API
const unauthResp = await apiContext.post("your-api-endpoint");
const unauthRespBody = await unauthResp.json();

// verify the response object
expect(unauthResp.status()).toBe("401");
// add other verifications here, e.g. specific error message returned
});

// Scenario 11
test(`Authenticated request should fail if userA uses userB's CSRF token`, async () => {
// ...
});
});

Taa-daa! Now you have a set of tests to check for tokens! Not that difficult, right? 😄

Tokens are very important to web applications, hence it is important that we include tests for it. Of course, whatever we have mentioned above aren’t the exhaustive list of test scenarios. Please add on more token-related tests on your own and share it with us!

We hope this article will further enhance on your current test suite after you’ve kickstarted the Playwright journey since our last article!

🧙🏼‍♀Team Merlin 💛
Application security is not any individual’s problem but a shared responsibility.


Testing tokens in Playwright was originally published in Government Digital Products, Singapore on Medium, where people are continuing the conversation by highlighting and responding to this story.