level 05 / api-authentication

API Auth: Headers, JWT, OAuth2

Authenticate API test requests using headers, Bearer tokens, JWT, and OAuth2 flows.

Authentication Strategies

StrategyHow passedCommon use
API KeyHeader or query paramSimple public APIs
Bearer tokenAuthorization: Bearer <token>REST APIs post-login
JWTSame as Bearer, token is self-containedStateless auth
OAuth2Token endpoint flowThird-party integrations
Basic AuthAuthorization: Basic base64(user:pass)Legacy or internal
Cookie / SessionCookie jarWeb session auth

API Key in Headers

test('api key in header', async ({ request }) => {
  const res = await request.get('https://api.example.com/data', {
    headers: { 'X-API-Key': process.env.API_KEY! },
  });
  expect(res.ok()).toBe(true);
});

Store keys in .env (gitignored), read via process.env. Never hardcode.

Bearer Token Flow

// fixtures/auth.ts
import { test as base, APIRequestContext } from '@playwright/test';

export const test = base.extend<{ authedRequest: APIRequestContext }>({
  authedRequest: async ({ playwright }, use) => {
    const ctx = await playwright.request.newContext({
      baseURL: process.env.API_BASE_URL,
    });
    const res = await ctx.post('/auth/login', {
      data: { username: 'admin', password: process.env.ADMIN_PASS },
    });
    const { token } = await res.json();
    await ctx.dispose();

    const authed = await playwright.request.newContext({
      baseURL: process.env.API_BASE_URL,
      extraHTTPHeaders: { Authorization: `Bearer ${token}` },
    });
    await use(authed);
    await authed.dispose();
  },
});
// tests/users.spec.ts
import { test } from '../fixtures/auth';
import { expect } from '@playwright/test';

test('admin can list users', async ({ authedRequest }) => {
  const res = await authedRequest.get('/users');
  expect(res.status()).toBe(200);
});

JWT Validation

JWTs are base64-encoded. Decode the payload to assert claims without a crypto library:

function decodeJwtPayload(token: string) {
  const [, payload] = token.split('.');
  return JSON.parse(Buffer.from(payload, 'base64url').toString());
}

test('jwt contains expected claims', async ({ request }) => {
  const res = await request.post('/auth/login', {
    data: { username: 'admin', password: 'secret' },
  });
  const { token } = await res.json();
  const payload = decodeJwtPayload(token);

  expect(payload.sub).toBe('admin');
  expect(payload.role).toBe('admin');
  expect(payload.exp).toBeGreaterThan(Date.now() / 1000);  // not expired
});

OAuth2 Client Credentials

Machine-to-machine OAuth2 — get a token from the token endpoint, attach to requests:

async function getOAuthToken(request: APIRequestContext): Promise<string> {
  const res = await request.post(process.env.OAUTH_TOKEN_URL!, {
    form: {
      grant_type: 'client_credentials',
      client_id: process.env.OAUTH_CLIENT_ID!,
      client_secret: process.env.OAUTH_CLIENT_SECRET!,
      scope: 'read:data',
    },
  });
  const { access_token } = await res.json();
  return access_token;
}

test('oauth2 client credentials flow', async ({ request }) => {
  const token = await getOAuthToken(request);
  const res = await request.get('/protected-resource', {
    headers: { Authorization: `Bearer ${token}` },
  });
  expect(res.ok()).toBe(true);
});

Playwright storageState for Session Auth

Reuse authenticated browser sessions across tests:

// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(config.projects[0].use.baseURL + '/login');
  await page.fill('[name=username]', 'admin');
  await page.fill('[name=password]', process.env.ADMIN_PASS!);
  await page.click('[type=submit]');
  await page.context().storageState({ path: 'playwright/.auth/admin.json' });
  await browser.close();
}
export default globalSetup;
// playwright.config.ts — use saved state
projects: [{ use: { storageState: 'playwright/.auth/admin.json' } }]