Web Authentication in Practice: Passwords, JWT, and Secure Cookies
September 23, 2026 · 7 min read

Authentication is the security layer where most breaches start — not because it's intellectually difficult, but because it involves a long chain of small decisions, each one individually defensible, and together either solid or subtly broken. This guide covers the complete flow: from how passwords are stored to why the specific attributes on a cookie matter more than you'd expect.
1. How passwords should be stored
The first rule: never store a password. Store only its hash. Encryption is reversible if you have the key — a database of encrypted passwords is catastrophic to leak. A hash cannot be reversed: you verify by hashing the input and comparing it to the stored value, but you can never go the other way.
Use Argon2id (the current OWASP recommendation) or bcrypt — never MD5 or SHA-256. General-purpose hash functions are designed to be fast. Password hashing functions are designed to be slow. That intentional 200ms delay on your server translates to thousands of years if an attacker tries to brute-force a leaked database. Pair it with a salt — a random value unique to each user — so two accounts with identical passwords produce completely different hashes. This makes precomputed rainbow table attacks useless.
2. After login: sessions vs. JWT
Once credentials are verified, the server needs a way to recognize the user on every subsequent request. Two approaches dominate: sessions, where the server stores state, and JWT, where the token carries the state.
With sessions, the server saves a record — in memory, Redis, or a database — and sends the client only a session ID via a cookie. Revocation is immediate: delete the record, the user is logged out. The tradeoff is that all your servers need access to the same session store, which complicates horizontal scaling.
With JWT, the server signs a self-contained token and ships it to the client. Future requests send the token back, and the server validates the signature locally — no database lookup. This scales effortlessly across multiple instances. The tradeoff is revocation: once issued, a JWT is valid until it expires, with no central record to delete.
3. JWT in practice
A JWT has three parts separated by dots: a header (algorithm and token type), a payload (claims: user ID, roles, expiration), and a signature. The header and payload are base64url-encoded — not encrypted. Anyone with the token can read them. Never put sensitive data in the payload.
The signature is what the server trusts. Using HS256, the same secret both signs and verifies. Using RS256, a private key signs and the public key verifies — useful when multiple services need to validate tokens without sharing secrets. Either way, verification is local and requires no external lookup.
JWT's structural weakness is revocation. Once issued, a token is valid until it expires. The standard solution: issue short-lived JWTs (15 minutes is common) paired with a long-lived refresh token stored in your database. The JWT handles authentication request by request; when it expires, the client presents the refresh token to get a new pair. The critical rule: rotate the refresh token on every use. If the same refresh token is ever presented twice, treat it as a breach — revoke all sessions for that user immediately.
The complete JWT flow: login, protected requests, and token refresh with rotation.
4. Where to store the token
The debate between localStorage and cookies resolves at one question: can JavaScript read it? localStorage is fully accessible to any script running on your page — your code, but also a compromised CDN asset, a third-party analytics library, or an XSS payload. A single injection point anywhere in your stack exposes the token.
An HttpOnly cookie is invisible to JavaScript by design. The browser attaches it automatically to every matching request, but no script can access or exfiltrate it. Add the right attributes and you close the two most common attack vectors:
Each attribute closes a specific attack vector. All four together form a solid baseline.
5. Additional security layers
These fundamentals get you most of the way there. A few more layers complete the picture:
- MFA — a second factor (TOTP, hardware key, or passkey) ensures a leaked password alone is not enough to get in.
- Rate limiting — cap login attempts to 5–10 per minute to make brute force impractical.
- Generic error messages — "Invalid credentials" instead of "User not found" denies attackers information about which emails exist.
- OAuth2/OIDC — delegate authentication entirely to a trusted provider; your app never handles the user's password at all.
“A login form takes an afternoon to build. The security around it takes considerably longer to think through — and one overlooked detail is enough to expose everyone who trusted you.”
Also available in
Leer en español