Lesson 2 of 7 · 30 min

Secure Authentication and Session Management Done Right

Store passwords safely, protect login flows and manage sessions and cookies correctly.

In this lesson1. Consider not building it yourself2. Storing passwords safely3. Protecting the login flow4. Sessions and cookies

Consider not building it yourself

Authentication is one of the easiest places to make serious mistakes, so the first question is whether you should build it at all. Mature identity providers and well-maintained framework libraries handle password storage, multi-factor authentication, account recovery and session management, and they receive security fixes from teams who focus on this full time. For many small teams, using an established provider or your framework's built-in authentication is the safest choice.

If you do build or customise login, follow the OWASP Authentication, Password Storage and Session Management cheat sheets. They are free, practical and regularly updated. The rest of this lesson explains the core ideas so you can review any implementation, including one from a provider, and understand what good looks like.

Storing passwords safely

Never store passwords in plain text, and never use fast general-purpose hashes such as MD5 or SHA-256 on their own. Use a password hashing function designed to be slow and memory-hard, such as Argon2id, scrypt or bcrypt, through a maintained library. These functions add a unique salt to each password automatically, so identical passwords produce different hashes. The example below is illustrative; check the library documentation and the OWASP cheat sheet for current recommended settings.

python · illustrative
# Illustrative Python using the argon2-cffi package.
# See the OWASP Password Storage Cheat Sheet for current guidance.
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher()  # library defaults are a reasonable start

def create_user(email: str, password: str) -> dict:
    return {"email": email, "password_hash": ph.hash(password)}

def check_login(user: dict, password: str) -> bool:
    try:
        ph.verify(user["password_hash"], password)
    except VerifyMismatchError:
        return False
    if ph.check_needs_rehash(user["password_hash"]):
        user["password_hash"] = ph.hash(password)  # then save it
    return True

Protecting the login flow

Attackers try leaked passwords from other sites against your login page, a technique called credential stuffing. Limit repeated failed attempts per account and per IP address, and add increasing delays or a challenge after several failures. Show the same generic message for a wrong email and a wrong password, so attackers cannot discover which accounts exist. Apply the same care to the password reset and sign-up flows, which are often forgotten.

Offer multi-factor authentication, and require it for administrators and staff with access to sensitive data. Authenticator apps and passkeys are stronger than SMS codes, although SMS is still better than nothing for many consumer apps in India and the Gulf where it is familiar to users. Encourage long passphrases rather than complex short passwords, and check new passwords against lists of known breached passwords where your library supports it.

  • Rate limit login, reset and sign-up endpoints
  • Use generic error messages for failed logins
  • Require multi-factor authentication for admin accounts
  • Make reset tokens single-use, random and short-lived

Sessions and cookies

After login, the server issues a session identifier, usually in a cookie. It must be long, random and generated by a secure library, never built from the user ID or a timestamp. Create a new session identifier at login to prevent session fixation, and destroy the session on the server when the user logs out. Set an idle timeout and an absolute lifetime, shorter for sensitive applications such as banking or health records.

Configure the cookie attributes carefully, as in the example below. Secure ensures the cookie only travels over HTTPS, HttpOnly stops JavaScript from reading it, which limits damage from cross-site scripting, and SameSite reduces cross-site request forgery risk. If you use tokens such as JWTs instead of server sessions, keep them short-lived, validate the signature and expiry on every request and plan how to revoke them. At XDQ Labs, reviewers check these settings on every new service.

text · illustrative
Illustrative session cookie (check your framework's docs):

Set-Cookie: session=<random-id>; Path=/; Secure; HttpOnly; SameSite=Lax

Secure    only sent over HTTPS
HttpOnly  not readable from JavaScript
SameSite  limits sending on cross-site requests
Key takeaways
  • Prefer an established identity provider or framework authentication
  • Hash passwords with Argon2id, scrypt or bcrypt through a maintained library
  • Rate limit login, reset and sign-up, and use generic error messages
  • Require multi-factor authentication for privileged accounts
  • Issue a fresh random session at login and set Secure, HttpOnly and SameSite
Try it

Review your application's login flow against this lesson. Check how passwords are hashed, whether login and reset endpoints are rate limited, and which attributes your session cookie has, using the browser developer tools. Write down every gap and fix the highest-risk one first.

Check your understanding

1. Which approach is suitable for storing passwords?

2. What does the HttpOnly cookie attribute do?

3. Why should the login error message be the same for a wrong email and a wrong password?

← PreviousThe OWASP Top 10 Explained for BeginnersNext lesson →Input Validation and Output Encoding: Preventing Injection and XSS
XDQ xAgents · AI lab for your company
While you sleep, your AI agents keep working.

From USD 49 or INR 2,999 a month. A person approves every step.

Start free, 14 days →See every agent300 credits · no card needed · prices include taxes🚀 Startup? Free landing page for 30 days →Need custom software or a security review? Send a brief →