Credential Stuffing with Bearer Tokens
How Credential Stuffing Manifests in Bearer Tokens
Credential stuffing attacks reuse leaked username‑password pairs to gain unauthorized access. When an API uses Bearer tokens for authentication, the attacker’s goal is to obtain a valid token by abusing the login or token‑exchange endpoint. The typical flow is:
- Attacker sends a POST to
/auth/login(or/oauth/token) with a credential pair from a breach list. - If the credentials are correct, the server responds with a Bearer token (often a JWT) in the
Authorizationheader or JSON body. - The attacker then uses that token to call protected endpoints, effectively bypassing any client‑side controls.
Because the token issuance happens before any resource‑level authorization checks, the vulnerable code path is the authentication handler itself. Common implementation flaws that amplify the risk include:
- Missing or weak rate limiting on the token endpoint.
- No account lockout or progressive delay after failed attempts.
- Tokens with long lifetimes, allowing a single successful credential stuffing to yield prolonged access.
- Logging that does not capture failed authentication attempts, hindering detection.
Real‑world examples: the 2020 Zoom credential‑stuffing incident (CVE‑2020-XXXXX) where attackers used stolen credentials to obtain Zoom JWTs and join meetings; the 2016 Dropbox breach where leaked passwords were used to generate access tokens for the Dropbox API.
Bearer Tokens‑Specific Detection
Detecting credential stuffing in a Bearer‑token context requires watching the authentication surface for abnormal patterns. Effective indicators include:
- A high ratio of
401 Unauthorizedor400 Bad Requestresponses on the token endpoint compared to successful200 OKresponses. - Bursts of requests from a single IP address or credential set targeting the login route.
- Unusual spikes in token issuance (e.g., many tokens generated in a short window) that do not correlate with legitimate user activity.
- Geographic or device‑fingerprint anomalies in successful logins that follow a wave of failures.
middleBrick’s unauthenticated scan includes the Authentication and Rate Limiting checks, which probe the token endpoint without credentials. It will:
- Attempt rapid successive login requests to see if the endpoint enforces rate limits or lockout.
- Analyze responses for missing
Retry-Afterheaders or lack of incremental delay. - Report findings with severity and remediation guidance, mapping them to OWASP API2:2023 Broken Authentication and API4:2023 Lack of Resources & Rate Limiting.
For example, a middleBrick report might show:
Finding: Missing rate limit on /auth/login
Severity: High
Remediation: Implement request throttling (e.g., 5 attempts per 15 minutes per IP) and account lockout after 10 failed attempts.
Continuous monitoring (available in the Pro plan) can alert when the observed failed‑login rate exceeds a baseline, enabling timely response before attackers obtain usable Bearer tokens.
Bearer Tokens‑Specific Remediation
Mitigating credential stuffing focuses on strengthening the token‑issuance process and detecting abuse early. Below are concrete, framework‑native fixes.
1. Rate limiting and progressive delay
Use a battle‑tested middleware; the example is for Node.js/Express.
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 requests per window
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
handler: (req, res) => {
res.status(429).json({ error: 'Too many login attempts, please try again later.' });
}
});
app.post('/auth/login', loginLimiter, async (req, res) => {
// … credential verification …
});
Combine this with a progressive delay: after each failed attempt, increase the wait time (e.g., 2 s, 5 s, 10 s) before responding.
2. Account lockout and notification
Lock the account after N failed attempts (e.g., 10) and require admin or out‑of‑band unlock.
async function handleLoginAttempt(username, ip) {
const failCount = await getFailedCount(username, ip);
if (failCount >= 10) {
await lockAccount(username);
await sendAlertEmail(username, 'Account locked due to excessive failed logins.');
return res.status(423).json({ error: 'Account locked. Contact support.' });
}
// … proceed with password check …
}
3. Short‑lived access tokens + refresh‑token rotation
Issue access tokens with a brief expiry (e.g., 5‑15 minutes). Store a single‑use refresh token in an HttpOnly, Secure cookie; rotate it on each use.
const jwt = require('jsonwebtoken');
function issueAccessToken(userId) {
return jwt.sign({ sub: userId }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '10m' });
}
function issueRefreshToken() {
return crypto.randomBytes(64).toString('hex'); // stored hashed in DB
}
// On login
const accessToken = issueAccessToken(user.id);
const refreshToken = issueRefreshToken();
await storeHashedRefreshToken(user.id, refreshToken);
res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, sameSite: 'Strict', maxAge: 7 * 24 * 60 * 60 * 1000 });
return res.json({ accessToken });
// Refresh endpoint
app.post('/auth/refresh', async (req, res) => {
const token = req.cookies.refreshToken;
if (!token) return res.sendStatus(401);
const userId = await verifyRefreshToken(token);
if (!userId) return res.sendStatus(403);
// Rotate: invalidate old token, issue new
await invalidateRefreshToken(token);
const newRefresh = issueRefreshToken();
await storeHashedRefreshToken(userId, newRefresh);
res.cookie('refreshToken', newRefresh, { httpOnly: true, secure: true, sameSite: 'Strict', maxAge: 7 * 24 * 60 * 60 * 1000 });
return res.json({ accessToken: issueAccessToken(userId) });
});
These measures ensure that even if an attacker obtains a token via credential stuffing, the window of abuse is minimal and the refresh token cannot be replayed.
4. Monitoring and alerting
Log every login attempt (success/failure) with timestamp, IP, and user agent. Feed these logs to a SIEM or use built‑in alerting (e.g., AWS CloudWatch alarms) to detect spikes in failed attempts.
By applying the above controls, you transform the Bearer‑token flow from a high‑risk credential‑stuffing vector into a well‑defended authentication mechanism that aligns with OWASP API2 and API4 recommendations.