level 05 / api-authentication
API Auth: Headers, JWT, OAuth2
Authenticate API test requests using headers, Bearer tokens, JWT, and OAuth2 flows.
Authentication Strategies
| Strategy | How passed | Common use |
|---|---|---|
| API Key | Header or query param | Simple public APIs |
| Bearer token | Authorization: Bearer <token> | REST APIs post-login |
| JWT | Same as Bearer, token is self-contained | Stateless auth |
| OAuth2 | Token endpoint flow | Third-party integrations |
| Basic Auth | Authorization: Basic base64(user:pass) | Legacy or internal |
| Cookie / Session | Cookie jar | Web 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' } }]