Authentication, Passwords, MFA, and Identity
Do not use a user ID supplied in a request as proof of identity.
6.1 Authentication versus authorization
Authentication answers: Who or what is making this request?
Authorization answers: May this principal perform this action on this resource now?
Identification names a claimed identity.
Federation allows an identity provider to communicate authentication/identity assertions to another service.
Do not use a user ID supplied in a request as proof of identity. Obtain the authenticated principal from a verified session or token.
6.2 Password storage
Never store plaintext passwords or reversible encrypted passwords. Store a password hash created by a purpose-built password-hashing function with a unique salt and an appropriate work factor. Prefer Argon2id where supported; scrypt, bcrypt, or PBKDF2 may be appropriate under platform or compliance constraints. Follow current library and organizational guidance rather than inventing parameters.
Conceptual Python example using a maintained library's high-level API:
_Not run — the argon2-cffi package is not installed on the build machine. A runnable standard-library alternative (scrypt) follows at the end of this section._
A production implementation must also handle malformed hashes, library-specific exceptions, rehashing when parameters change, rate limits, and account-recovery policy. Do not log either the password or its hash. Library APIs and exception behavior can vary by version.
A runnable standard-library example: scrypt argon2-cffi (the library above) was not installed on the build machine, so that snippet was not run. Python's standard library includes scrypt, one of the acceptable alternatives the text lists; this example shows the same structure — unique salt, deliberately costly function, stored parameters, constant-time comparison:
Output (CPython 3.13.1): Because each hash has its own random salt, two users with the same password get different stored values. The parameters are stored with the hash so they can be raised later and old hashes re-hashed at the next successful login. Choose the parameters from current guidance for your platform rather than copying these.
Try it: salts, cost and policy
# Illustrative API: install and consult the chosen library's current documentation.
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
hasher = PasswordHasher() # Uses library defaults; review current guidance.
def hash_password(password: str) -> str:
return hasher.hash(password)
def verify_password(stored_hash: str, password: str) -> bool:
try:
return hasher.verify(stored_hash, password)
except VerifyMismatchError:
return Falseimport hashlib
import hmac
import secrets
N, R, P = 2**14, 8, 1 # cost parameters, stored with the hash
def hash_password(password: str) -> str:
salt = secrets.token_bytes(16)
digest = hashlib.scrypt(password.encode(), salt=salt, n=N, r=R, p=P, dklen=32)
return f"scrypt${N}${R}${P}${salt.hex()}${digest.hex()}"
def verify_password(stored: str, password: str) -> bool:
_, n, r, p, salt_hex, digest_hex = stored.split("$")
candidate = hashlib.scrypt(password.encode(), salt=bytes.fromhex(salt_hex),
n=int(n), r=int(r), p=int(p), dklen=32)
return hmac.compare_digest(candidate, bytes.fromhex(digest_hex))
a = hash_password("correct horse battery staple")
b = hash_password("correct horse battery staple")
print("same password, same stored value?", a == b)
print("verify correct password:", verify_password(a, "correct horse battery staple"))
print("verify wrong password: ", verify_password(a, "correct horse battery stapler"))
print("stored format:", "$".join(a.split("$")[:4]) + "$<salt>$<digest>")same password, same stored value? False
verify correct password: True
verify wrong password: False
stored format: scrypt$16384$8$1$<salt>$<digest>6.3 Password policy
Permit long passphrases and use a generous maximum length that prevents resource abuse.
Block commonly compromised passwords where feasible.
Avoid arbitrary composition rules that encourage predictable substitutions.
Do not force periodic changes without evidence of compromise or a justified policy requirement.
Support password managers and paste.
Rate-limit attempts and detect automated abuse without making account lockout an easy denial-of-service tool.
Use secure recovery and reset flows.
6.4 Multi-factor authentication (MFA)
Factors are commonly categorized as:
Something you know: password or PIN Something you have: authenticator device or security key
Something you are: biometric characteristic Two passwords are not two different factors. Prefer phishing-resistant authentication, such as FIDO2/WebAuthn security keys or passkeys, for high-risk use cases. TOTP can be useful but is susceptible to phishing and requires careful secret protection. SMS has SIM-swap and interception risks and should not be the preferred strong factor for high-risk accounts.
MFA design must cover enrollment, recovery, device loss, replacement, revocation, and suspicious changes.
6.5 Account recovery
Recovery can become the weakest authentication path. Use short-lived, single-use, high-entropy reset tokens; store token hashes where appropriate; bind tokens to the intended account and purpose; invalidate them after use; avoid revealing whether an account exists; and notify users of important changes. Never send a password in email.
Single-use reset tokens, stored as hashes Output (CPython 3.13.1):
A fast hash (SHA-256) is acceptable here because the token is 256 bits of randomness — unlike a password, it cannot be guessed from a dictionary. In a database, mark the token used in the same transaction that changes the password.
import hashlib
import hmac
import secrets
import time
store = {} # token_hash -> (account_id, purpose, expires_at, used)
def issue_reset_token(account_id: str, now: float) -> str:
token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(token.encode()).hexdigest()
store[token_hash] = (account_id, "password_reset", now + 15 * 60, False)
return token # sent to the user; only the hash is kept
def redeem(token: str, now: float) -> str:
token_hash = hashlib.sha256(token.encode()).hexdigest()
record = store.get(token_hash)
if record is None:
return "invalid"
account_id, purpose, expires_at, used = record
if used or purpose != "password_reset" or now > expires_at:
return "invalid"
store[token_hash] = (account_id, purpose, expires_at, True) # single use
return f"ok for {account_id}"
t0 = time.time()
token = issue_reset_token("user-17", t0)
print("first use: ", redeem(token, t0 + 60))
print("second use:", redeem(token, t0 + 120))
late = issue_reset_token("user-17", t0)
print("after 16 minutes:", redeem(late, t0 + 16 * 60))
print("stored keys look like:", len(next(iter(store))), "hex characters (SHA-256), not t
…first use: ok for user-17
second use: invalid
after 16 minutes: invalid
stored keys look like: 64 hex characters (SHA-256), not the token6.6 Sessions and tokens
A session identifier is a bearer credential: anyone who obtains it may be able to act as the user. Protect it in transit and storage, rotate it after authentication or privilege changes, expire it appropriately, revoke it on logout or risk events, and prevent session fixation.
6.7 OAuth 2.0 and OpenID Connect
OAuth 2.0 is primarily an authorization framework for delegated access.
OpenID Connect (OIDC) adds an identity layer on top of OAuth 2.0.
Use Authorization Code flow with PKCE for appropriate user-facing clients.
Validate issuer, audience, signature, expiration, nonce where applicable, redirect URI, state, and protocol requirements.
Use well-maintained libraries and provider metadata.
Do not treat an arbitrary access token as an identity token.
Avoid the implicit flow for new implementations.
Never place client secrets in public browser or native-app bundles.
Authorization Code flow with PKCE Text equivalent: the client keeps a random *code verifier* secret and sends only its hash. Someone who intercepts the authorization code cannot redeem it without the verifier. state ties the response to the request the client started, nonce ties the ID token to that sign-in, and the API still makes its own authorization decision.
Output (CPython 3.13.1): This follows RFC 7636's S256 method: Base64url without padding of the SHA-256 digest. Use your OAuth library's implementation in real code.
sequenceDiagram
actor U as User
participant C as Client app
participant AS as Authorization server
participant API as Resource API
C->>C: Create code_verifier and code_challenge = SHA-256(verifier)
C->>AS: Redirect user with code_challenge, state, nonce
U->>AS: Sign in and consent
AS-->>C: Redirect back with authorization code and state
C->>C: Check that state matches
C->>AS: Exchange code + code_verifier
AS->>AS: Check SHA-256(verifier) equals the stored challenge
AS-->>C: Access token (+ ID token for OIDC)
C->>C: Validate ID token: issuer, audience, signature, expiry, nonce
C->>API: Call with access token
API->>API: Validate token and authorize the operationimport base64
import hashlib
import secrets
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
verifier = b64url(secrets.token_bytes(32))
challenge = b64url(hashlib.sha256(verifier.encode()).digest())
print("verifier length:", len(verifier), "characters")
print("challenge length:", len(challenge), "characters")
# The server recomputes the challenge from the verifier sent with the code exchange:
print("server check passes:", b64url(hashlib.sha256(verifier.encode()).digest()) == challenge)
print("a different verifier passes:", b64url(hashlib.sha256(b"guess").digest()) == challenge)verifier length: 43 characters
challenge length: 43 characters
server check passes: True
a different verifier passes: False6.8 API keys and service identities
API keys are credentials, not user passwords. Scope them, rotate them, monitor usage, avoid placing them in URLs, and never commit them to source control. Prefer short-lived workload identity or scoped tokens over long-lived static keys where supported.
Practice
Design a sign-in, MFA enrollment, password reset, lost-device recovery, logout, and suspicious-login workflow. Identify the security properties and abuse cases for every step.
Quiz
? Which password storage approach is appropriate?
- AES-encrypt passwords so support staff can read them back. :: Reversible storage turns a database leak into a password leak.
- SHA-256 of the password. :: A fast hash lets attackers test billions of guesses per second.
+ A salted, deliberately costly password hash such as Argon2id, scrypt, bcrypt or PBKDF2. :: These are built for password storage.
- Base64 of the password. :: Base64 is an encoding; anyone can reverse it.
? Which are properties of a good password-reset token? (select all that apply)
+ High entropy from a cryptographically secure generator :: It must not be guessable.
+ Short lifetime and single use :: Limits the window for misuse.
+ Bound to one account and one purpose :: A reset token must not be usable for anything else.
- Included in analytics events for debugging :: Tokens are bearer secrets and must not be logged.
? What does PKCE protect against?
- Weak user passwords :: PKCE is about the authorization code exchange.
+ An intercepted authorization code being redeemed by someone else :: Without the code verifier, the code is useless.
- Cross-site scripting in the client :: PKCE does not stop XSS.
- Expired a
…