Intermediate

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)
  • curl and 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 alg header enables the classic alg: none and 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.

FAQ

Frequently asked questions

Is it safe to run these curl probes against production?

Run them against staging whenever you can. Most of the probes are read-only, but the mass assignment test actually modifies a record, and even the read probes touch real user data if they succeed. If staging does not exist, use a dedicated test account and clean up after yourself.

Do I still need this audit if I use a managed identity provider?

Yes. A hosted provider handles password storage and login flows well, but authorization is still entirely your code. IDOR, missing function-level checks, and mass assignment all live in your endpoints, and no identity provider can know which user is allowed to see which invoice.

Should a failed authorization check return 403 or 404?

Either is defensible. A 403 admits the resource exists, while a 404 hides it, which some teams prefer for objects a user should not even know about. What actually matters is that the behavior is consistent across endpoints and that it is never a 200.

My JWTs cannot be revoked. Is that a finding?

It is a design tradeoff you should make deliberately rather than inherit. Keep the expiry short so a stolen token has a small window, pair it with a refresh flow, and consider a server-side denylist for logout and compromise if the stakes justify the statefulness. A long-lived stateless token is a credential you can never take back.

The mass assignment probe worked and I became admin. What is the fix?

Allowlist the fields an update handler accepts instead of binding the whole request body to your model. Fields like role, permissions, and account status should only change through dedicated, separately authorized code paths. And reset your test account's role before you forget it is elevated.

Free · no strings shown later

Get your free code audit

A repo URL and two minutes of your time. A Webisoft engineer sends back written findings (security, tech debt, performance) with file-and-line specifics.

Request my free audit