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 Refererheader 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 Authorization header. 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 expclaim 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 ResearchMay 18, 20267 min read
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.
- Security ResearchMay 19, 20266 min read
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.
- API Reverse EngineeringMay 18, 20268 min read
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.