← All writing

Building Secure Authentication from Scratch

Most tutorials on "secure auth" stop at hashing passwords and issuing a JWT. That's maybe a third of the picture. Here's the fuller version, based on how I've built login systems across a handful of production apps - a FastAPI marketplace, a PHP-based branch operations system, and a couple of school portals.

Hashing: bcrypt, and don't roll your own rounds logic

Passwords get hashed with bcrypt (or argon2 if the framework supports it easily) before they ever touch the database, full stop. The one detail people get wrong isn't the algorithm, it's forgetting to keep the cost factor configurable so it can be bumped up as hardware gets faster without a data migration.

Access tokens short, refresh tokens rotated

I issue a short-lived JWT access token (typically 15-60 minutes) for actual API calls, and a longer-lived refresh token used only to mint new access tokens. The refresh token is stored server-side or as an httpOnly cookie, never in localStorage where any injected script could read it. On every refresh, the old refresh token is invalidated and a new one issued - so a stolen refresh token is only useful once before the legitimate user's next request breaks the thief's session.

Role checks belong on the server, always

Hiding an admin button in the UI is a UX decision, not a security one. Every admin-only endpoint gets its own dependency that re-checks the user's role against the database on every request - never trusting a role claimed on the frontend or even inside an old token. This is the same require_admin pattern I reuse across every FastAPI project I build.

CSRF and rate limiting aren't optional extras

If auth uses cookies at all, CSRF protection has to come with it - a same-site cookie policy plus a CSRF token on state-changing requests. Separately, login and password-reset endpoints get rate-limited per IP regardless of anything else, since brute-forcing a login form is the single most common attack any public site sees. A few requests per minute is usually plenty for a real user and painfully slow for an attacker.

WebAuthn, when the stakes are higher

For systems where a compromised password is genuinely costly - I added this to a branch operations system handling daily production data - biometric auth via WebAuthn removes passwords from the equation entirely for day-to-day logins. It's more setup than JWT-and-bcrypt, but for the right system it closes an entire category of attack.

None of this is complicated in isolation. What makes auth "secure" in practice is doing all of it together and not skipping the boring parts - rotation, rate limits, and server-side role checks - because they don't feel as interesting as picking a hashing algorithm.