Skip to content

How to Build a Secure Login System for a Web App

Web Development6 min read

Build secure login: sessions vs tokens, password hashing, MFA, cookie settings, rate limiting and common mistakes, with checklist.

In this article
  1. Decide: build, buy or use a library
  2. Sessions vs tokens
  3. Handling passwords correctly
  4. Setting the session cookie safely
  5. Multi-factor authentication
  6. Defences around the login form
  7. Common mistakes
  8. A launch checklist
  9. Building it properly

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.

FactorServer-side sessionSigned token (JWT)
Revoking access immediatelyEasyHarder; needs short expiry or deny list
Scaling across serversNeeds a shared session storeNo lookup needed to verify
Browser web appExcellent defaultWorks, but storage choice matters
Mobile or third-party API clientsLess naturalNatural fit
Typical mistakePredictable IDs or missing cookie flagsLong-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
  }
}

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:

  1. Passkeys and security keys (WebAuthn/FIDO2). Resistant to phishing because they are bound to your domain.
  2. Authenticator apps (time-based one-time codes). Good protection and widely supported.
  3. 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 none algorithm 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

  1. Passwords hashed with Argon2id, scrypt or bcrypt through a maintained library.
  2. Breached-password check and sensible length limits in place.
  3. Session cookies set with httpOnly, secure and sameSite; ID regenerated on login.
  4. MFA available, and required for privileged roles.
  5. Rate limiting on login, reset and MFA endpoints.
  6. Generic error messages and safe reset flow.
  7. Server-side authorisation checks on every protected route.
  8. Audit logging and alerting configured.
  9. 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.

Frequently asked questions

Should I use sessions or JWTs?

For a traditional browser web app, server-side sessions in a secure httpOnly cookie are usually simpler and easier to revoke. JWTs suit APIs and multiple services, but need short lifetimes and a revocation plan.

How should passwords be stored?

Only as hashes created with a slow, purpose-built algorithm such as Argon2id, scrypt or bcrypt, using a maintained library that applies unique salts. Never store plain text or use fast hashes like SHA-256 alone.

Is SMS a good second factor?

It is better than none, but weaker than authenticator apps or passkeys because codes can be intercepted or socially engineered. Use SMS as a fallback rather than the preferred option.

Should I build authentication myself?

Rarely. A managed identity provider or a well-maintained library reduces risk. You still need to configure it correctly and handle authorisation in your own code.

Keep reading

Let’s build it

Ready to build with CloudWiss?

Tell us what you want to build, move to the cloud or keep running. We reply with a clear next step.