The worst version of a login endpoint tells the attacker exactly what they need:
- "No account with this email" — try another email.
- "Incorrect password" — you found a real account, now work on the password.
That is user enumeration, and it turns a login form into an account-discovery API. My login endpoint returns the same response for a missing email and a wrong password:
$user = User::where('email', $request->validated('email'))->first(); if (! $user || ! Hash::check($request->validated('password'), $user->password)) { return response()->json(['message' => 'Invalid credentials'], 401); } One condition, one message, one status code. A wrong email and a wrong password are indistinguishable from the outside.
The tradeoff is honest: legitimate users get a slightly less helpful error. "Invalid credentials" does not tell you whether you mistyped your email or your password. I accept that, because the frontend renders the message as-is and password managers make mistyped emails rare. Security wins over a marginally nicer error.
The real fix for brute force is rate limiting on the login route, which is a separate decision — but the generic message is the one that costs nothing and closes the enumeration hole on day one.
Small decisions like this are why I extracted my auth flow into the Laravel + React SaaS Starter Kit instead of rewriting it per project: https://kamranofficial.gumroad.com/l/mhekoig


