Authentication and sessions
Authentication answers “who are you?”; authorisation answers “what may you do?”. This lesson covers the first, and the sessions that remember the answer between requests.
Storing passwords
Never store passwords in plain text or with reversible encryption, and never with a fast hash such as SHA-256 or MD5: attackers with a leaked database can try billions of guesses per second on fast hashes.
Use a slow, salted password hashing function: Argon2id (preferred), scrypt or bcrypt. A unique random salt per password means identical passwords produce different hashes, which defeats precomputed tables. Libraries handle the salt for you:
from argon2 import PasswordHasher
ph = PasswordHasher()
stored = ph.hash("correct horse battery staple") # save this string
ph.verify(stored, attempt) # raises on mismatch
Also:
- allow long passwords and passphrases, and check new ones against lists of breached passwords;
- rate-limit login attempts per account and per IP address;
- return the same message for “unknown email” and “wrong password”, so attackers can’t discover which accounts exist.
Sessions
After login, the server must recognise the user on later requests. Two common approaches:
Server-side sessions. The server creates a random session id (at least 128 bits from a secure random generator), stores it with the user id, and sends it in a cookie. Each request looks the id up. Logging out or revoking access is simply deleting the row. Store a hash of the id, so a leaked sessions table can’t be used to log in.
Self-contained tokens such as JWTs. The token carries signed claims (user id, expiry), and the server verifies the signature without a database lookup. This scales easily across services, but a stolen token stays valid until it expires, and revoking one requires a denylist, which brings back the lookup. Keep token lifetimes short and pair them with refresh tokens you can revoke.
For a typical web application, server-side sessions in cookies are simpler and safer.
Cookie flags
Set-Cookie: sid=q3Zs...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000
- HttpOnly: JavaScript can’t read the cookie, so an XSS bug can’t steal it.
- Secure: sent only over HTTPS.
- SameSite=Lax: not sent on most cross-site requests, which blocks common CSRF attacks.
Rotate the session id at login, to prevent session fixation, and on privilege changes. Expire idle sessions.
Multi-factor authentication
A second factor stops most account takeovers that start with a stolen password. In order of strength:
- Passkeys and security keys (WebAuthn): phishing-resistant, because the browser only uses the key on the real site.
- Authenticator app codes (TOTP): good, but a convincing phishing page can relay them.
- SMS codes: better than nothing, but vulnerable to SIM swapping.
Signing in with another provider
“Sign in with GitHub” or Google uses OAuth 2.0 with OpenID Connect. Your app redirects to the provider, the user approves, and the provider redirects back with a one-time code that your server exchanges for tokens. Always:
- send and verify a random
statevalue, to block login CSRF; - use PKCE for mobile and single-page apps;
- register exact redirect URLs, and never redirect users to an arbitrary
nextURL without checking it belongs to your site.
Delegating authentication means you store no passwords at all, which removes a whole class of risk.