Encryption hides messages. Signatures prove authorship. But before any of that, a system needs to answer a more basic question: who is this person? Authentication is the protocol that establishes identity, and it ties together nearly every primitive from the chapters before it.
Encryption and signatures assume you already know who you are talking to. Authentication is the step before that: proving you are who you claim to be. In person, faces and voices work. Over a network, you need a protocol.
Everything in this course so far has assumed the hard part is already solved. Alice encrypts a message to Bob. Bob verifies Alice's signature. But how does the server know that the person typing a password is actually Alice, and not someone who guessed it? How does a website know the browser session belongs to you after you close the tab and come back?
Authentication is the answer. It is the process of proving an identity claim, and it sits at the foundation of every secure system. Without authentication, encryption protects messages nobody can trust, and signatures prove authorship nobody can verify.
Every authentication method falls into one of three categories. The rest of this chapter explores how each one works, why each one fails on its own, and how combining them creates real security.
Passwords, PINs, security questions. A secret that only the legitimate user should know.
A phone, a hardware security key, a smart card. A physical device that generates or stores credentials.
Fingerprints, face scans, iris patterns. Biometrics that are unique to your body.
Digital signatures from Chapter 10 prove that a message came from the holder of a private key. Authentication is a broader question: how does a system verify that you are authorized to access it? The answer has evolved from simple passwords to cryptographic challenge-response protocols.
A password is the simplest authentication protocol: a shared secret between you and the server. You type it, the server checks it. This has been the default since the 1960s, and it is still the most common method today.
The first computer password system appeared in 1961 at MIT's Compatible Time-Sharing System (CTSS). Multiple users shared one computer, and each needed private files. The solution was simple: each user picks a secret string, the system stores it, and you prove your identity by typing it back.
Sixty years later, passwords remain the dominant form of authentication. Not because they are good, but because they are easy to understand, cheap to implement, and require nothing beyond a keyboard.
Humans are bad at randomness (Chapter 4). A truly random 8-character password drawn from lowercase, uppercase, and digits has about 48 bits of entropy:
But real human passwords are not random. People use dictionary words, birthdates, and predictable patterns like “Password1!”. Studies of leaked password databases show that most human-chosen passwords have closer to 20-30 bits of actual entropy. That is roughly a million to a billion guesses, well within reach of a modern GPU.
Worse, people reuse passwords across services. A breach at one site gives attackers credentials for dozens of others. This is called credential stuffing, and it is one of the most common attack vectors on the internet.
The fundamental problem with passwords is not technical, it is human. People reuse them, pick predictable ones, and write them on sticky notes. Every other authentication method in this chapter exists because passwords alone are not enough.
If a server stores passwords in plaintext and gets breached, every user is compromised instantly. The solution: store a hash of the password, not the password itself. But plain hashing is not enough.
In 2009, RockYou stored 32 million passwords in plaintext. When attackers breached the database, every password was immediately readable. In 2013, Adobe lost 153 million encrypted (not hashed) passwords using the same encryption key for all of them, which is effectively the same as plaintext.
The solution comes from Chapter 2: hash the password before storing it. When a user logs in, hash the submitted password and compare it to the stored hash. The server never needs to know the actual password.
SHA-256 is fast. Too fast. An attacker with a modern GPU can compute billions of SHA-256 hashes per second. A 30-bit-entropy password (about a billion possibilities) falls in under a second.
Rainbow tables make it even worse. These are precomputed lookup tables mapping common passwords to their hashes. If two users have the same password, they have the same hash. An attacker who cracks one cracks both.
A salt is a random value stored alongside the hash. Instead of , you compute . Same password, different salt, completely different hash. Rainbow tables become useless because each user's hash is unique even if their passwords are identical.
The salt does not need to be secret. It is stored in the database next to the hash. Its purpose is to force the attacker to brute-force each user individually, instead of attacking all users at once with a precomputed table.
Salting solves the rainbow table problem, but a fast hash function still lets an attacker make billions of guesses per second. Key stretching deliberately slows down the hashing process. PBKDF2 runs the hash function hundreds of thousands of times in a chain. bcrypt uses the Blowfish cipher with an expensive key schedule. Argon2 goes further by requiring large amounts of memory, which defeats GPU parallelism.
With 600,000 iterations, a single PBKDF2 computation takes roughly half a second. An attacker trying to brute-force a 48-bit keyspace now faces:
Key stretching does not make passwords unbreakable, but it shifts the economics decisively. Cracking one password takes serious hardware and time. Cracking millions becomes impractical.
Type a password and see how PBKDF2 derives a key. Adjust the iteration count to feel the computational cost, and toggle comparison mode to see how different salts produce completely different hashes from the same password.
Modern best practice is Argon2id with at least 64 MB of memory and 3 iterations. PBKDF2 with 600,000+ iterations is the NIST recommendation when Argon2 is not available. Never use plain SHA-256 or MD5 for passwords.
You prove your identity once with a password. But HTTP is stateless: every request is independent. Without sessions, you would need to send your password with every click. The solution: after verifying the password, the server issues a token.
After a successful login, the server generates a long random string (from Chapter 4: randomness), stores it in a database alongside the user ID, and sends it to the browser as a cookie. Every subsequent request includes the cookie automatically. The server looks up the token to find the associated user.
Session tokens are simple and effective. The server has complete control: it can revoke a session instantly by deleting the token from the database. The tradeoff is that every request requires a database lookup, and the session store becomes a scaling bottleneck.
JSON Web Tokens take a different approach. Instead of storing session data on the server, they encode it directly in the token. A JWT has three base64url-encoded parts separated by dots: header, payload, and signature.
The header declares the algorithm (e.g., HS256). The payload contains claims: the user ID, expiration time, and any other data the server needs. The signature is an HMAC or digital signature over the header and payload, proving the token was issued by the server and has not been tampered with.
No database lookup needed. The server verifies the signature, checks the expiration, and trusts the claims. The tradeoff: once issued, a JWT cannot be revoked before it expires (without maintaining a blocklist, which reintroduces server-side state).
A JWT is three base64url-encoded parts separated by dots: header, payload, and signature. Edit the payload and watch the token update. Toggle tamper mode to see what happens when the payload changes but the signature does not.
{
"alg": "HS256",
"typ": "JWT"
}{
"sub": "1234567890",
"name": "Alice",
"iat": 1774742160,
"exp": 1774745760
}...
Server stores state in a database. Each request triggers a lookup.
Easy to revoke. Delete the token from the database and the session ends immediately.
Scaling bottleneck. The session store must handle every authenticated request.
Stateless. All session data lives in the token. No server-side storage.
Hard to revoke. Valid until expiry unless you maintain a blocklist.
Scales easily. Any server can verify the token without shared state.
JWTs are not encrypted by default. The payload is base64url-encoded, not hidden. Anyone who intercepts a JWT can read its contents. JWTs provide integrity (tamper detection), not confidentiality. If you need to hide the payload, use JWE (JSON Web Encryption).
If your password is stolen through phishing, a data breach, or shoulder surfing, the attacker has everything they need. Multi-factor authentication requires proof from at least two different categories.
A password is a single point of failure. If it leaks, the attacker gets full access. Multi-factor authentication (MFA) adds a second barrier. Even if your password is compromised, the attacker also needs your phone or hardware key. Compromising two independent factors is exponentially harder than compromising one.
The key word is independent. Two passwords are not two-factor authentication. A password and a PIN stored on the same device is barely better than one factor. The factors must come from different categories: something you know, something you have, something you are.
The most common second factor is a 6-digit code from an authenticator app. The protocol behind it is TOTP (RFC 6238), and it relies on cryptographic primitives you already know.
During setup, the server generates a random secret key and shares it with your authenticator app (usually via QR code). Both sides now hold the same secret. Every 30 seconds, both sides compute:
The HMAC from Chapter 3 provides authentication. The time step ensures each code is valid for only 30 seconds. The truncation extracts a human-readable 6-digit number from the 20-byte HMAC output. Because both sides share the same secret and the same clock, they independently generate the same code.
Watch a time-based one-time password generate in real time. The same secret and the same 30-second window always produce the same 6-digit code. Below, you can see every step of the computation.
TOTP codes are vulnerable to phishing. If you type the code into a fake login page, the attacker can use it immediately (it is valid for 30 seconds). TOTP also requires a shared secret, which can be stolen from the server. This is why FIDO2 and passkeys are the modern replacement, as we will see next.
Passkeys replace passwords with public-key cryptography. Your device generates a key pair. The private key never leaves your device. Authentication is a challenge-response signed with your private key. No shared secret ever crosses the network.
Passkeys (FIDO2/WebAuthn) flip the authentication model entirely. Instead of proving you know a secret the server also knows, you prove you control a private key that the server has never seen. The server stores only your public key, exactly like the digital signature schemes from Chapter 10.
No password database to breach. No shared secret to phish. The credential is bound to the website's domain, so even a pixel-perfect phishing page cannot extract a valid response. The private key is protected on your device by biometrics or a local PIN.
The WebAuthn protocol has two phases: registration (creating the credential) and authentication (proving you hold it).
Server sends a random challenge to the device.
Device generates a new key pair (e.g., ECDSA P-256). The private key is stored in secure hardware on the device.
Device signs the challenge and sends the public key + signature back to the server.
Server verifies the signature and stores the public key. No secret is ever shared.
Server sends a fresh random challenge.
Device signs the challenge with the stored private key.
Server verifies the signature with the stored public key. If valid, the user is authenticated.
Step through the WebAuthn authentication flow. The device generates a key pair, the server sends a challenge, and the device proves its identity by signing the challenge with its private key. No password is ever transmitted.
Passkeys are supported in all major browsers and operating systems. Apple, Google, and Microsoft have all committed to passkey support through the FIDO Alliance. For new systems, passkeys are the recommended primary authentication method.
Instead of managing passwords yourself, let a trusted provider handle authentication. OAuth 2.0 and OpenID Connect let users prove their identity through a provider they already trust.
Building a secure authentication system is hard. You need to handle password hashing, session management, account recovery, MFA, brute force protection, and more. Every piece of this is a potential vulnerability. For most applications, delegating this responsibility to a dedicated identity provider (Google, GitHub, Apple) is both safer and easier.
The user clicks “Login with Google.” Google authenticates the user (using whatever method it supports: passwords, passkeys, security keys). Your application receives a signed token proving the user's identity. You never see or store the user's Google password.
User clicks “Login with Google.” Your app redirects the browser to Google's authorization endpoint with your app's client ID and a redirect URI.
The user authenticates directly with Google (password, passkey, or security key). Your application is not involved in this step.
Google redirects back to your app with a short-lived authorization code in the URL.
Your server sends the authorization code to Google (server-to-server, not through the browser) and receives an access token and an ID token (a signed JWT).
Your server verifies the JWT signature (Chapter 10) and reads the user's identity claims: email, name, and a unique subject identifier. You create a local session for the user.
OAuth 2.0 by itself is an authorization protocol: it tells you what a user can access, not who the user is. OpenID Connect (OIDC) adds an identity layer on top. The ID token, a JWT containing identity claims, is the OIDC addition.
When people say “Login with Google,” they mean OIDC. The distinction matters when building systems: OAuth alone gives you a token to call APIs on the user's behalf. OIDC gives you a cryptographically signed proof of who the user is.
OAuth 2.0 alone is an authorization protocol, not authentication. OpenID Connect (OIDC) adds the identity layer. The distinction matters: OAuth tells you what a user can access, OIDC tells you who the user is.
Authentication is where cryptographic theory meets real-world security. These guidelines reflect how modern systems handle identity safely.
Passkeys eliminate passwords, resist phishing, and remove the risk of credential database breaches. Support them as the primary authentication method for new systems.
If you must store passwords, use Argon2id with recommended parameters (64 MB memory, 3 iterations, 1 parallelism). PBKDF2 with 600,000+ iterations is the fallback when Argon2 is unavailable.
Even if the primary login uses passwords, require a second factor for account recovery, password changes, and high-value actions. TOTP is the minimum. FIDO2 hardware keys are the gold standard.
Check the signature, expiry (exp), issuer (iss), and audience (aud). Never trust the alg header blindly. The "alg:none" attack has compromised real systems by tricking servers into skipping verification.
Authentication is the gateway to every secure system. The primitives from earlier chapters (hashing, HMAC, digital signatures, asymmetric cryptography) all converge here. Getting it right means combining the right primitives in the right order, and never cutting corners on password storage or token validation.
This chapter traced the evolution of user authentication: from shared secrets to cryptographic challenge-response. Each method builds on primitives from earlier chapters, and each one exists because the previous generation was not enough.
Passwords are the oldest form of authentication but are fundamentally limited by human behavior. People choose weak passwords, reuse them, and fall for phishing.
Password storage requires salting and key stretching. PBKDF2, bcrypt, and Argon2 make brute-force attacks computationally expensive. Never store passwords as plain hashes.
Tokens and sessions solve the statefulness problem. Session tokens are simple and revocable. JWTs encode claims in a signed, portable format that requires no server-side storage.
Multi-factor authentication adds a second barrier. TOTP uses HMAC and a shared time step to generate 6-digit codes. It is better than passwords alone, but still vulnerable to phishing.
Passkeys replace shared secrets entirely with public-key challenge-response. Phishing-resistant, breach-proof, and backed by all major platforms through the FIDO Alliance.
OAuth and OpenID Connect delegate authentication to trusted providers, reducing the burden on individual applications and giving users consistent, well-secured login experiences.
Authentication is where all the primitives come together. Hashing secures passwords. HMAC powers TOTP. Digital signatures enable passkeys. Randomness generates session tokens and challenges. Next, we will see how these same cryptographic building blocks power cryptocurrency and blockchain systems.