๐Ÿ“š Sanjay's Study Notes Home Roadmap HTML CSS JavaScript TypeScript Node SQL & Prisma React Tailwind Auth Apps Git & Deploy

09 ยท Auth โ€” who are you, and what may you do?

Right now ANYONE who can reach your API can delete every book with one request. Phase 8 fixes that: librarians log in, routes check who's asking, and roles decide what's allowed. This is the phase where your app becomes trustable.

Two words that sound alike and must never be confused AuthentiCATION โ€” WHO are you? (login, proving identity). AuthoriZATION โ€” WHAT may you do? (a member may browse; only a librarian may issue books). Every security bug story starts with someone mixing these up. Login answers the first; roles answer the second.

1. Passwords โ€” the iron rule: never store them

The rule before all rules You never store a password. Not "encrypted", not hidden โ€” never stored at all. Databases leak (hacks, backups, logs, one careless SELECT in a screenshot). What you store is a hash โ€” and even then, a special kind.
What a hash is, and why it works A hash function is a one-way meat grinder: hash("tiger123") โ†’ "$2b$10$X4kPq..." โ€” same input always gives the same output, but there's no way back from output to input. Login then never compares passwords; it compares hashes: grind what the user typed, check it matches the stored grind. The server can verify you know the password without ever keeping it.
// bcrypt โ€” the standard tool (npm install bcrypt)
const hash = await bcrypt.hash(password, 10);       // at registration โ†’ store THIS
const ok   = await bcrypt.compare(typed, hash);    // at login โ†’ true/false
Why bcrypt specifically โ€” salt and slowness, both deliberate Fast hashes (like SHA-256) can be guessed at billions/second on a GPU โ€” attackers pre-grind entire dictionaries. bcrypt fights back twice: a random salt mixed into every hash (two users with the same password get different hashes โ€” pre-computed tables are useless), and it's deliberately slow (that 10 = 2ยนโฐ internal rounds; tune it up as computers speed up). Slow is a feature: irrelevant per login, devastating per billion guesses.

2. Tokens โ€” how the server remembers you (without remembering you)

HTTP is stateless โ€” every request arrives a stranger. Logging in once must somehow cover the next thousand requests. The modern answer: at login, the server issues a signed token (a JWT โ€” JSON Web Token) that the client shows on every request.

eyJhbGciOi...  .  eyJ1c2VySWQiOjcsInJvbGUiOi...  .  4Hb32k9dQ...
   HEADER              PAYLOAD                    SIGNATURE
 (algorithm)     ({ userId: 7, role: "librarian",   (the seal)
                    exp: 1756600000 })
A JWT is a stamped ID card, not a secret box The payload is just base64 โ€” anyone can read it (never put secrets inside). The magic is the third part: a signature computed from header+payload+a secret key only the server knows. Change one character of the payload ("role":"member" โ†’ "librarian") and the signature no longer matches โ€” forgery detected instantly. The server doesn't store sessions at all; the card proves itself. exp makes it expire like a visitor badge.
The login flow, end to end 1) POST /login with username+password โ†’ 2) server bcrypt.compares โ†’ 3) server signs a JWT containing { userId, role } โ†’ 4) client keeps it and sends it on every request in a header: Authorization: Bearer eyJhbG... โ†’ 5) server verifies the signature and trusts the payload. Steps 4โ€“5 repeat for every protected request; passwords travel exactly once.

3. Protecting Fastify routes โ€” the checkpoint pattern

// a preHandler runs BEFORE the route โ€” a checkpoint at the door
app.post("/loans", { preHandler: [requireLibrarian] }, async (request) => {
    // only reached if the checkpoint passed
});

async function requireLibrarian(request, reply) {
    const user = await verifyToken(request);   // checks the signature
    if (!user) return reply.code(401).send({ error: "login required" });
    if (user.role !== "librarian") return reply.code(403).send({ error: "librarians only" });
}
StatusMeansMemory hook
401 Unauthorizedwe don't know who you are โ€” no/bad token"show me your card"
403 Forbiddenwe know exactly who you are โ€” and no"your card doesn't open this door"
This is just your guard clauses, promoted if (!user) return ... โ€” the same early-return shape as if (!book) return; from Task 4.4, now guarding a door instead of an array lookup. A preHandler is a guard clause that Fastify runs for you before the handler. Reusable across every protected route: write the checkpoint once, list it on each door that needs it.

4. Roles โ€” authorization in the data

model User {
    id           Int    @id @default(autoincrement())
    username     String @unique
    passwordHash String                    // the bcrypt output โ€” NEVER the password
    role         String @default("member") // "member" | "librarian"
}

The Library's permission map โ€” decided in the preHandlers:

ActionMemberLibrarian
browse/search booksโœ…โœ…
see their own loansโœ…โœ…
issue / return booksโŒโœ…
add / delete booksโŒโœ…

5. The React side

// after login: keep the token (Zustand store), attach it on every call
fetch("http://localhost:3000/loans", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ bookId, memberName }),
});
Two truths that surprise everyone (1) Hiding a button is not security. The UI can hide "Delete" from members, but anyone can send the HTTP request directly โ€” the SERVER's checkpoint is the real lock; the UI's hiding is just politeness. Never trust the client. (2) Where to keep the token is a real decision: localStorage is simple but readable by any JS on the page (XSS risk); httpOnly cookies are safer from XSS but bring their own complications (CSRF). We'll start simple and learn the trade-off properly when we get here.

6. The Phase 8 arc

StepWhat gets built
1. User model + registerPrisma migration; bcrypt.hash on the way in
2. POST /loginbcrypt.compare โ†’ sign and return a JWT
3. Checkpointsverify-token preHandler; then the role check
4. Protect the write routesissue/return/add/delete require librarian
5. React login screenform โ†’ token in store โ†’ Authorization header everywhere
6. Milestoneonly logged-in librarians can issue books โ€” verified by trying without a token (401) and as a member (403)

7. The mental model to keep

IdeaOne line to remember
Authentication vs authorizationwho you are vs what you may do
Password storagenever โ€” store a bcrypt hash; compare grinds, not secrets
Salt + slowsame password โ‰  same hash; slow per login, fatal per billion guesses
JWTreadable ID card with a tamper-proof seal; server stores nothing
401 vs 403no card vs wrong card
preHandleryour guard clause, standing at the door before the handler
Golden rulethe server is the lock; the UI only hides doorknobs

โ† Prev: Tailwind + Zustand  ยท  Back to contents  ยท  Next: Desktop & Mobile โ†’