Software Development with AI 12 min read Updated Jul 16, 2026

How to Review Authentication Code With AI Without Missing Common Flaws

Four hundred lines of login code and a yes-or-no. You can read every line, find nothing, and still be wrong — because what will hurt you is in the file nobody opened alongside it: a session that outlives a logout, a reset that leaves the old one working.

Build an Auth Review Prompt

You can read every line and still miss it

The ask is four hundred lines and a yes-or-no: is our login code fine? So you read it, carefully, and you find nothing — and you might be completely wrong, not because you skimmed, but because the file you were handed isn't where the answer lives. It's in the logout handler nobody opened alongside it, where the cookie gets cleared and the server-side session keeps working. Or in the password-reset flow, where a user changes their password precisely because they think someone else is in their account, and every session that person already has stays valid. Two files, written eight months apart, each correct on its own terms. The flaw is the property that should hold between them and doesn't, and no amount of reading one of them will surface it.

That's what makes authentication a poor fit for reading code and a good fit for reviewing a *lifecycle*: identity gets established, a session comes into being, it's carried and sometimes elevated, it's meant to end — and alongside all that there are side doors, reset and recovery, that can quietly reopen it. AI is genuinely useful for that sweep, because it's patient about cross-checks that get skipped at 5pm. Be exact about what it's working from, though. The model sees a short, fixed list: the code you paste, the configuration you paste, and the prose you write about the environment around them. There's no second channel — no repository, no source control, no identity provider, no database or session store, nothing running — so nothing here logs in, replays a session, tests a token, executes a line, or finds out what your framework does by default. It is not a vulnerability scanner and it cannot tell you whether a flaw is real. What comes back is a set of candidates, and one that reads convincingly has been neither reproduced nor confirmed by anything. A qualified security reviewer decides which survive; anything high-stakes still wants real testing and a proper review of the built thing. Nothing here makes an implementation secure, fixed, or free of vulnerabilities — and the purpose throughout is finding and closing gaps in a system you own, not probing anyone else's.

One boundary worth setting now, because it saves confusion later: this is authentication — establishing who someone is and creating the authenticated state that follows. Whether that established identity may then perform a given action on a given record is authorization, a separate model with its own design work. When something here brushes against permissions, it belongs to that side, and this review hands it over rather than absorbing it.

Where the flaws collect

Read the lifecycle as a sequence and the pattern becomes obvious. A credential is verified. A session is created and handed to the browser. It's carried, maybe refreshed, maybe elevated. It's supposed to be destroyed on logout. And in parallel: password reset, and MFA recovery, each of which can produce an authenticated state without anyone logging in. Every stage is usually implemented competently. The flaws gather at the joins — the transition where something should have changed and didn't, or should have been revoked and wasn't.

That geometry is why the same handful of flaws recur across codebases that share no code. A session identifier that existed before login and still exists after it means authentication changed who the session belongs to without changing the session. A logout that clears a cookie has ended the browser's *copy* while the server's copy waits patiently. A reset that sets a new password and touches nothing else has handed the user a remedy that doesn't remedy anything, at the moment they're using it because they're frightened. An MFA recovery route that's easier than the login it protects has quietly relocated where your authentication is decided. None of those is a wrong line.

It also tells you what to ask for. Not "is this secure" — that question has no answer and it will get you a confident paragraph. Ask for the lifecycle walked stage by stage, with three things reported per stage: what the supplied material shows, what it doesn't show, and what should be true at that transition. The value isn't insight. It's that a model will actually walk all seven stages the session review names, including the three you'd have skimmed because you were sure about them.

Step 1: Trace one login from request to cookie

Before any review prompt, follow one successful login all the way through and collect what it touches — the route that accepts the credentials, the function that verifies them, whatever creates the session, the place the cookie or token gets attached, the middleware that reads it on the next request. That path usually crosses three or four files, and assembling it is the work the model cannot do for you: it sees what you paste, so a lifecycle pasted in fragments gets reviewed in fragments, and the seams between fragments are exactly what you came for.

Then do the same for the paths that produce an authenticated state *without* a login: the reset-consumption endpoint, the MFA challenge, the remember-me cookie, the invite or magic link if you have one. Those get skipped far more often than the login does, because they don't feel like authentication — and they end in the same place holding the same session. Add the configuration that governs any of it: cookie settings, session store config, timeout values.

Then write the environment down in prose, because much of what decides a finding is a claim the code cannot make. The session store and its TTL. Whether rate limiting lives upstream. What terminates TLS. Whether the login goes through your framework's own auth helper or hand-rolls the session write — which turns out to matter more than almost anything else you'll paste. A review conducted without those facts will invent an environment and be confidently wrong about it.

Step 2: Mark the parts you can't see

Half the accuracy in an authentication review comes from one instruction: make the model separate what the supplied material shows from what it's assuming. Left alone, it reads a controller that never mentions rate limiting and reports the absence as a finding — when your rate limiting is a gateway rule it has never seen. Both halves of that are the same error: treating silence in a fragment as evidence about a system.

So write the contract explicitly. Every observation names what it rests on. Anything the code doesn't demonstrate becomes a question rather than a claim — "the pasted code doesn't show a new session identifier being issued after authentication; get the middleware" is useful, and "no session regeneration" is an assertion the fragment can't carry. Hold that line even when the answer seems obvious, because the headline is where it slips: a finding whose body says *based on the pasted fragment only* and whose title says *is not* has hedged in the small print and asserted in the part people read.

This is also where you head off the remediation problem. A model asked to fix authentication will produce specific-looking code for a framework and version it is inferring. Ask for the property that should hold and what to verify, and let the person who knows the stack write the change. The Session Management Review Prompt is a copy-paste prompt built on exactly this lifecycle — it walks fixation, expiry, rotation on privilege change, invalidation on logout and password change, storage, cookie flags and concurrency, in that order, against what you paste. Keep its own framing: treat the output as a review to verify, not a security audit or a guarantee that every session hole was caught. The Code Review Prompt Generator is the tool that builds it, and its session-review setup is what makes the prompt sharp rather than general — a security focus rather than an architecture one, a strict style, and an objective already pointed at the classic holes. It emits prompt text; you run it in your own AI assistant.

Step 3: Check the transitions where the session changes hands

Work the stages where state moves, because that's where things get forgotten. At creation: does a fresh session identifier come into existence at the moment authentication succeeds, or does the one from the anonymous visit carry on with new privileges attached? This is the place to be careful about framework claims in both directions. Several frameworks do issue a new identifier as part of their own login call — but that's the *login helper* doing it, not the session object noticing it was written to. Code that sets a user id into the session by hand generally bypasses the helper and gets none of it. So "the framework probably handles this" is a hypothesis about a specific call you either made or didn't, and it's answerable by looking rather than by assuming.

Then how the session is carried. Cookie flags are small and consequential, and worth understanding rather than pattern-matching: `HttpOnly` keeps the cookie out of reach of scripts on the page, `Secure` keeps it off plaintext connections, and `SameSite` governs whether it rides along on requests other sites initiate — which is why that one is entangled with cross-site request forgery rather than with theft, and why loosening it has consequences past the cookie itself. (`SameSite=None` also requires `Secure`, which is the one hard interaction between the three.) Scope matters too: the domain and path a cookie is set for decide how far it travels, and a session cookie scoped wider than the app that owns it is quietly shared with everything else on that domain.

Then the ending, which is where confident wrongness concentrates. Logout should destroy the session where the session actually lives; clearing the browser's cookie removes the client's ability to present it and does nothing to the record on the server. Expiry has the same double life — a cookie's lifetime and the server-side record's are different settings, and a cookie that dies at browser close in front of a record that lives two weeks is not a fourteen-minute session. And expiry itself is two decisions, not one: an absolute lifetime that ends the session regardless, and an idle timeout that ends it after inactivity. Having only one is allowed. Having only one by accident is not.

Step 4: Follow the doors that bypass the front one

Password reset and MFA recovery are authentication — they just don't look like it, which is why they get built by whoever had the ticket and reviewed by nobody. Walk the reset token through its life: how it's generated, how long it lives, whether using it once retires it, what happens to the old one when a user requests another. Then the question that decides whether the feature works at all: when the password changes, what happens to sessions that already exist? If they survive, the user has done the thing your product told them to do about a compromised account and the compromise is untouched. That transition is the most commonly missing line in the whole lifecycle.

MFA has the same shape one level up. Enrollment should be tied to an authenticated user and should say what it does about existing sessions. The challenge should be something a login cannot be completed without, rather than a step the flow can be resumed past. And recovery — codes, fallback, the support path — is the one to look at hardest, because a recovery route weaker than the factor it recovers has moved where your authentication is decided. If losing a phone can be resolved with something anyone could also produce, the second factor is decorative.

The Credential and MFA Policy Prompt is a copy-paste prompt for the standard those paths ought to meet — password storage, factors, lockout and rate-limiting, and recovery designed so it isn't weaker than the login it protects. It's design-side rather than review-side, and that's the point: a review needs something to review *against*, and if nobody ever wrote the policy down, a good share of your findings are really the discovery that there wasn't one.

Step 5: Write findings a maintainer can act on

A finding that says "session management is weak" gets closed as wontfix, and it deserves to be. The shape that survives contact with the person who wrote the code carries: where it is, what was observed, what that observation rests on in the supplied material, why it matters in terms of what becomes possible, how bad it is, what should change, and what has to be checked before anyone believes it. That last field is what keeps the review honest — it's the standing admission that a model reading a fragment has established nothing.

Severity should track consequence, not the model's confidence. A session that outlives a password reset is severe because it defeats the user's own remedy at the moment they reach for it, and that's true regardless of how the finding is worded. Rank by what it costs if true, and let the verification field carry the doubt rather than softening the severity to hedge. It's fine — normal, in fact — for two findings to be equally severe on very different evidence; what isn't fine is a severity that quietly encodes a guess.

Then separate two things a model will happily merge. "The reset token has no visible expiry" is a finding: there's a right answer and you can go and look. "Login says *no account with that email* when the account doesn't exist and *wrong password* when it does" is a decision, because the fix has a real cost — identical responses are harder to use and generate support contacts from people who mistyped their address — and because login is rarely the only door: signup and the reset-request endpoint usually answer the same question, so closing one may buy very little. That's a product call with an accountable owner, not something a code review gets to settle. The same is true of account lockout, which trades a brute-force risk for an availability one and has several defensible designs.

And once findings exist, the verification field is a work list. Tests are how a candidate becomes a fact — the Authentication Test Prompt is a copy-paste prompt for that step, built with the Test Case Prompt Generator, and its own line on the division of labour is the right one: review finds, tests verify. You write and run them; nothing here executes anything.

Common mistakes

What separates a review that finds the real gap from one that produces a tidy list nobody acts on:

  • Pasting the login handler and calling it the auth code. The flaws live between login, logout, reset and MFA. A fragment gets a fragment's review, and the seam is what you needed looked at.
  • Letting silence become a finding. Code that doesn't mention rate limiting isn't evidence there is none — it might be a gateway the model never saw. Absence in a fragment is a question.
  • Asserting in the headline what you hedged in the body. "Is not rotated" and "no rotation is visible" are different claims, and only one of them is supported when the middleware wasn't pasted.
  • Deciding the framework handles it. Frameworks that rotate the session do it inside their own login call — code that writes the session by hand skips that path entirely. It's a question about which call you made, not a background assumption.
  • Accepting the model's remediation code. It's guessing at your framework and version. Take the property that should hold; let someone who knows the stack write the line.
  • Reviewing logout as if the cookie were the session. Clearing the browser's copy ends nothing on the server. The record is what has to be destroyed, where it lives.
  • Settling the enumeration and lockout trade-offs in a code review. Both have real costs and several defensible answers, and both belong to an accountable owner rather than to whoever ran the prompt.
  • Reading the output as a verdict. These are candidates for a qualified reviewer — NewPrompt doesn't scan your repository, run a login, or confirm that one of them is real.

Reviewing a login, logout and password-reset flow

A server-side web app, pasted as three fragments plus what the reviewer knows about the environment.

Three pasted fragments and the environment prose around them, reviewed stage by stage: candidates whose headlines claim only what the evidence carries, one hedged because the middleware is missing and two not because the handlers are whole, plus a struck assumption and a trade-off handed to its owner
WHAT WAS PASTED  (fictional pseudocode, one app, three files)

  # login_controller
  user = users.find_by_email(params.email)
  if user is None:            return render("No account with that email")
  if not verify(params.password, user.password_hash):
                             return render("Wrong password")
  session["user_id"] = user.id        # <- written by hand, not via
  cookie.set("sid", session.id)       #    the framework's login call
  redirect("/dashboard")

  # logout_controller
  cookie.delete("sid")
  redirect("/")

  # password_reset_controller
  token = reset_tokens.find(params.token)
  if token is None:          return render("Invalid link")
  user = users.find(token.user_id)
  user.password_hash = hash(params.new_password)
  users.save(user)
  render("Password updated")

WHAT THE REVIEWER ALSO SUPPLIED  (prose -- the code cannot say these)
  - sessions live in Redis; TTL is 14 days, set in config not shown
  - rate limiting is at the gateway, not in application code
  - TLS terminates at the load balancer
  - cookie attributes are framework defaults; the config was not pasted

WHAT IS NOT VISIBLE  (stated, so it cannot quietly become a finding)
  - the session middleware
  - the cookie/session configuration
  - reset-token generation, expiry, and single-use handling
  - anything about MFA -- the app may not have it

CANDIDATE FINDINGS

  [HIGH] no replacement of the session identifier is visible at login
    where     login_controller, after verify()
    observed  session["user_id"] is assigned to the existing session.
              no call that issues a new identifier appears here.
    based on  this fragment only. the middleware was not supplied.
              note the headline says "not visible," not "absent" --
              the difference is the whole discipline.
    why       if no new identifier is issued, the one a visitor
              arrived holding becomes an authenticated one, and the
              pre-login value keeps working with new privileges.
    should be a fresh identifier issued the moment authentication
              succeeds, the old one discarded
    verify    1. does an auth helper or middleware issue one here?
                 several frameworks do it inside their own login call
                 -- but this code writes session["user_id"] by hand,
                 which usually skips that path. get the middleware.
              2. and if it is absent: how would a pre-login identifier
                 be set or learned here at all? that needs a route --
                 an id accepted from a query string, a cookie writable
                 from a subdomain. the fragment shows none of those
                 either. both halves get answered before anyone rates
                 this finding for real.

  [HIGH] logout removes the cookie and leaves the session alive
    where     logout_controller
    observed  cookie.delete("sid") only. no server-side destroy.
    based on  the whole handler -- it is three lines. nothing is
              hidden here, which is why this one is not hedged.
    why       the browser stops presenting the identifier; Redis still
              honours it for up to 14 days. any copy held elsewhere
              keeps working, and the user believes they logged out.
    should be destroy the session server-side, then clear the cookie
    verify    confirm nothing else destroys it (middleware, a hook)

  [HIGH] a password reset leaves existing sessions valid
    where     password_reset_controller
    observed  the hash is written; no session invalidation appears.
    based on  the whole handler
    why       this is the flow a user reaches for when they think
              someone else is in their account. as written, the reset
              changes what a future login needs and nothing about the
              access that already exists. the remedy does not remedy.
    should be invalidate all of that user's sessions on password
              change (in a token-based reset the user is usually not
              logged in, so there is no current one to preserve)
    verify    is a hook doing this elsewhere? if not, fix this first.

  [OPEN QUESTION -- not a finding, and not the reviewer's call]
    login answers "No account with that email" and "Wrong password"
    differently, which tells anyone asking whether an address is
    registered. the fix -- one identical response -- has a real cost:
    it is worse to use and it generates support contacts from people
    who mistyped. and login is not the only door: signup and the
    reset-request endpoint usually answer the same question, so
    fixing this one alone may buy nothing. that trade-off belongs to
    whoever owns the account experience, decided on purpose.

  [NOT A FINDING -- assumption the code cannot support]
    a first pass flagged "no rate limiting on login." the reviewer's
    notes say rate limiting is at the gateway, which the model never
    saw. silence in a fragment is not evidence -- struck, and recorded
    as: confirm the gateway rule covers this route.

  [NOT A FINDING -- yet]
    cookie attributes were not pasted. HttpOnly / Secure / SameSite
    are unknown, not missing. -> get the config and re-run this
    section rather than guessing at defaults.

WHAT HAPPENS NEXT
  three candidates, none confirmed. nothing here logged in, replayed a
  session, or reproduced anything. the reset finding goes to whoever
  owns auth with its verification note attached; the enumeration
  question goes to the account owner as a decision; the cookie section
  gets re-run once the config exists. tests are what turn any of this
  from plausible into known.

Where this fits in NewPrompt

This is one pass inside a larger security review, and the AI Security Review Workflow is that arc: put the model in a security engineer's seat, review for the threats that actually cause breaches, then prove the risky paths with tests. Its scope leads with auth in its own words — it's for reviewing code where a vulnerability would actually hurt, and it's explicit that it reviews existing code rather than designing security from scratch. This guide is that middle step taken deep on one subsystem, and its third step is where your verification notes go. As the workflow puts it, NewPrompt supplies the prompts and step order; you run them in your AI tool and own the security judgment — the output is a review artifact you act on, not certified-secure code, and no tool finds every vulnerability.

The concrete pieces are three, and they divide the work cleanly. The Session Management Review Prompt walks the session lifecycle, and the Code Review Prompt Generator is the tool that builds it, with a session-review setup that aims the prompt at those holes rather than at code quality in general. The Authentication Test Prompt, built with the Test Case Prompt Generator, is the verification half — review finds, tests verify. And the Credential and MFA Policy Prompt is the standard side: the policy a review measures against, worth writing down before you discover that half your findings are its absence.

Two neighbours are worth keeping distinct, because the names overlap and the jobs don't. Designing an access-control model decides what an authenticated actor may then do — roles, resources, actions, a permission matrix — and it's a design artifact produced before there's code. This is the other side of that door: not what you may do once you're in, but how the system decided you were you. And if your session rides in a stateless token rather than a server-side record, one part of this changes shape — revocation on logout and invalidation after a reset stop being a delete and become a design problem of their own. That's a genuinely different review with its own traps, and it isn't this one.

What makes authentication worth this much care is that its failures are patient. A missing rotation breaks nothing: the app works, the tests pass, and the property that should have held simply doesn't — for years, until the day it matters and then it matters all at once. Which is also why the review can't stop at a list. Three fragments went in and three plausible sentences came out about a system nothing has run. Turning those into something you can act on is the unglamorous part: getting the config, finding out whether the login goes through the framework's own call, writing the test that proves the session really is gone, and putting the enumeration question in front of the person whose product it is. That's a morning's work, and it's the entire difference between a review and a document.

Tools for this guide

Each generates the prompt described above — you run it in your own AI assistant.

Ready-made resources

Reusable prompts and templates for the exact steps in this guide.

Take it further

When this task is one step inside a larger workflow or build.

FAQ

What if the authentication is a library I didn't write?

Then the artifact changes and the discipline doesn't. You're no longer reviewing an implementation — you're reviewing your configuration and your call sites, which is often where the flaw actually is: the option left at its default, the hook the library exposes that nobody wired up, the place your code reaches around the library and writes the session itself. So paste the config, the initialization, and every place your application calls into the library, and describe the version. The library's own behaviour is the thing you go and read rather than infer, because this is the exact case where a model will fill the gap confidently — it has seen a lot of that library and it will tell you what it usually does, which is a claim about the ecosystem rather than about your version and your settings. Treat those statements as questions with a documentation link attached.

The findings look convincing. What does that actually establish?

That the prose is good — which is exactly the trap, because a wrong finding and a right one read identically. Nothing here ran your code, attempted a login, replayed a session, or reproduced anything. A model reading a fragment can report that no new session identifier is issued when your framework's login call does it, or that rate limiting is missing when it lives in a gateway it never saw. Both arrive with the same fluency as the finding that's genuinely there. That's why every candidate carries what it rests on and what has to be checked, and why the checking is the step that produces knowledge rather than a document. Treat the output as a list of things to go and confirm; a qualified security reviewer decides which survive, and anything high-stakes still wants real testing and a proper audit of the implementation.

Should the AI write the fix?

Take the property, not the patch. A model asked to remediate authentication produces code that looks native to a framework it's inferring from a fragment — the right shape for some version of something, aimed at a stack it hasn't seen. "A fresh session identifier should be issued the moment authentication succeeds, and the previous one discarded" is durable, checkable, and framework-agnostic; the three lines that supposedly do it in your codebase are a guess with syntax. There's a second reason to keep the review on properties: an auth fix that's subtly wrong fails silently and passes tests, so the change wants to be written by someone who knows the stack and verified by a test that fails without it — which is what the verification note on each finding is for.

Why does a password reset need to touch sessions at all?

Because of why people use it. A reset is the control a user reaches for when they believe someone else is in their account — it's the product's answer to "make it stop." If the flow writes a new password hash and leaves existing sessions valid, the person who is already in stays in, and the user has performed the remedy without receiving one. It's the clearest case of the pattern this review is built around: nothing on any line is wrong, the reset works exactly as written, and the property that makes the feature meaningful was never anyone's line to write. The same logic covers the neighbours — changing a password while logged in, disabling a factor, revoking a device — each should have a stated answer for what happens to sessions that already exist, and "nothing" is allowed to be that answer as long as somebody chose it.