How to Audit Authentication in Your App
Broken access control and authentication failures sit at the top of the OWASP list year after year, and for a boring reason: this code is written once, works in the happy path, and is never tested from the attacker’s side. Your login screen looks fine. The question an audit asks is what happens when someone skips the login screen entirely.
This tutorial is a structured pass over your authentication and authorization, with real probes you can run against a running instance. It is deliberately tool-light, because auth bugs are logic bugs, and logic bugs need a human reading the code and testing the edges. Automated scanners miss most of them. Pair this with auditing authentication and authorization for the conceptual grounding.
What you’ll need
- A running instance of your app you can safely poke (staging, never production data you cannot afford to touch)
curland access to your browser’s dev tools- Read access to the auth-related source code
- About an hour
1. Map every entry point
You cannot audit what you have not listed. Start by enumerating every route and function that either authenticates a user or makes an access decision. Grep is enough to get moving:
# Find route definitions and auth middleware references
grep -rEn "(router\.(get|post|put|delete)|@app\.route|requireAuth|isAuthenticated|authorize|@login_required)" src/Build a simple table: route, method, who should be allowed, and which check enforces that. The gaps write themselves. Any protected route with no visible check in that column is your first finding. Pay special attention to newer endpoints, admin panels, internal tools, and anything with “v2” or “beta” in the path, which is where enforcement most often lags behind the UI.
2. Audit password and credential handling
If you store passwords, they must be hashed with a slow, salted, purpose-built algorithm: bcrypt, scrypt, or Argon2. Fast hashes (MD5, SHA-1, SHA-256) are a finding on their own, because they let an attacker who steals your database test billions of guesses per second.
# Look for weak hashing and any sign of plaintext storage
grep -rEn "(md5|sha1|sha256)\(" src/
grep -rIn "password" src/ | grep -iE "(=|:)\s*req|body|params"Then check the runtime protections around login:
- Is there rate limiting on the login endpoint? Without it, brute force is trivial.
- Does a failed login reveal whether the username exists? “No such user” versus “wrong password” is an enumeration leak. Both cases should return the same generic message and take similar time.
- Are password requirements enforced server-side, not just in the browser?
3. Inspect session and token security
However you keep users logged in, verify the security properties rather than assuming them. For cookie-based sessions, inspect the response headers on login:
curl -i -X POST https://staging.example.com/login \
-d '[email protected]&password=correct-horse'In the Set-Cookie header you want to see HttpOnly (blocks JavaScript from reading the cookie), Secure (HTTPS only), and SameSite=Lax or Strict (limits cross-site sending). A session cookie missing HttpOnly turns any XSS into full account takeover.
For JWTs, read the validation code carefully:
- Is the signature actually verified on every request, with a fixed expected algorithm? Accepting the token’s own
algheader enables the classicalg: noneand algorithm-confusion attacks. - Is there a real expiry (
exp), and is it short? A token valid for a year is a credential you can never revoke. - What is the revocation story on logout or compromise? Stateless tokens do not revoke themselves.
4. Probe authorization on protected routes
This is the highest-value step. Take a protected route and hit it three ways: with no credentials, with a valid low-privilege user, and with a valid but wrong user. The UI enforces nothing; only the server does.
# 1. No token at all. Must be rejected with 401.
curl -i https://staging.example.com/api/admin/users
# 2. A normal user's token against an admin route. Must be 403, not 200.
curl -i https://staging.example.com/api/admin/users \
-H "Authorization: Bearer $USER_TOKEN"If step 2 returns data, you have a missing function-level authorization check, which is one of the most common and most serious findings there is. The endpoint checked that you are logged in but never that you are allowed. Understanding why dynamic probing like this catches what static analysis cannot is the whole point of SAST vs DAST.
5. Test for the common broken access control bugs
With the probing setup from step 4, sweep for the classics.
Insecure direct object references (IDOR). Take a request for a resource you own and change the ID to one you do not:
# You are user 1001. Ask for user 1002's invoice.
curl -i https://staging.example.com/api/invoices/1002 \
-H "Authorization: Bearer $USER_TOKEN"A 200 with someone else’s data means the server checked that you are logged in but not that the object belongs to you. Test this on every resource that takes an ID.
Privilege escalation via mass assignment. Try setting a field you should not control, like a role, through a normal update:
curl -i -X PATCH https://staging.example.com/api/me \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"role":"admin"}'Then re-fetch your profile and check whether the role stuck. If it did, your update handler trusts client-supplied fields it should ignore.
6. Review account recovery and logout
Attackers who cannot get through the front door go around the back, and password reset is the back door. Audit it as carefully as login:
- Reset tokens must be long, random, single-use, and short-lived (minutes, not days).
- The reset flow must not disclose whether an email is registered.
- Resetting a password should invalidate existing sessions, otherwise an attacker who is already in stays in.
Finally, confirm that logout genuinely ends the session. Log in, capture the session or token, log out, and replay the old credential:
# After logging out, the old token must now be rejected.
curl -i https://staging.example.com/api/me \
-H "Authorization: Bearer $OLD_TOKEN"A 200 here means logout is cosmetic, the session lives on the client but never dies on the server, and “log out on all devices” does nothing. If you offer MFA, verify it cannot be skipped by hitting the post-MFA endpoint directly with a session that only cleared the first factor.
Get a second pair of eyes (free)
You now have a real auth audit, which is more than most teams ever do. But authorization bugs hide in the interactions between features, and the most dangerous ones are the checks that exist everywhere except the one new endpoint. After your own pass, get a free code audit: a Webisoft engineer will manually review your authentication and access control alongside the rest of the codebase and hand you a prioritized report. It is free and it is a human, which is exactly what auth needs.