Race Condition with Bearer Tokens
How Race Condition Manifests in Bearer Tokens
Race conditions in Bearer Token authentication occur when multiple concurrent requests manipulate token state in ways that create security vulnerabilities. The most common manifestation involves token revocation timing where a user logs out or their session is terminated, but active requests continue to use the now-invalid token before the system fully processes the revocation.
Consider this scenario: A user makes an API request that triggers a long-running operation (like file processing or batch data export). During this operation, the user logs out or their session expires. The server initiates token revocation, but the original request completes using the cached token before revocation propagates through the system. This creates a window where an invalidated token still grants access.
async function processDocument(req, res) {
const token = req.headers.authorization.split(' ')[1];
const user = await validateToken(token); // Initial validation
// Long-running operation starts
const result = await heavyProcessing(user.id);
// Token could be revoked during processing
// But we already validated it at the start
return res.json(result);
}The critical flaw here is single-point validation. The token is checked once at the beginning, but never re-validated during the operation. If revocation occurs mid-process, the system still completes the operation using what should be an invalid token.
Another Bearer Token race condition pattern involves concurrent token refresh operations. When a token is about to expire, multiple requests might simultaneously trigger refresh logic:
let cachedToken = null;
let refreshPromise = null;
async function getFreshToken() {
if (cachedToken) return cachedToken;
// Race condition: multiple requests hit this simultaneously
if (!refreshPromise) {
refreshPromise = refreshToken()
.then(token => {
cachedToken = token;
refreshPromise = null;
return token;
});
}
return refreshPromise;
}
async function apiCall() {
const token = await getFreshToken();
return fetch('/api/endpoint', {
headers: { Authorization: `Bearer ${token}` }
});
}Without proper locking or promise deduplication, this can result in multiple refresh operations, inconsistent token states across requests, and potential authentication failures or security bypasses.
Database-level race conditions also affect Bearer Token systems. Consider a token revocation endpoint that updates a database record and then clears an in-memory cache:
app.post('/logout', async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
// Step 1: Update database (revoke token)
await db.query('UPDATE sessions SET active = false WHERE token = $1', [token]);
// Step 2: Clear cache (potential race condition)
await cache.delete(`session:${token}`);
res.json({ success: true });
});If Step 2 completes before Step 1 fully propagates through the database, a concurrent request might still find the token in cache and use it successfully, bypassing the revocation.
Bearer Tokens-Specific Detection
Detecting race conditions in Bearer Token systems requires both static analysis and dynamic testing approaches. Static analysis examines code paths for single-point validation patterns, missing re-validation, and improper token state management.
Key indicators to scan for:
// Anti-pattern: Single validation at function entry
function processRequest(req) {
const token = extractToken(req);
const user = validateTokenOnce(token); // Only validated once
// Long operation without re-validation
return doWork(user);
}Dynamic testing involves creating concurrent request scenarios that stress token state transitions. This includes:
- Concurrent token refresh operations to detect duplicate refresh logic
- Logout during active operations to test revocation timing
- Rapid token rotation to expose caching inconsistencies
middleBrick's black-box scanning approach is particularly effective for detecting Bearer Token race conditions because it tests the actual runtime behavior without requiring source code access. The scanner creates controlled concurrent request scenarios and monitors token state changes throughout the request lifecycle.
For example, middleBrick tests:
// Simulated race condition test
async function testTokenRaceCondition(baseUrl) {
// Start long operation with valid token
const longOp = fetch(`${baseUrl}/long-operation`, {
headers: { Authorization: 'Bearer VALID_TOKEN' }
});
// Immediately trigger token revocation
await fetch(`${baseUrl}/logout`, {
headers: { Authorization: 'Bearer VALID_TOKEN' }
});
// Wait for long operation to complete
const result = await longOp;
// If operation completed successfully after revocation, race condition exists
return result.status === 200;
}middleBrick also analyzes OpenAPI specifications for race condition indicators, such as endpoints that perform state-changing operations without proper concurrency controls or token re-validation requirements.
The scanner's LLM security capabilities extend to detecting race conditions in AI-powered authentication systems that use Bearer Tokens for API access to language models, testing for prompt injection scenarios that could manipulate token state during concurrent operations.
Bearer Tokens-Specific Remediation
Effective remediation of Bearer Token race conditions requires implementing proper token lifecycle management and concurrency controls. The most robust approach involves re-validation at critical operation points rather than single-point validation.
async function processDocument(req, res) {
const token = req.headers.authorization.split(' ')[1];
// Re-validate token before critical operations
const validateAndCheckRevocation = async () => {
const user = await validateToken(token);
const isActive = await checkSessionActive(user.id);
if (!isActive) throw new Error('Session revoked');
return user;
};
try {
const user = await validateAndCheckRevocation();
const result = await heavyProcessing(user.id);
// Re-validate before completing operation
await validateAndCheckRevocation();
return res.json(result);
} catch (error) {
return res.status(401).json({ error: error.message });
}
}For token refresh race conditions, implement proper locking or promise deduplication:
class TokenManager {
constructor() {
this.token = null;
this.refreshPromise = null;
this.lock = false;
}
async getFreshToken() {
if (this.token) return this.token;
if (this.lock) {
// Wait for ongoing refresh to complete
if (this.refreshPromise) await this.refreshPromise;
return this.token;
}
this.lock = true;
this.refreshPromise = this.performRefresh();
try {
const newToken = await this.refreshPromise;
this.token = newToken;
return newToken;
} finally {
this.lock = false;
this.refreshPromise = null;
}
}
async performRefresh() {
// Actual refresh logic
const response = await fetch('/auth/refresh', {
headers: { Authorization: `Bearer ${this.token}` }
});
return response.json().then(data => data.token);
}
}For database-level race conditions, use transactional operations with proper isolation levels:
app.post('/logout', async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
try {
await db.transaction(async (client) => {
// Step 1: Update session status
await client.query('UPDATE sessions SET active = false WHERE token = $1', [token]);
// Step 2: Immediately check if token was found
const result = await client.query('SELECT active FROM sessions WHERE token = $1', [token]);
if (result.rows.length === 0 || !result.rows[0].active) {
throw new Error('Session not found or already inactive');
}
// Step 3: Clear cache only after successful DB update
await cache.delete(`session:${token}`);
});
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});Implement token versioning to detect stale tokens:
async function validateToken(token) {
const [header, payload, signature] = token.split('.');
const data = JSON.parse(atob(payload));
// Check token version against current system version
if (data.version !== CURRENT_TOKEN_VERSION) {
throw new Error('Token version outdated');
}
// Additional validation logic...
Using middleBrick's CLI tool, you can scan your API endpoints for these race condition patterns before deployment:
npx middlebrick scan https://api.example.com --test-concurrency --check-token-revocationThe CLI will simulate concurrent requests and token state changes to identify race condition vulnerabilities in your Bearer Token implementation.