• Agents
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Guides
  • Blog
  • Docs
  • OWASP Top 10
  • Glossary
  • FAQ

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Home/OWASP Top 10/API Top 10/API2: Broken Authentication
API2CWE-287CWE-307

The login endpoint anyone can brute force
Broken Authentication

Auth that works for the happy path and quietly falls over the moment anyone pokes at it.

Authentication flaws appear in roughly one in three APIs tested by OWASP contributors.

Reference: API Top 10 (2023) — API2·Last updated April 7, 2026·By Flowpatrol Team
Broken Authentication illustration

Your login form works on the first try. A password manager autofills, the token comes back, the user lands in the dashboard. What the model didn't build is everything that happens when the login form is called a million times, or when the token is never rotated, or when the password reset link never expires.

Broken Authentication is the whole family of bugs where proving who you are goes wrong: brute-forceable login, guessable tokens, forever-valid JWTs, password reset links that never expire, sessions that don't terminate. Any one of them lets an attacker become a user they are not.

What your AI actually built

You asked for auth. You got a /login route that checks a password, signs a JWT, and sends it back. It works. That's the part that gets tested.

What it didn't build: rate limiting on login. Lockout after failed attempts. Short-lived tokens. Rotation on password change. A refresh flow that actually invalidates old tokens. A reset link that expires in fifteen minutes instead of forever.

On top of that, half the generated JWT middleware accepts tokens with `alg: none`, or signs with a hardcoded secret that ends up in the client bundle. The login passes testing because nobody tested the parts that matter.

How it gets exploited

The attacker finds the login endpoint from a mobile app or SPA bundle — /api/auth/login, JSON in, JWT out.

1
Dump a wordlist
They grab the top 10,000 passwords from a breach corpus and run them against a list of emails scraped from a public leak. No rate limiting, so all 10,000 requests per account go through.
  • 2
    Crack a token
    They sign up legitimately, grab their JWT, and check the algorithm. HS256 with a six-character secret copied from a Stack Overflow answer. Offline dictionary attack finds it in under a minute.
  • 3
    Forge anyone
    Now they can mint a token for any user_id they want. The server happily accepts it because the signing key is the same for every user and the expiration is two weeks out.
  • 4
    Persist
    Even after the real user changes their password, the attacker's forged token still validates. Nothing rotates the key, nothing tracks token version, nothing logs out the other session.
  • The attacker has a permanent backdoor into every account on the platform — not because they broke crypto, but because the auth system was built as a shape, not as a lifecycle.

    Vulnerable vs Fixed

    Vulnerable — no rate limit, weak secret, no rotation
    // app/api/auth/login/route.ts
    import jwt from 'jsonwebtoken';
    
    const JWT_SECRET = 'supersecret'; // hardcoded, short, copied from a tutorial
    
    export async function POST(req) {
      const { email, password } = await req.json();
    
      const user = await db.user.findUnique({ where: { email } });
      if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
        return Response.json({ error: 'Invalid credentials' }, { status: 401 });
      }
    
      // 30-day token. No jti. No refresh. No revocation.
      const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '30d' });
      return Response.json({ token });
    }
    Fixed — rate limited, strong secret, short-lived, revocable
    // app/api/auth/login/route.ts
    import jwt from 'jsonwebtoken';
    import { rateLimit } from '~/lib/rate-limit';
    
    const JWT_SECRET = process.env.JWT_SECRET!; // 256-bit, server-side only
    
    export async function POST(req) {
      const ip = req.headers.get('x-forwarded-for') ?? 'unknown';
      const { email, password } = await req.json();
    
      const ok = await rateLimit(`login:${ip}:${email}`, { max: 5, window: '15m' });
      if (!ok) return Response.json({ error: 'Too many attempts' }, { status: 429 });
    
      const user = await db.user.findUnique({ where: { email } });
      if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
        return Response.json({ error: 'Invalid credentials' }, { status: 401 });
      }
    
      const token = jwt.sign(
        { sub: user.id, tokenVersion: user.tokenVersion },
        JWT_SECRET,
        { expiresIn: '15m', algorithm: 'HS256' },
      );
      return Response.json({ token });
    }

    Three cheap changes: a per-IP-per-email rate limit stops brute force, a real secret out of env stops forgery, and a token version in the claims lets you bump it on password change and invalidate every old session at once. The refresh flow (not shown) lives behind a cookie with stricter controls.

    A real case

    Peloton's API let strangers pull any user's private profile data

    In 2021 a researcher found that Peloton's API returned profile data for any user ID to unauthenticated callers — a missing auth check that turned 3 million users into an open directory.

    Related reading

    Glossary

    Brute Force Protection (Rate Limiting)

    What we find

    broken authentication

    References

    • API2: Broken Authentication — official OWASP entry
    • OWASP API Security Top 10 (2023) — full list
    • CWE-287 on cwe.mitre.org
    • CWE-307 on cwe.mitre.org

    Stress test your auth the way an attacker would.

    Flowpatrol probes login, reset, refresh, and JWT handling across every endpoint. Five minutes, one URL, no agent.

    Try it free