Build secure login: sessions vs tokens, password hashing, MFA, cookie settings, rate limiting and common mistakes, with checklist.
In this article
To build a secure login system, use a proven library or identity provider rather than writing your own, store passwords only as salted hashes made with a slow algorithm such as Argon2id, issue sessions through secure cookies, require multi-factor authentication (MFA) for sensitive accounts, and put rate limiting on every authentication endpoint. Most breaches of login systems come from small implementation gaps rather than broken cryptography, so the details below matter.
This article is general engineering guidance. Standards evolve, so check the current OWASP Authentication and Session Management cheat sheets and, where relevant, NIST's digital identity guidance. If you handle personal data of people in the EU, discuss your obligations under GDPR with a qualified adviser; this is not legal advice.
Decide: build, buy or use a library
Authentication is a solved problem with unforgiving edge cases. You have three realistic options:
- Managed identity provider (for example Auth0, Microsoft Entra ID, Amazon Cognito, or Firebase Authentication). It handles storage, MFA, password reset and social login. You trade some control and ongoing fees for less risk.
- Open-source library or framework feature (such as Auth.js, Passport, or built-in identity in .NET and Spring). You host it yourself and are responsible for correct configuration.
- Custom build. Rarely justified. Only do this with an experienced team and a security review.
Whichever you choose, the concepts below still apply because you must configure and integrate it correctly.
Sessions vs tokens
This is the most common design question. Both prove "this request comes from a logged-in user"; they differ in where the state lives.
Server-side sessions
After login, the server creates a random session identifier, stores the session data (in a database or Redis), and sends the identifier to the browser in a cookie. On each request the server looks up the session. Revoking access is simple: delete the session.
Tokens (such as JWTs)
A signed token contains the user's claims, and the server verifies the signature instead of looking anything up. This suits distributed systems and APIs consumed by several clients. The drawback is revocation: a valid token stays valid until it expires, unless you add a deny list or keep lifetimes short and use refresh tokens.
| Factor | Server-side session | Signed token (JWT) |
|---|---|---|
| Revoking access immediately | Easy | Harder; needs short expiry or deny list |
| Scaling across servers | Needs a shared session store | No lookup needed to verify |
| Browser web app | Excellent default | Works, but storage choice matters |
| Mobile or third-party API clients | Less natural | Natural fit |
| Typical mistake | Predictable IDs or missing cookie flags | Long-lived tokens kept in localStorage |
For a traditional browser-based web application, server-side sessions in a hardened cookie are usually the simpler and safer choice. Tokens earn their place when several independent services or native apps need to verify identity.
Handling passwords correctly
- Never store plain text or reversible encryption. Store only a hash.
- Use a purpose-built password hashing algorithm. OWASP currently recommends Argon2id, with scrypt or bcrypt as acceptable alternatives. Do not use plain SHA-256 or MD5 for passwords; they are designed to be fast, which helps attackers.
- Let the library handle salts. Each hash should include a unique random salt, which modern libraries do automatically.
- Allow long passphrases and all characters, including spaces. Set a sensible maximum length to prevent abuse, not a short one.
- Prefer length over composition rules. Forcing symbols and digits tends to produce predictable patterns. Instead, check candidates against lists of known breached passwords.
- Do not force periodic changes without evidence of compromise; current guidance advises against it.
- Allow password managers and pasting into fields.
Here is a minimal Node.js example using the argon2 package. Tune cost parameters to your hardware by following the library and OWASP guidance:
import argon2 from "argon2";
export async function hashPassword(plain) {
return argon2.hash(plain, { type: argon2.argon2id });
}
export async function verifyPassword(hash, plain) {
try {
return await argon2.verify(hash, plain);
} catch {
return false; // malformed hash: treat as failure
}
}
Setting the session cookie safely
If you use a session cookie, set its attributes deliberately:
res.cookie("sid", sessionId, {
httpOnly: true, // not readable by JavaScript, limits XSS damage
secure: true, // sent only over HTTPS
sameSite: "lax", // limits cross-site request forgery
maxAge: 1000 * 60 * 60 * 8, // 8 hours, choose to suit your risk
path: "/",
});
Generate the session identifier with a cryptographically secure random source, create a new identifier on login to prevent session fixation, and destroy the session on the server at logout. Add CSRF protection for state-changing requests, either with tokens or by relying on strict SameSite settings plus origin checks.
Multi-factor authentication
MFA adds a second proof beyond the password. Options, from strongest to weakest against phishing:
- Passkeys and security keys (WebAuthn/FIDO2). Resistant to phishing because they are bound to your domain.
- Authenticator apps (time-based one-time codes). Good protection and widely supported.
- SMS or email codes. Better than nothing, but vulnerable to interception and social engineering. Use as a fallback, not a first choice.
Require MFA for administrators and for any account that can move money or access sensitive data. Provide recovery codes and a careful recovery process, because account recovery is a frequent attack path. WCAG 2.2 also adds an "Accessible Authentication" criterion, so avoid login steps that depend on memory puzzles or transcribing codes without allowing paste or autofill; see our accessibility guide.
Defences around the login form
- Rate limiting and lockout. Slow down repeated failures per account and per IP address. Prefer progressive delays over permanent lockout, which attackers can use to block real users.
- Generic error messages. Say "email or password is incorrect" so attackers cannot discover which emails are registered. Apply the same care to password reset messages.
- Secure reset flow. Use single-use, short-lived random tokens sent to the verified address, store only a hash of the token, and invalidate other sessions after a reset.
- HTTPS everywhere, with HSTS enabled.
- Logging and alerting. Record logins, failures, resets and MFA changes, and notify users of new-device sign-ins.
- Authorisation checks on the server. Logging in proves who someone is, not what they may do. Check permissions on every request.
Common mistakes
- Rolling your own cryptography or token scheme. Use vetted libraries.
- Storing JWTs in
localStorage. Any cross-site scripting flaw can then steal them. Prefer httpOnly cookies for browser apps. - Accepting the
nonealgorithm or not verifying token signatures, issuer and audience. Always validate every claim you rely on. - Weak reset flows. Predictable tokens, no expiry, or security questions with guessable answers undo everything else.
- Revealing whether an account exists through different messages or response times.
- Trusting the client. Hiding a button is not access control.
- Skipping dependency updates. Authentication libraries receive security patches; subscribe to advisories.
A launch checklist
- Passwords hashed with Argon2id, scrypt or bcrypt through a maintained library.
- Breached-password check and sensible length limits in place.
- Session cookies set with httpOnly, secure and sameSite; ID regenerated on login.
- MFA available, and required for privileged roles.
- Rate limiting on login, reset and MFA endpoints.
- Generic error messages and safe reset flow.
- Server-side authorisation checks on every protected route.
- Audit logging and alerting configured.
- Independent security review or penetration test before launch for anything handling sensitive data.
Building it properly
Login is the front door of your product, and it deserves specialist attention from the first design discussion rather than a late review. Our team builds authentication into custom applications and hardens existing ones as part of web application development, including security hardening as a standard deliverable. If you want an experienced second pair of eyes on your design, get in touch.
Share this article
General educational information, not legal or professional advice for your specific situation.
