HIGH credential stuffingbearer tokens

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 Authorization header 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 Unauthorized or 400 Bad Request responses on the token endpoint compared to successful 200 OK responses.
  • 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-After headers 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.

Frequently Asked Questions

Does enabling rate limiting on the login endpoint affect legitimate users?
When configured sensibly (e.g., 5 attempts per 15 minutes per IP), rate limiting blocks only the rapid, automated bursts typical of credential stuffing. Genuine users who mistype a password a few times will still succeed within the limit, while attackers are throttled or blocked.
How can I test whether my API is vulnerable to credential stuffing before deploying changes?
Run a middleBrick scan against the API’s authentication endpoint. The tool will issue rapid login requests without credentials and report whether rate limiting, lockout, or excessive token issuance is missing, giving you concrete findings to fix before release.