Common auth mistakes I find when reverse-engineering APIs
After years of poking at APIs that weren't meant to be poked at, these are the auth patterns that break most often — and why.
I've reverse-engineered enough APIs to notice patterns. Not in how auth is supposed to work — in how it actually breaks. These are the mistakes I see over and over, with real examples of what they look like in the wild.
1. Token in the URL query string
GET /api/v1/documents?token=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0Mn0.abc HTTP/1.1
Host: app.target.comThis token is now in: server access logs, browser history, any proxy logs, the Referer header if the user clicks an external link, and whatever analytics platform captures full URLs. I've extracted valid tokens from Google-cached URLs, Wayback Machine snapshots, and shared Slack screenshots where someone forgot to redact the URL bar.
💡 The fix Always send tokens in the
Authorizationheader. If you need URL-based auth (webhooks, email links), use short-lived, single-use tokens with a separate validation endpoint.
2. Tokens that never expire
I decode every JWT I intercept. Here's what I often find:
{
"sub": "user_42",
"email": "admin@company.com",
"role": "admin",
"iat": 1704067200,
"exp": 1735689600 // ← 1 year from issuance
}A year-long token. No refresh rotation. If I grab this once — from a log, a leaked .env, a compromised laptop — I have admin access for 365 days. I've seen worse: tokens with no exp claim at all, meaning they're valid forever.
⚠️ What I test I take an old token (even months old) and replay it. If it still works, there's no token rotation and no revocation mechanism. This is surprisingly common in internal APIs.
3. Client-side role enforcement
The frontend hides the admin panel. The API doesn't check. I see this pattern constantly in SPAs:
// Frontend router (React/Vue/Angular)
if (user.role === 'admin') {
showAdminPanel();
}
// But the API endpoint has NO role check:
// GET /api/admin/users HTTP/1.1
// Authorization: Bearer <regular_user_token>
//
// HTTP/1.1 200 OK
// [{"id": 1, "email": "ceo@company.com", "role": "admin"}, ...]The developer assumed the router IS the security boundary. It's not. I just call the endpoint directly with curl. This is the #1 reason I always map the full API surface before testing auth — hidden endpoints are often unprotected endpoints.
4. Predictable reset tokens
I request a password reset and intercept the token. Then I look at the pattern:
# Request 1 at 14:30:01 → token: 173820601142
# Request 2 at 14:30:05 → token: 173820601542
# Request 3 at 14:30:09 → token: 173820601942
# Pattern: timestamp (unix ms) + user_id
# Predictable. Brute-forceable in seconds.Even "random" 6-digit codes are vulnerable if there's no rate limiting on the verification endpoint:
$ for code in $(seq 100000 999999); do
resp=$(curl -so /dev/null -w "%{http_code}" \
-X POST https://api.target.com/reset/verify \
-d '{"code":"'$code'","email":"victim@company.com"}')
[ "$resp" != "400" ] && echo "FOUND: $code" && break
done
# 900,000 attempts. No lockout. No rate limit.
# Average time to crack: ~15 minutes.💡 Defense Use cryptographically random tokens (32+ bytes). Rate limit verification attempts (max 5 per email per hour). Lock the account after 10 failed attempts. Expire tokens after 15 minutes.
5. IDOR on user-scoped endpoints
The single most common vulnerability I find. The test is trivial:
# Authenticated as user 42
GET /api/users/42/documents HTTP/1.1 → 200 ✓ (my docs)
# Change the ID
GET /api/users/43/documents HTTP/1.1 → 200 ✓ (someone else's docs)
# The API checks: "is this request authenticated?" ✓
# The API does NOT check: "does this user own resource 43?" ✗I test every endpoint that has an ID parameter. User IDs, document IDs, org IDs, invoice IDs — anything that references a specific resource. In my experience, about 30% of APIs with sequential IDs have at least one IDOR.
6. JWT audience confusion
In microservice architectures, multiple services share the same JWT issuer. The token has an aud (audience) claim — but does each service actually validate it?
// Token issued for: billing-service
{
"sub": "user_42",
"aud": "billing-service",
"role": "user",
"iss": "auth.internal.company.io"
}
// Send it to: admin-service
// GET /api/admin/config HTTP/1.1
// Authorization: Bearer <billing-service-token>
//
// HTTP/1.1 200 OK ← admin-service accepted a billing token
// {"database_url": "postgres://...", "api_keys": [...]}The admin service validated the signature (correct) but didn't check the audience (wrong). A token meant for one service grants access to another. This is lateral movement within the architecture.
Why these keep happening
┌─ THE AUTH DECAY TIMELINE ────────────────────────┐
│ │
│ Month 1: Auth implemented. Works. Tested. │
│ Month 3: New endpoints added. Auth copied. │
│ Month 6: Intern adds endpoint. Forgets auth. │
│ Month 12: Refactor. Auth middleware skipped. │
│ Month 18: "Why does /api/internal/* work │
│ without a token?" │
│ │
└──────────────────────────────────────────────────┘Auth is implemented once, early in the project, and never systematically re-audited. The codebase grows, new endpoints get added by different developers, and nobody re-checks whether the auth middleware actually covers everything. That's the gap I exploit — and the gap I help teams close.
Related posts
- Security
How I analyze API security headers in 30 seconds
A quick checklist for reading HTTP response headers and spotting security misconfigurations before you even look at the response body.
May 18, 2026 · 7 min - Security
How I got free cinema credit by ordering -2 popcorns
A missing input validation on M-Tix Cinema XXI's food ordering API let me increase my account balance by submitting negative quantities. No tools needed — just a browser.
May 19, 2026 · 6 min - API RE
What your JWT tokens reveal about your backend
JWTs are meant to be opaque to users. They're not. Here's what I learn about your architecture just by decoding one.
May 18, 2026 · 8 min