Every few years a team invents a new way to store sessions in localStorage, roll their own JWT crypto, or hash passwords with SHA-256 because “we don’t need bcrypt for an internal tool.” Then the internal tool is on the internet, or the JWT is signed with none, or XSS steals the tokens.

Authentication is a solved-enough problem that inventing it is a smell. You still need to understand the pieces so you can configure a library, not so you can write a novel in auth.ts.

This is the baseline Artikals readers should insist on: sessions or tokens, password storage, cookies, CSRF, OAuth as a client, and the mistakes that keep showing up in incident reports.

Passwords

Store a password hash with a slow KDF: Argon2, bcrypt, or scrypt. Not SHA-256, not MD5, not “encrypt” with a reversible key. A hash is one-way. Encryption is for data you must get back. You never need the user’s password back.

Salt is built into those KDFs. Do not invent a global salt. Do not disable the work factor to make tests fast without a test-only config.

Reset flows: expired, single-use tokens, sent over email. Do not reset to a temp password you email in the clear if you can avoid it. Do not reveal whether an email is registered if product can live with a generic message. Often product cannot. Be consistent with what signup already leaks.

Login rate limit. Lockouts that create a denial of service on a victim’s email are a tradeoff. Prefer rate limits per IP and per account that slow down instead of a hard lock without a recovery path.

Sessions vs JWTs

A server session: store a random id in an httpOnly, Secure, SameSite cookie. The server looks up the session. Revocation is deleting the row. This is still the simplest model for a first-party web app.

A JWT access token: the client sends it. The server verifies the signature. Revocation is harder. You need short expiry, a blocklist, or a version number on the user. If you do not have any of those, you cannot fire someone until the token expires.

Do not put JWTs in localStorage if you can use httpOnly cookies for a first-party site. XSS reads localStorage. httpOnly cookies are not readable to JavaScript. You then must handle CSRF.

If you have a mobile app and a SPA on another domain, tokens in memory plus a refresh cookie on a dedicated auth domain is a pattern libraries exist for. Do not hand-roll refresh rotation unless you have read the specs and the threat model.

Cookies, SameSite, and CSRF

SameSite=Lax cookies are not sent on most cross-site POSTs. That removes a class of CSRF. SameSite=None; Secure is needed for some cross-site cases and brings CSRF back. Then you need tokens or double-submit patterns.

A JSON API that only accepts Content-Type: application/json and does not accept CORS from random origins is harder to CSRF from a simple form. It is not a complete story. Know your cookie policy.

Secure means HTTPS only. Without it, cookies travel in the clear. HSTS helps. Localhost is special-cased in browsers.

OAuth and “Sign in with”

Use a maintained library. Redirect URIs must be exact. State parameter to prevent CSRF on the redirect. PKCE for public clients (SPAs, mobile). Do not log the authorization code. Do not put client secrets in the frontend. Public clients do not get secrets.

When Google is down, your login is down if it is the only method. Offer a backup for staff.

Authorization is not authentication

Knowing who the user is does not mean they can delete the org. Check authorization on every request with the id from the session, not from a body field userId the client sent. IDOR is still the bug: change the id in the URL and see another user’s invoice. Tests should try that.

Admin flags in a JWT payload are not proof unless you verified the signature and the issuer. Anyone can mint a payload. See every “decode JWT” tutorial that never verifies.

MFA, recovery, and session listing

MFA reduces password stuffing damage. Recovery codes must be shown once and stored hashed. Session list and “log out everywhere” are how you recover from a stolen cookie. Build them before you need them.

SMS MFA is better than nothing and weaker than TOTP or WebAuthn. Prefer WebAuthn/passkeys when you can. Do not make MFA so painful that users disable it.

Secrets in the repo

The auth “basics” include not committing .env. Rotate if you did. Git history still has it. Assume it leaked.

A review list

  • Passwords: Argon2/bcrypt/scrypt
  • No tokens in localStorage without a XSS story you accept
  • Cookies: Secure, httpOnly, SameSite chosen on purpose
  • CSRF handled for cookie sessions
  • Authorization on the server using session identity
  • OAuth via a library, PKCE for public clients
  • Short-lived access, revocable sessions
  • Rate limits on login and reset

If a blog post says “JWT is stateless so it’s simpler,” ask them how they revoke. If they say they do not, that is the product decision, not a law of physics.

You should not invent crypto. You should not invent session fixation fixes from memory. Use the framework’s auth, or a known provider (Auth0, Clerk, Cognito, WorkOS, or your language’s battle-tested session library). Configure it with the list above. ## Session fixation and logout

On login, issue a new session id. Do not reuse an anonymous id. On logout, delete the server session and expire the cookie. “Logout” that only clears localStorage while the cookie still works is a theater logout.

If you rotate refresh tokens, detect reuse of an old refresh token as theft and kill the family of sessions. Libraries that implement rotation have this. Homemade rotation often misses reuse detection.

Cookies on subdomains

Domain=.example.com shares cookies with all subdomains. That is convenient and wide. A buggy staging.example.com can receive or set cookies you did not intend if DNS is sloppy. Prefer host-only cookies unless you have a real SSO reason.

Password managers and autofill

Your login form should use autocomplete attributes so managers work. Disabling paste on password fields is hostile and causes weaker passwords. If a compliance person asks for it, push back with the OWASP guidance. Users will paste anyway via the manager.

If you expire sessions on password change, do it on the server for all sessions except the current one, or all including current and force re-login. Document which. Users who change password on phone and stay logged in on a stolen laptop need “log out everywhere.”

Email as an identity

If email is the username, normalize case on the way in. Ada@x.com and ada@x.com should not be two users unless you have a very good reason. Unique indexes on the normalized form prevent the race. Login should use the same normalization.

CSRF on JSON APIs, again

SameSite helps. Custom headers (X-Requested-With) help a bit because forms cannot set them easily. CORS must not reflect arbitrary origins with credentials. Test a malicious origin. If you get cookies, you misconfigured CORS.

Artikals will keep saying this because the industry still treats auth as a weekend project. It is not. It is the door. Doors are boring when they work.

Leave a Reply

Your email address will not be published. Required fields are marked *