Authentication Testing
API V2 uses JWT Bearer tokens. This guide walks through testing the complete authentication lifecycle β from login to token refresh and tamper detection β and shows how to reuse an API-obtained token in a UI test.
Authentication Flow
- Login: POST credentials β receive JWT token
- Attach token: Include
Authorization: Bearer <token>on every request - Refresh: POST with current token before expiry to extend the session
- Expiry: After 3600 s the token is rejected with 401
Test Scenarios
1. Successful Login
javascript
test('valid credentials return JWT token', async () => {
const response = await fetch('https://api.testauto.app/api/v2/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'user', password: 'user123' }),
});
expect(response.status).toBe(200);
const data = await response.json();
expect(data.token).toBeDefined();
expect(data.username).toBe('user');
expect(data.expiresIn).toBe(3600);
});2. Invalid Credentials
javascript
test('wrong password returns 401', async () => {
const response = await fetch('https://api.testauto.app/api/v2/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'user', password: 'wrongpassword' }),
});
expect(response.status).toBe(401);
const data = await response.json();
expect(data.message).toMatch(/invalid/i);
});3. Authenticated API Request
javascript
test('token grants access to protected endpoint', async () => {
const loginResp = await fetch('https://api.testauto.app/api/v2/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'user', password: 'user123' }),
});
const { token } = await loginResp.json();
const tasksResp = await fetch('https://api.testauto.app/api/v2/tasks', {
headers: { Authorization: `Bearer ${token}` },
});
expect(tasksResp.status).toBe(200);
const data = await tasksResp.json();
expect(Array.isArray(data.content)).toBe(true);
});4. Missing Token
javascript
test('request without token returns 401', async () => {
const response = await fetch('https://api.testauto.app/api/v2/tasks');
expect(response.status).toBe(401);
});5. Tampered Token
javascript
test('tampered token is rejected', async () => {
const loginResp = await fetch('https://api.testauto.app/api/v2/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'user', password: 'user123' }),
});
const { token } = await loginResp.json();
// Flip the last character of the signature
const tampered = token.slice(0, -1) + (token.endsWith('A') ? 'B' : 'A');
const response = await fetch('https://api.testauto.app/api/v2/tasks', {
headers: { Authorization: `Bearer ${tampered}` },
});
expect(response.status).toBe(401);
});6. Token Refresh
javascript
test('can refresh token before expiry', async () => {
const loginResp = await fetch('https://api.testauto.app/api/v2/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'user', password: 'user123' }),
});
const { token: originalToken } = await loginResp.json();
const refreshResp = await fetch('https://api.testauto.app/api/v2/auth/refresh', {
method: 'POST',
headers: { Authorization: `Bearer ${originalToken}` },
});
expect(refreshResp.status).toBe(200);
const { token: newToken } = await refreshResp.json();
expect(newToken).toBeDefined();
expect(newToken).not.toBe(originalToken);
});Using an API Token in a UI Test
The recommended pattern for testing authenticated UIs is to obtain the token via API (fast), then inject it into the browser session before navigating β avoiding the need to drive the login form in every test.
typescript
import { test, expect, request as apiRequest } from '@playwright/test';
test('logged-in user can create a task via UI', async ({ page, request }) => {
// Step 1: Get token via API (fast, no UI interaction)
const loginResp = await request.post('https://api.testauto.app/api/v2/auth/login', {
data: { username: 'user', password: 'user123' },
});
const { token } = await loginResp.json();
// Step 2: Inject the token into browser localStorage before navigation
await page.goto('/task-manager');
await page.evaluate((t) => {
localStorage.setItem('auth_token', t);
}, token);
// Step 3: Reload so the app picks up the stored token
await page.reload();
// Step 4: Verify authenticated state and perform UI actions
await expect(page.getByText('Welcome, user')).toBeVisible();
await page.getByRole('link', { name: 'Add Task' }).click();
await page.getByLabel('Title').fill('Token-authenticated task');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Token-authenticated task')).toBeVisible();
});π‘ Why this pattern? Filling login forms in every test is slow and couples your UI tests to the login page. Getting the token via API and injecting it directly is faster, more focused, and less fragile.
Best Practices
- β Never log tokens β they are credentials
- β Test expiry behaviour β verify the API rejects expired tokens
- β Use API setup for UI auth β inject token instead of driving the login form
- β Use test users only β never test with real or production credentials
- β Test error messages carefully β they should not leak system details
- β Verify role-based access β test with both admin and regular user tokens