Automating OAuth 2.0 Token Refresh in Postman with Pre-Request Scripts
Stop manually copying OAuth tokens in Postman. A collection‑level pre‑request script can check expiry, call your custom refresh endpoint, and inject fresh tokens automatically—works in CI/CD too.
14 Sept 2025, 03:59 UTC

The Problem: Manual Token Management Breaks Test Flow
Running a collection of dozens of API tests against a protected service can be frustrating when the access token expires every 15 minutes. The typical workaround—copying a new token from the auth server, pasting it into an environment variable, and restarting the run—breaks continuous integration and slows down development. Postman’s built‑in OAuth 2.0 helper handles standard flows, but many providers use custom token endpoints, non‑standard claims, or require client assertions that the helper doesn’t support.
Takeaway: A collection‑level pre‑request script can check the token’s expiry, call the refresh endpoint automatically, and update environment variables before each request—no manual intervention, no external tooling.
How Pre‑Request Scripts Handle Token Refresh
Pre‑request scripts run JavaScript before every request in a collection. They can read and write pm.environment (persisted across runs) and pm.variables (in‑memory), and they can make HTTP calls with pm.sendRequest(). The typical pattern is:
- Store
access_token,refresh_token, andexpires_at(epoch milliseconds) in environment variables. - Before each request, compare
Date.now()withexpires_atminus a safety buffer (e.g., 60 s). - If the token is missing or about to expire,
POSTto the token endpoint with the appropriate payload. - On success, parse the response, calculate a new
expires_at, and write all three values back withpm.environment.set(). - Inject the fresh
access_tokeninto the request header viapm.request.headers.add().
Because the logic lives at the collection level, every request inherits the same refresh logic without duplication.
Worked Example: Custom Token Endpoint with Client Assertion
The following script assumes a provider that requires a signed JWT client assertion instead of a plain client_secret. Replace buildClientAssertion() with your signing logic (HS256, RS256, etc.).
// Collection-level pre‑request script
const TOKEN_URL = 'https://auth.example.com/oauth2/token';
const BUFFER_MS = 60_000; // refresh 60s before expiry
const accessToken = pm.environment.get('access_token');
const refreshToken = pm.environment.get('refresh_token');
const expiresAt = parseInt(pm.environment.get('expires_at'), 10);
// If no token yet or expired (with buffer), refresh
if (!accessToken || !refreshToken || !expiresAt || Date.now() >= expiresAt - BUFFER_MS) {
console.log('Token missing or near expiry, refreshing...');
const clientAssertion = buildClientAssertion(); // implement per provider spec
const payload = {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
client_assertion: clientAssertion
};
pm.sendRequest({
url: TOKEN_URL,
method: 'POST',
header: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: { mode: 'urlencoded', urlencoded: Object.entries(payload) }
}, (err, res) => {
if (err) {
console.error('Token refresh failed:', err);
return;
}
const data = res.json();
if (!data.access_token) {
console.error('Unexpected token response:', data);
return;
}
const newExpiresAt = Date.now() + (data.expires_in * 1000);
pm.environment.set('access_token', data.access_token);
pm.environment.set('refresh_token', data.refresh_token ?? refreshToken);
pm.environment.set('expires_at', newExpiresAt.toString());
console.log('Token refreshed, expires at', new Date(newExpiresAt).toISOString());
});
}
// Attach current token to outgoing request
pm.request.headers.add({ key: 'Authorization', value: `Bearer ${pm.environment.get('access_token')}` });
// Placeholder: replace with your signing logic
function buildClientAssertion() {
// Example: return jwt.sign({...}, privateKey, { algorithm: 'RS256' });
return 'YOUR_SIGNED_JWT_HERE';
}
Where to run: In Postman desktop or web, open the collection → Pre‑request Scripts tab. Permissions: Editor access on the collection. Expected check: Open Postman Console (View → Show Postman Console) and run a single request; you should see a “Token refreshed” log and the Authorization header on the outgoing request. Risk: If buildClientAssertion() returns a malformed JWT, the auth server returns 400 and the script logs the error but continues with a stale token—subsequent requests will 401.
Trade‑offs and Limitations
- Latency per request: The script runs before every request. A 300 ms token endpoint adds 300 ms to each call. Mitigate by caching the token in
pm.variablesfor fast‑path checks, only hittingpm.environmentwhen the buffer threshold is crossed. - Race conditions in parallel runs: Collection Runner can execute requests concurrently. Two threads may both see an expired token and trigger simultaneous refreshes, wasting quota or causing one to fail. Use
pm.variables.set('refresh_lock', true)as a lightweight mutex, or delegate refresh to a dedicated external service. - Secret exposure:
client_secret,refresh_token, and signed assertions stored in environment variables are visible in the Postman UI and exported JSON files. For production pipelines, inject secrets at runtime via Postman Vault (paid) or CI secret injection (postman collection run -e env.json --env-var client_secret=$SECRET). - Version compatibility:
pm.sendRequest()callback style works across versions, butasync/awaitsupport arrived in Postman v9. If your CI uses an older CLI, stick to callbacks. - Clock skew: The 60‑second buffer helps, but if the auth server’s clock runs ahead, you may still hit 401. Consider fetching the server time from a lightweight endpoint or using the
Dateheader from the token response to calibrate.
Verify It Works in Your Pipeline
- Create a test collection with the script above pointed at a sandbox auth server (Keycloak, Auth0 trial, or a mock server).
- Run locally: open Postman Console, execute a few requests spaced over the token lifetime, confirm refresh logs appear exactly once per expiry window.
- Export collection and environment:
postman collection export <collection_uid> -o coll.jsonandpostman environment export <env_uid> -o env.json. - Run headless:
postman collection run coll.json -e env.json --iteration-count 10. Watch for token persistence across iterations (environment variables survive between runs in the same CLI invocation). - Add a CI step that injects secrets at runtime rather than committing them.
If the console shows duplicate refresh attempts or 401s after refresh, revisit the locking strategy and buffer size. The script is a starting point—adapt the token endpoint payload, error handling, and storage to your provider’s contract.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.