Q20 of 37 · API testing
Walk through testing an OAuth 2.0 flow from your API tests.
Short answer
Short answer: Use the client credentials grant for service-to-service tests (no UI). For authorization code flow, programmatically POST to /authorize → capture redirect → POST to /token. Cache tokens within a test (don't re-auth per call). Test the refresh flow, scope enforcement, and token expiry.
Detail
OAuth 2.0 has several flows; test design depends on which one your API uses.
Client Credentials grant (service-to-service):
The simplest. Your test exchanges a client id + secret for an access token in one call:
const res = await request.post('/oauth/token', {
form: {
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
scope: 'reports:read',
},
});
const { access_token } = await res.json();
Cache the token across the suite if it's long-lived; otherwise refresh per test class.
Authorization Code grant (interactive — needs user consent):
Conceptually:
GET /authorize?response_type=code&client_id=...&redirect_uri=...— user consent UI.- After consent, the auth server redirects to
redirect_uri?code=ABC123. POST /token { grant_type: authorization_code, code: ABC123 }returns the access token.
For tests, the consent UI is the obstacle. Options:
- Use a test user with pre-granted consent. Most providers let you skip the UI for known test clients.
- Drive the consent UI with Playwright in a single setup test, capture the code, exchange for a token.
- Mock the auth server entirely and have your API trust the mock during tests.
Token caching:
let cached: { token: string; expires: number } | null = null;
async function getToken() {
if (cached && cached.expires > Date.now() + 60_000) return cached.token;
// ... fetch token, set cached
}
Tests to write:
1. Token expiry & refresh. Issue a token, wait for expiry (or stub clock), retry — assert 401. Then exchange the refresh token for a new access token, retry — assert 200.
2. Scope enforcement. Issue tokens with different scopes; confirm endpoints enforce them:
const readToken = await getToken({ scope: 'reports:read' });
const res = await request.delete('/reports/42', headers(readToken));
expect(res.status()).toBe(403);
3. Tampering. Modify the token; expect 401.
4. Revocation. Revoke a token; subsequent calls return 401.
5. Multi-tenant tokens. A token for tenant A cannot access tenant B resources.
Pitfalls:
- Token leakage in logs. Tests should redact tokens from CI output. A leaked token is a real production secret depending on environment.
- Refresh token rotation. Some providers issue a new refresh token with each refresh; using the old one fails. Tests must update the cached refresh token.
- PKCE for public clients (mobile, SPA). If your API issues PKCE-only flows, generate a code verifier per test:
const verifier = randomString(64);
const challenge = base64url(sha256(verifier));
The interview signal: knowing client_credentials vs authorization_code, comfortable caching tokens for performance, and naming refresh + scope as critical to test alongside happy path.
// EXAMPLE
// Client credentials helper
async function getServiceToken(scope = 'reports:read'): Promise<string> {
const res = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID!,
client_secret: process.env.CLIENT_SECRET!,
scope,
}),
});
if (!res.ok) throw new Error(`Auth failed: ${res.status}`);
return (await res.json()).access_token;
}// WHAT INTERVIEWERS LOOK FOR
// COMMON PITFALL
// Related questions
What is the difference between authentication and authorisation?
API testing
Explain the structure of a JWT and how to test endpoints that use it.
API testing
How do you handle OAuth 2.0 client credentials flow in REST Assured?
REST Assured
How would you handle complex OAuth 2.0 / OIDC flows in Karate?
Karate