A Login Form Is the Tip of the Iceberg
A login system looks like a weekend task. Two fields, a button, check the password, set a session. You could write it in an hour. And that hour of code would probably work, in the sense that the right person gets in and the wrong password gets rejected.
The problem is everything around it.
Here's a partial list of what a login system actually has to handle once real people use it:
- Storing passwords with a slow, salted hash (not encryption, not plain SHA-256)
- Password reset by email, with tokens that expire, can only be used once, and can't be guessed
- Rate limiting and lockouts so nobody can try a million passwords
- Not revealing whether an email address has an account ("wrong email" vs "wrong password" leaks that)
- Session handling: secure cookies, regenerating the session ID on login, proper logout, timeouts
- CSRF protection on every form that changes something
- Two-factor authentication, ideally passkeys too
- Email verification on signup
- Changing an email address safely without letting an attacker hijack the account
- Logging out other sessions after a password change
- Timing-safe comparisons so response times don't leak information
- Account recovery when someone loses their second factor
None of these is hard to understand. Each one is easy to get slightly wrong. And a slightly wrong login system looks exactly like a correct one right up until someone finds the hole. You don't get a failing test. You get a breach.
That's the core of my argument. Security code has an unusual property: it can be fully working and fully broken at the same time. A bug in your checkout page shows up as an angry customer. A bug in your password reset flow shows up as a quiet stranger in someone's account.
So what should you do instead? It depends on what you're building.
If you use a framework, use its auth system. Laravel, Django, Rails, ASP.NET and the rest all ship with login, registration, reset and session handling that thousands of people have poked at. It's boring and that's the point.
If you'd rather hand it off entirely, there are hosted identity services and open source identity servers (Keycloak and Authentik are common self-hosted picks) that handle login for you over standard protocols like OpenID Connect. Your app just trusts the token that comes back. This also gets you single sign-on across multiple apps, which is nice if you run several.
If you're writing plain code with no framework, and I have a soft spot for that, lean on the language's built-in tools and well-reviewed libraries for the pieces. In PHP, for example, password_hash() and password_verify() take care of hashing properly, and session_regenerate_id(true) on login prevents session fixation. Use a maintained library for TOTP or WebAuthn rather than writing the crypto yourself. You'll still be assembling the parts, so read the OWASP cheat sheets on authentication and session management before you start. They're free and they're very good.
I'm not saying nobody should ever write auth code. People have to write these libraries, and building one as a learning project teaches you a ton. Just don't put that learning project in front of real users' passwords. And if you do roll your own for production, treat it like the riskiest code in your app, because it is. Get someone else to review it.
The login form is the part people see. The iceberg under it is the part that keeps their accounts safe.

Comments