Case: Implement Login
Login is: credentials arrive, the identity is verified, an authenticated session is created, and future requests are associated with it. The concept and its operations are derivable; the security-sensitive mechanics — hashing, tokens, CSRF — are learned from Security Engineering, not improvised.
The situation, the reflex, and why it stalls
Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.
You need a login and you know it is security-sensitive. How do you derive the concept, its state and its operations without inventing the parts that must not be invented?
The store needs accounts so orders belong to someone. You have used login forms your whole life and never built one. You know vaguely that passwords must not be stored in plain text and that "sessions" and "tokens" exist, and you do not know where the concept ends and the cryptography begins.
Copy a tutorial's auth code, or install the first auth library the framework recommends and wire its example. There is a form, a route, a cookie and a redirect within the hour, and it logs you in.
The tutorial's login works and you cannot say what state it created. There is a cookie; you do not know what is in it, where its counterpart lives on the server, or what "logging out" actually deletes. When a user reports being logged out on a second tab, there is no model to reason with.
- The tutorial's login works and you cannot say what state it created. There is a cookie; you do not know what is in it, where its counterpart lives on the server, or what "logging out" actually deletes. When a user reports being logged out on a second tab, there is no model to reason with.
- The pasted code compares the typed password against a stored value with
==. Whether the stored value is a hash, which hash, and whether the comparison leaks timing is not something the tutorial discussed — and you would not know to ask, because the concept was never separated from the mechanism. - The library handles everything, which means the four operations — register, log in, check the current session, log out — exist only as configuration. The first requirement the library does not cover ("sessions expire after inactivity", "one device at a time") has nowhere to go.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Separate the concept from the cryptography. The concept: a user presents credentials; the system verifies that they identify a known user; it creates an authenticated session; later requests carry something that associates them with that session. That is derivable by the ordinary loop — meaning, state, operations, rules, examples.
- Mark the places where the concept touches security-sensitive mechanics — how credentials are stored, how the session identifier is generated and carried, how a forged request is rejected — and route each one to the domain that teaches it. Those are not places to be clever; they are places to be correct, and correct is documented.
- Derive the state honestly: a user record with an identifier and a stored credential verifier (never the credential); a session with an identifier, a user reference and a created-at; and the association between a request and a session. Challenge every field as usual — but the verifier field's type is decided by Security, not by you.
- Write the operations with their errors, and write the examples including the invalid ones: wrong password, unknown user, expired session, request with no session. The invalid cases are the product here; the happy path is the easy part.
Meaning, state and the line that should not be crossed
The four clauses — present credentials, verify identity, create a session, associate later requests — are the meaning, and everything derivable follows from them. The state canvas is small: users with a verifier, sessions with a user and a time. The one field people try to add and must not is the password itself; the concept never reads it after verification, so it has no reason to exist.
The board separates what the loop can derive from what it must not invent. The unknowns are real and each one is routed rather than answered here — the experiment is "read the lesson, then implement around its answer".
- ✓Meaning: verify credentials identify a known user; remember that for later requests.
- ✓State: users(id, email, verifier); sessions(id, userId, createdAt). No password field.
- ✓Operations: register, logIn, currentUser, logOut. Invalid cases written first.
- ~One session per device, several per user; logging out one does not log out the others — until a requirement says so.
- ~Email is the login name and is unique — a rule that belongs to register and to the database.
? How do I store the password safely?
becomes What is the verifier — which hash, which salt, which cost — and how is it compared without leaking timing?
experiment Do not experiment. Read Security's password storage lesson and use the primitive it names; verify by checking that two registrations with the same password store different verifiers.
? What is a session, mechanically?
becomes How is the session id generated so it cannot be guessed, where is it carried, and what flags does the carrier need?
experiment Read Security's sessions lesson; then write the example "a request carrying a made-up session id → anonymous" and make it pass.
? What is CSRF and do I have it?
becomes Can a page on another site cause this browser to send a state-changing request that carries the session automatically?
experiment Read the CSRF defence lesson; write the example "a POST without the defence token → rejected" once the defence exists.
Three unknowns, none of them blockers, none of them answered by guessing. The concept work continues while the reading happens.
Operations and rules — the invalid cases are the product
For most concepts the happy path is the interesting part. For login it is the opposite: the happy path is a lookup and a comparison, and the value is in what happens when the comparison fails, when the session is missing, when the session is stale. The rule below is the one that surprises most learners, and the state change is the happy path shown once so that log out can be shown as its inverse.
The rule "wrong email and wrong password are indistinguishable" is a case of Rules That Live Elsewhere in reverse: it is a security rule that must be enforced in the concept's own logic, because the response text is produced here.
- logOut(s1) is the inverse: sessions loses s1 and nothing else changes; a later request carrying s1 is anonymous because the server no longer knows it — deleting the cookie alone would not achieve that.
- currentUser(request) never errors: a missing or unknown session yields anonymous, and the caller decides whether anonymous is allowed here — that decision is authorisation, a different concept (Where Authorization Must Live in Security).
- register enforces email uniqueness and stores the verifier; it never returns the verifier, and it never logs the password.
users = { alice → verifier }, sessions = {}users unchanged, sessions = { s1 → { userId: alice, createdAt: t } }; response carries s1rule An unknown email and a wrong password produce the same response, so that the response cannot be used to discover which emails are registered.
↓ becomes validation Look up the user; whether the lookup fails or the verifier does not match, return the same error and take a comparable amount of time.
user = users.byEmail(email) ok = user exists and verify(password, user.verifier) if not ok: reject "invalid credentials" -- same text, both cases
Representation, the trace, and what comes next
currentUser runs on every request, so the sessions representation is chosen for it: a map from session id to session, O(1) average, against an array of sessions scanned per request, O(n) in the number of live sessions. Users need two lookups — by email for log in, by id for everything else — which in memory is two maps and in a database is a unique index on email (Hash Map in DSA; Composite Indexes and the Leftmost-Prefix Rule in Database).
The trace runs the only operation that every other feature depends on. Its branch is the whole point: found-and-valid against anything else.
- 1Users and sessions in memory; register and logIn with the verifier taken from Security
because The concept is testable with two users and no browser.
- 2currentUser and logOut, with the examples for unknown, expired and forged session ids
because Every later feature calls currentUser; its invalid cases must be right before any page depends on it.
- 3The transport: how the session id travels, with the flags and the CSRF defence from Security
because Only now is there a browser involved, and only now does the mechanism matter.
- 4Persistence of users and sessions
because A restart logging everyone out is the first thing a user notices — the same trigger as the cart's V2.
- inputrequest with session identifier s1 (carried however the sessions lesson says it is carried)
- lookupsessions.get(s1) → { userId: alice, createdAt: t }; users.byId(alice) → alice
- branchsession exists and is not expired → authenticated; otherwise → anonymous (no error)
- mutationnone — reading who you are must not change state; a later "lastSeenAt" version breaks this and must say so
- outputalice
1function logIn(email, password):2 user = users.byEmail(email)3 ok = user exists and verify(password, user.verifier) -- verify: Security / password-storage4 if not ok: reject "invalid credentials"5 session = { id: newSessionId(), userId: user.id, createdAt: now } -- newSessionId: Security / sessions6 sessions.put(session.id, session)7 return session.id -- carried per Security / sessions; defended per csrf-defenseTwo functions in this pseudocode are deliberately not defined here. The concept is complete without them; the implementation is not, and the lines say where their definitions come from.
The implementation ladder
Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.
Authentication = Establishing which known user is behind a request, and remembering that answer for the requests that follow.
- Does a session have identity? Yes. Two sessions for the same user on two devices are two sessions; logging one out must not log out the other. The session token is its identity — it is also the only thing the client holds, which is what makes a stolen token dangerous.
- Who owns it? The user it was created for. A session belongs to exactly one user; a user may have many sessions. Nothing else — a page, a cart, a comment — owns a session; they ask it who the user is.
- How long does it exist? From login until logout or expiry. Expiry is a rule that does not exist in V0 and is the first thing V5 adds, because a session that lives forever is a token that is dangerous forever.
- Should it survive reload? Yes — that is the entire reason the session exists. The token is kept by the browser (a cookie or storage) and sent with every request; the session record lives on the server. The concept of "the same user, later" is what the token carries across the reload.
- Should it survive login? The question inverts: login *creates* it. A second login by the same user creates a second session, or replaces the first — a rule the product decides.
- users[].ididkeepThe thing every other concept refers to as "the owner"; sessions point at it.
- users[].usernamestring, uniquekeepThe credential the person presents to say who they claim to be.
- users[].passwordHashopaque stringkeepSomething the server can check a presented password against.
- sessionsmap from token to { userId, createdAt }keepThe memory that turns "logged in a minute ago" into "this request is Alice".
- sessions[].createdAttimestampdependsExpiry needs to know how old the session is.
- sessions[].lastSeenAttimestampdropIdle timeout and "active devices" screens.
- users[].roleenumdropAdmins can do more.
- loginAttemptscounter per usernamedependsLocking out a guesser.
- create Register — the new user id
- domain Login — a session token
- read Current user — the user id the session belongs to, or none
- delete Logout — nothing
- • A session belongs to exactly one user.
- • A logged-out session is invalid.
- • Verification never reveals which part was wrong.
- • The password is never stored.
- • A token is unguessable.
- • Usernames are unique.
How to do it
Most important first.
- Write the meaning in four clauses — present credentials, verify identity, create a session, associate later requests — and check that every operation you list maps to one of them (Nouns to Data, Verbs to Behaviour).
- List the state and challenge it: user id (keep), email (keep, the login name), password (never stored — replaced by a verifier whose form Security decides), session id (keep), session user (keep), session created-at (keep — expiry reads it), "remember me" (depends — a V2 requirement).
- Write the operations: register, log in, current user (from a request), log out. For each, the errors are the interesting part; write them first (Operation Contracts).
- Write the rules: a session belongs to exactly one user; a request without a valid session is anonymous, not an error; a wrong password and an unknown email produce the same response; logging out invalidates the session on the server, not only in the browser.
- Draw the state change for log in and log out, then the trace for "current user" — it is the operation every other request calls, and the one whose branch (session found and valid, or not) decides everything downstream.
- Before implementing the verifier, the session identifier or the cookie flags, read the linked Security Engineering lessons; implement the concept's logic around their answers, not instead of them.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- Meaning: verify that whoever sent these credentials is a known user, and remember that verification for their later requests. Identity: a session has identity — two sessions for the same user on two devices are two sessions, and logging one out must not log the other out unless a rule says so. Lifetime: from log in until log out or expiry.
- State after the challenge: users(id, email, verifier); sessions(id, userId, createdAt). The
passwordfield was proposed and rejected — the concept never needs the password again after verification, so the verifier is what is stored, and its form (a salted, slow hash) is Security's answer, linked below.lastSeenAtis deferred to the inactivity-expiry requirement. - Operations: register(email, password) → a user; logIn(email, password) → a session or "invalid credentials"; currentUser(request) → a user or anonymous; logOut(session) → the session is gone. Rules: one user per session; wrong email and wrong password are indistinguishable from outside; a request with no session is anonymous; log out invalidates server-side.
- Examples: sessions = [] → logIn(alice, correct) → sessions = [s1 → alice]; logIn(alice, wrong) → sessions unchanged, "invalid credentials"; logIn(nobody, anything) → the same response, not "unknown user"; currentUser(request carrying s1) → alice; logOut(s1) → sessions = [], and a later request carrying s1 → anonymous.
- Representation: sessions keyed by session id, because currentUser runs on every request and must find the session by id — O(1) average in a map, O(n) in an array of sessions. Users keyed by email for log in and by id for everything else. Then the security questions, each routed: how is the verifier computed (Password Storage in Security), how is the session id generated and carried (Sessions), how is a forged cross-site request rejected (CSRF Defense).
How you know it worked
What now exists that did not before, and what question you can now ask.
- You can draw the state after log in and after log out and name what changed, and you can say what a request carries that connects it to a session.
- Every operation has its invalid cases written before its happy path, and wrong-email and wrong-password produce the same example output.
- The security-sensitive lines in your implementation each cite a Security lesson rather than a guess; nothing about hashing or token generation is improvised.
- When the requirement "expire after an hour of inactivity" arrives you know it adds one field, one rule, one check in currentUser and one example — before touching the library.
The questions you can now ask
The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.
- ?What are the four clauses of login here, and does every operation map to one of them?
- ?What is stored instead of the password — and which domain decides its form?
- ?What does a request carry that associates it with a session, and what happens when it carries nothing?
- ?Which responses must be indistinguishable from outside, and does my example show that?
- ?Which lines of my implementation are security-sensitive, and which lesson is each one taken from?
What can go wrong
- The separation becomes an excuse: "the concept is derived, the mechanics are someone else's problem", and the verifier is implemented with a fast hash because the lesson that said otherwise was linked and not read.
- Over-derivation: register, log in, log out, password reset, email verification, two-factor and "remember me" are all specified before a single user can log in. V1 is register, log in, current user, log out; the rest arrive as requirements.
- The session state is derived and then not used: a library is installed anyway and its session model, not yours, is what runs — and the derivation was documentation for a system that does not exist.
- Deriving the concept before installing a library means understanding more than you strictly needed to for V1; the library would have hidden the session model and still worked.
- Routing the mechanics to Security is a reading assignment in the middle of an implementation; it is slower than pasting, by exactly the amount of time it takes to learn what must not be guessed.
- Indistinguishable failure responses make support harder — "the email is wrong" would be friendlier — and the rule is a deliberate choice of the attacker's ignorance over the user's convenience.
- "Derive login yourself means implement the hashing yourself." No — derive the concept, its state and its operations; take the verifier, the session identifier and the transport from the domain that owns them. The line runs between the logic and the primitives.
- "Using an auth library is outsourcing understanding." Using one you can explain — which state it holds, which of your four operations it implements, where it enforces which rule — is delegation; using one you cannot is the reflex.
- "Security is a V2 concern for a login." The forbidden version of this is "security does not matter, it is an MVP" — false for login in particular, because the credential is the one thing that cannot be re-issued after a leak. Some things can be simplified; this one cannot be ignored (What Cannot Be Simplified).
Where this applies
Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.
- ILLUSTRATIVEAlice, the session s1 and the inactivity expiry of an hour are invented to show the shape; the security mechanics are deliberately left to the linked domain rather than sketched here.
- DOMAIN-SPECIFICFor a store, cookie sessions with server-side state are the natural V1; for a public API consumed by other servers, the "association" clause is a token and the state model shifts — the four clauses stay, the representation changes.
- GENERALThe move — derive the concept, mark the security-sensitive mechanics, route them — applies to payments, uploads and anything else where a part of the implementation must be correct rather than merely working.
Where the depth lives
This domain asks the question and hands the answer off by name.
- — The manifesto's "What Are You Delegating?" at /manifesto/delegating: an auth library delegates the mechanics; the session model and the four operations stay yours.