Passwords and Authentication
Proving who you are: authentication factors, good password practice, how passwords are stored safely, multi-factor authentication and passkeys.
Authentication factors
Authentication is proving that you are who you claim to be. The claim itself — typing a username — is identification; deciding what you may do afterward is authorization (see What Is Information Security? and Access Control). An authentication factor is a category of proof:
| Factor | Examples | Typical weakness |
|---|---|---|
| Something you know (knowledge) | Password, passphrase, personal identification number (PIN), security question | Guessed, phished, reused, leaked in a breach |
| Something you have (possession) | Phone with an authenticator app, hardware security key, smart card, badge | Lost or stolen; codes can be phished |
| Something you are (inherence) | Fingerprint, face, iris, voice | Cannot be changed when compromised; false matches and false rejections |
| Somewhere you are (location) | Company network, country, Global Positioning System (GPS) position | Easy to spoof alone; used as an extra signal |
| Something you do (behavior) | Typing rhythm, how you hold the phone | Probabilistic; used as an extra signal |
Multi-factor authentication (MFA; see Remote Access for the basic definition) means factors of different kinds. A password plus a security question is still one factor — two things you know — and can both be phished in the same fake form. Location and behavior are usually not counted as full factors; they feed risk-based decisions, such as asking for an extra check when someone signs in from a new country.
Why passwords fail
Passwords are cheap and familiar, which is why they will be around for years. But they fail in predictable ways:
- Guessing: people choose patterns — a season and a year, a team name with an exclamation mark — that attackers try first. Password spraying tries a few such common passwords against many accounts, staying under lockout limits.
- Phishing: a convincing fake login page collects the password directly (see Social Engineering and Phishing).
- Breaches: a website is hacked and its password database is stolen. If the passwords were stored badly, they can be recovered offline at billions of guesses per second.
- Reuse: the same password on many sites turns one breach into many.
Reuse and breaches together enable credential stuffing: attackers take username and password pairs leaked from one service and try them automatically on other services. No guessing is involved, so it works against any password, however strong, that is reused. Defenses are unique passwords per site, MFA, checks against breached-password lists, and detection of many logins from the same sources.
Good password practice
The United States National Institute of Standards and Technology (NIST) publishes guidance on digital identity in Special Publication (SP) 800-63B. It overturned many older habits, and much of the world follows it. Its main points for passwords:
-
Length over complexity: require a minimum length
and allow long passwords (at least 64 characters), but do not
demand a mix of uppercase, digits and symbols. Such rules produce
Summer2025!, not security. The 2025 revision (SP 800-63B-4) asks for at least 15 characters when a password is the only factor, and at least 8 when it is combined with another factor. - No forced periodic changes: changing every 90 days makes people choose weaker passwords with a counter at the end. Force a change when there is evidence of compromise.
- Check against a blocklist: reject passwords that appear in lists of breached passwords, dictionary words and passwords based on the service or username.
- Allow paste and password managers, and allow all printable characters, including spaces.
- No password hints and no security questions ("your mother's maiden name"): the answers can often be found online.
- Limit failed attempts (rate limiting or lockout), so online guessing is slow.
Passphrases and entropy
The strength of a randomly generated secret is measured in bits of entropy: length × log2(size of the pool it is drawn from). Every extra bit doubles the number of guesses an attacker needs. A passphrase of random words — chosen by dice or a generator from a list such as the 7,776-word list of the Electronic Frontier Foundation (EFF) — is easy to type and remember, and each word adds almost 13 bits.
import math
def entropy_bits(pool_size, length):
"""Bits of entropy of a secret picked uniformly at random from the pool."""
return length * math.log2(pool_size)
def readable(seconds):
for unit, size in (("years", 365 * 24 * 3600), ("days", 24 * 3600), ("hours", 3600)):
if seconds >= size:
return f"{seconds / size:,.0f} {unit}"
return f"{seconds:,.0f} seconds"
candidates = [
("8 random characters (94 symbols)", 94, 8),
("12 random characters (94 symbols)", 94, 12),
("16 random lowercase letters", 26, 16),
("4 random words (7,776-word list)", 7776, 4),
("6 random words (7,776-word list)", 7776, 6),
]
GUESSES_PER_SECOND = 10**10 # an offline attack on a fast, unsalted hash
for label, pool, length in candidates:
bits = entropy_bits(pool, length)
average = 2 ** (bits - 1) / GUESSES_PER_SECOND # on average, half the space
print(f"{label:<35}{bits:5.1f} bits {readable(average)}")
8 random characters (94 symbols) 52.4 bits 4 days 12 random characters (94 symbols) 78.7 bits 754,567 years 16 random lowercase letters 75.2 bits 69,141 years 4 random words (7,776-word list) 51.7 bits 2 days 6 random words (7,776-word list) 77.5 bits 350,510 years
Two lessons. First, length wins: 16 lowercase letters beat 8
characters of every kind by far. Second, the formula only holds for
secrets chosen at random. A human-chosen password such as
Amsterdam2025! has 13 characters, but an attacker who
tries city names with years and a symbol finds it in a tiny fraction
of that time. The slow password hashing described below makes every
guess thousands of times more expensive, which is why the time per
guess in the example is a worst case.
Password managers
A password manager stores all of a person's passwords in an encrypted vault, unlocked with one strong master password (ideally plus MFA), and generates long, random, unique passwords for every site. It solves reuse, and it also helps against phishing: the browser extension fills in a password only on the real domain, so a lookalike site gets nothing — a useful warning sign in itself. Organizations should provide an approved manager rather than leave people to use notebooks and spreadsheets. The master password and the recovery method of the vault are now the crown jewels: protect them accordingly.
How systems store passwords
A system needs to check a password, not to know it. So it should never store passwords, and also not encrypt them: encryption can be reversed by anyone who obtains the key, and the key is usually on the same server. Instead, systems use password hashing: they store the output of a one-way function (see hash functions) that is deliberately slow and uses a unique salt per password. At login, the system hashes what was typed in the same way and compares the results.
A salt is a random value, unique for each stored password, that is combined with the password before hashing and stored next to the hash. It is not secret. It makes identical passwords produce different hashes, so an attacker cannot see which users share a password, and it makes precomputed tables of hashes (rainbow tables) useless — every password must be attacked separately.
Ordinary hash functions such as SHA-256 are designed to be fast, which is exactly wrong here: fast means billions of guesses per second on a graphics card. Password hashing uses slow key derivation functions with an adjustable cost:
- Argon2 (Argon2id): winner of the Password Hashing Competition (2015), memory-hard; the first choice in current guidance from the Open Worldwide Application Security Project (OWASP).
- scrypt: memory-hard, so attacks on graphics cards and special hardware are expensive.
- bcrypt: widely used since 1999, still acceptable; note that it only uses the first 72 bytes of a password.
- PBKDF2 (Password-Based Key Derivation Function 2): repeats a hash function many times; required where only Federal Information Processing Standards (FIPS) validated algorithms are allowed.
Python's standard library has PBKDF2 and scrypt (Argon2 and bcrypt need third-party packages). This example uses fixed salts so that its output is reproducible; real code generates a fresh random salt for every password.
import hashlib
password = b"correct horse battery staple"
# Fixed demo salts so the output is reproducible. In real code every user
# gets a fresh random salt: os.urandom(16) or secrets.token_bytes(16).
salt_ada = bytes.fromhex("8f1c2a7d4b6e9013a5c7e2f40d18b36c")
salt_bob = bytes.fromhex("2e9b51c0f7a34d86b1e0c95a73d26f18")
# A fast, unsalted hash: identical passwords give identical hashes.
print("SHA-256 ", hashlib.sha256(password).hexdigest()[:32])
# PBKDF2: a salted, deliberately slow key derivation function.
for name, salt in (("ada", salt_ada), ("bob", salt_bob)):
digest = hashlib.pbkdf2_hmac("sha256", password, salt, 600_000)
print(f"PBKDF2 {name} ", digest.hex()[:32])
# scrypt: slow and memory-hard (n = CPU/memory cost, r = block size).
# demo cost; OWASP recommends n=2**17 (needs maxmem raised)
digest = hashlib.scrypt(password, salt=salt_ada, n=2**14, r=8, p=1, dklen=32)
print("scrypt ada ", digest.hex()[:32])
SHA-256 c4bbcb1fbec99d65bf59d85c8cb62ee2 PBKDF2 ada e9cdf5c7d3d7310355c58d296bdee319 PBKDF2 bob f934f9c6733c906ca527d028a9d1da2b scrypt ada 1ec881e6eabb08a57429759a3a5530a9
Ada and Bob chose the same password, yet their stored hashes are unrelated, because their salts differ. The 600,000 iterations make each PBKDF2 hash take a noticeable fraction of a second — nothing for one login, a huge obstacle for billions of guesses. (Outputs are shortened to 32 hexadecimal characters for the page.)
Verifying a password
The stored record contains everything needed to repeat the
calculation: algorithm, cost, salt and hash. Verification compares
the result with hmac.compare_digest, which takes the
same time no matter where the first difference is, so the response
time does not leak how much of the hash was correct.
import hashlib
import hmac
ITERATIONS = 600_000
def hash_password(password, salt):
digest = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, ITERATIONS)
# Store algorithm, cost, salt and hash together, so you can raise the cost later.
return f"pbkdf2_sha256${ITERATIONS}${salt.hex()}${digest.hex()}"
def verify_password(password, stored):
_, iterations, salt_hex, hash_hex = stored.split("$")
digest = hashlib.pbkdf2_hmac(
"sha256", password.encode(), bytes.fromhex(salt_hex), int(iterations)
)
# Constant-time comparison: takes the same time wherever the first difference is.
return hmac.compare_digest(digest.hex(), hash_hex)
demo_salt = bytes.fromhex("8f1c2a7d4b6e9013a5c7e2f40d18b36c") # real code: os.urandom(16)
stored = hash_password("correct horse battery staple", demo_salt)
print(stored[:60] + "...")
print(verify_password("correct horse battery staple", stored))
print(verify_password("Correct horse battery staple", stored))
pbkdf2_sha256$600000$8f1c2a7d4b6e9013a5c7e2f40d18b36c$e9cdf5... True False
Multi-factor authentication
A second factor means a stolen password alone is no longer enough. But the methods differ a lot in what they stop:
| Method | How it works | Main weakness | Strength |
|---|---|---|---|
| Text message (Short Message Service, SMS) or voice code | A code sent to your phone number | SIM swapping (a criminal moves your number to their own subscriber identity module, or SIM), interception, phishing | Weakest, still better than nothing |
| Authenticator app (TOTP) | A six-digit code that changes every 30 seconds | A fake site can ask for the code and use it at once | Good |
| Push notification | Approve the sign-in in an app | MFA fatigue: repeated prompts until someone taps "approve" | Good, with number matching |
| Hardware security key, passkey (FIDO2) | The device proves itself with a key pair bound to the site | Loss of the key; needs a backup method | Strongest: phishing-resistant |
How authenticator codes work
A time-based one-time password (TOTP, defined in Request for Comments (RFC) 6238) is a short code computed from a secret shared between the app and the server (exchanged once, through the QR code at enrollment) and the current time. Both sides count 30-second steps since 1 January 1970, compute a hash-based message authentication code (HMAC; see HMAC) of that step number with the secret, and turn it into six digits. No network connection is needed; the clocks only have to agree roughly. This example uses the test secret from the RFC and fixed times; for times 59 and 1111111109 the codes match the last six digits of the RFC's test values:
import hashlib
import hmac
import struct
def totp(secret, unix_time, step=30, digits=6):
"""Time-based one-time password (RFC 6238, HMAC-SHA-1 as most apps use)."""
counter = unix_time // step # number of 30-second steps
mac = hmac.new(secret, struct.pack(">Q", counter), hashlib.sha1).digest()
offset = mac[-1] & 0x0F # "dynamic truncation"
code = struct.unpack(">I", mac[offset:offset + 4])[0] & 0x7FFFFFFF
return str(code % 10**digits).zfill(digits)
# The test secret from RFC 6238; a real secret comes from the QR code at enrollment.
secret = b"12345678901234567890"
for t in (31, 59, 61, 1111111109):
print(f"time {t:>10} step {t // 30:>8} code {totp(secret, t)}")
time 31 step 1 code 287082 time 59 step 1 code 287082 time 61 step 2 code 359152 time 1111111109 step 37037036 code 081804
Times 31 and 59 fall in the same 30-second step and give the same code. Note what TOTP does not do: it does not check which website asks for the code. A user on a convincing fake login page types the code there, and an attacker who relays it in real time (an adversary-in-the-middle phishing kit) gets in. That is the gap phishing-resistant methods close.
Push notifications and MFA fatigue
With push MFA, the user only taps "approve". Attackers who already have the password abuse this with MFA fatigue (also called push bombing): they trigger prompt after prompt, sometimes at night, sometimes combined with a phone call from "IT support", until the user approves one to make it stop. Countermeasures are number matching (the user types the number shown on the sign-in screen into the authenticator app; it stops blind approvals, but not a real-time relay phishing site, which shows the number too), showing the location and application in the prompt, limiting the number of prompts, and a clear rule: an unexpected prompt is denied and reported. See Social Engineering and Phishing.
Phishing-resistant MFA and passkeys
FIDO2 is a set of open standards from the FIDO Alliance and the World Wide Web Consortium (W3C): WebAuthn (Web Authentication) in the browser and the Client to Authenticator Protocol (CTAP) between the browser and an authenticator such as a hardware security key. Instead of a shared secret, it uses public-key cryptography (see asymmetric encryption).
A passkey is a FIDO2 credential that replaces the password: a key pair created for one specific website. The private key stays on the user's device or in their password manager, unlocked locally with a fingerprint, face or PIN; the website stores only the public key.
Three properties make passkeys and security keys strong:
- Origin binding: the browser includes the real domain in what is signed, and the authenticator only uses a key for the domain it was created for. A phishing site on a lookalike domain cannot get a valid response.
- Nothing reusable on the server: a breach of the website reveals only public keys, which are useless to an attacker.
- Two factors in one gesture: possession of the device plus the fingerprint or PIN that unlocks it (when the site requires user verification).
Passkeys come in two kinds. Synced passkeys are backed up through the platform account or password manager (for example to all of someone's Apple, Google or Microsoft devices), so a lost phone does not lock the user out. Device-bound passkeys live in one piece of hardware — a security key, or a device's Trusted Platform Module (TPM) or secure enclave — and are designed so they cannot be exported or copied, which high-security environments and administrators often require. Smart cards with certificates, used widely in government, are also phishing-resistant.
Account recovery: the side door
Every strong login has a recovery path for people who lose their phone or forget their password, and attackers know it. If a help desk resets a password and MFA for anyone who calls with a name and an employee number, the strongest passkey is irrelevant: the attacker simply phones the help desk. Several well-known breaches started exactly like this.
- Verify identity as strongly as practical — a callback to a number on file plus a manager's confirmation, or in-person ID; treat a video call alone as insufficient, since faces and voices can be faked. Never call back a number the caller provides.
- Treat a request to reset MFA, especially for an administrator, as high risk; notify the user through a second channel when it happens.
- Avoid security questions; give users backup methods (a second security key, recovery codes stored safely) instead.
- Help desk staff must be allowed — and expected — to say no and escalate when a caller pushes or rushes.
Single sign-on
In organizations, users rarely have a separate password for every application. With single sign-on (SSO), they authenticate once at a central identity provider, which vouches for them to the applications through standards such as Security Assertion Markup Language (SAML) and OpenID Connect (OIDC). This is explained in Identity and Access Management. For security it has a clear trade-off: fewer passwords, one place to enforce strong MFA, and one switch to disable a leaver everywhere — but also one account that unlocks everything, so that identity provider and its administrator accounts need the strongest protection you have.