We closed Module 7 leaving an uncomfortable question on the table. The buyTickets function in src/repositories/purchases-sql.js solves overselling with a flawless transaction: it locks the session row, decrements the capacity and issues the tickets. And it receives a userId… that arrives in the request body. In other words: whatever the client feels like sending. Anyone can buy in Lucía's name, look at Marc's orders, publish an event at Teatro Almendra without being its organizer, or cancel someone else's tickets.

This module fills that gap. And we start with what almost nobody explains properly: what authenticating actually is, how it differs from authorizing, why HTTP makes it hard, and what strategies exist to solve it. This lesson writes barely any code: it builds the mental map without which the five lessons that follow are just recipes copied without understanding.

Contents

  1. Authentication versus authorization
  2. Authentication factors and MFA
  3. The core problem: HTTP is stateless
  4. Strategies for carrying identity between requests
  5. Cookies, properly explained
  6. The Escena Viva threat model
  7. Why you never roll your own cryptography
  8. The design chosen for Escena Viva
  9. Personal data and GDPR
  10. Common mistakes and tips
  11. Exercises
  12. Conclusion

  1. Authentication versus authorization

These are two different questions asked at two different moments.

Authentication Authorization
Question Who are you? What are you allowed to do?
Moment On the way in (and on every request, when the credential is verified) On each specific action
Result An identity: user 64f…, role organizer A permission: yes / no
Typical failure Invalid credential Valid identity but no permission
HTTP code 401 Unauthorized (badly named: it means "not authenticated") 403 Forbidden

In Escena Viva the difference is very concrete:

  • Authentication: Lucía types [email protected] and her password. The server checks that the password matches what it has stored and concludes: this request comes from Lucía.
  • Authorization: Lucía, already identified, requests GET /api/orders/ord-042. The server checks that this order is hers. If it belongs to Marc, the answer is 403 (or 404 — we will see why that is sometimes preferable).

Confusing the two produces real holes. The two classic mistakes:

  1. Authenticating and assuming the permission. "If they logged in, they must be allowed." That is how the most common API flaw is born: GET /api/orders/:id returns the order to any authenticated user who guesses the identifier. It is called insecure direct object reference (IDOR), and we attack it in lesson 08-05.
  2. Authorizing without authenticating properly. Trusting a role that arrives from the client, or a userId in the body — exactly what buyTickets does today. Authorization rests on authentication: if the foundation is made of paper, so is the building.

There is a third concept worth naming so it does not get mixed in: identification is stating who you are (the e-mail); authentication is proving it (the password). An identifier is never a credential. Sending only userId is pure identification, and that is why the API is indefensible as it stands.

  1. Authentication factors and MFA

A factor is a category of proof. There are three classic ones:

Factor What it is Examples Main weakness
Something you know Secret knowledge Password, PIN, security question Reused, leaked, guessed
Something you have Possession of an object Phone with a TOTP app, FIDO2 key, card Lost or stolen; SMS can be intercepted (SIM swapping)
Something you are Biometric trait Fingerprint, face Cannot be changed once compromised; it usually unlocks a local factor rather than travelling over the network

MFA (multi-factor authentication) means using factors from different categories. Password + TOTP code is MFA. Password + security question is not: both are "something you know", and both leak from the same database.

Ranked by real-world robustness, from weakest to strongest: SMS < e-mail < TOTP (authenticator app) < physical FIDO2/WebAuthn key. SMS is better than nothing, but it is the link that targeted attacks break.

In Escena Viva we will implement the first factor properly (password, lesson 08-02) and leave the second factor documented as the natural extension: an administrator account that can cancel tickets and change roles is exactly the place where MFA stops being optional.

  1. The core problem: HTTP is stateless

HTTP is a stateless protocol: each request is independent and the server remembers nothing about the previous one. That is a design virtue (it allows scaling, load balancing, retrying), but it means that:

Even though Lucía identifies herself at POST /auth/login, the next request POST /api/purchases arrives as if the server had never seen her before.

The only way out is for every request to carry a proof of identity. All of web authentication boils down to answering three questions about that proof:

  1. Where is it stored on the client? (cookie, JavaScript memory, native store)
  2. How does it travel? (automatic cookie, Authorization header)
  3. How does the server verify it? (by looking it up in a store, or by checking a signature)

The strategies that follow are simply different combinations of those three answers.

  1. Strategies for carrying identity between requests

Strategy How it travels Server state Advantages Drawbacks When to pick it
HTTP Basic Authorization: Basic base64(user:password) on every request None Trivial to implement Sends the password over and over; no logout; no expiry; unusable without TLS Almost never in production; internal tools and quick tests
Server session + cookie Cookie holding a random identifier, sent automatically by the browser Yes: the session lives on the server Instant revocation; the cookie reveals nothing; mature and well understood Shared state (needs Redis with several processes); vulnerable to CSRF unless protected; awkward across domains Server-rendered web apps and same-site SPAs
Self-contained token (JWT) Authorization: Bearer <token> No (the token verifies itself) Stateless, scales easily; works across domains and for mobile; useful between microservices Cannot be revoked without adding state; if stored badly in the browser, XSS steals it; the payload is visible APIs consumed by mobile apps, cross-domain SPAs, service to service
API key Custom header (X-API-Key) or Authorization Yes (key table) Simple for integrations; easy to rotate and revoke per client Identifies an application, not a person; no natural expiry; leaks into repositories Machine-to-machine integrations, webhooks, internal services
OAuth 2.0 / OpenID Connect Token issued by a third party (Google, GitHub, Auth0) Depends on the flow Delegates passwords to the provider; SSO; granular consent Complex; dependency on a third party; many flows and many ways to get it wrong "Sign in with Google", access to third-party APIs, corporate identity

And the practical question, answered without hedging:

  • Native mobile app: tokens (short-lived JWT access token + refresh token in the operating system's secure store). Cookies fit poorly outside the browser.
  • Web with a server that renders HTML: server sessions with a cookie. It is the safest and simplest option; the browser already knows how to do its part.
  • Service to service: API keys or OAuth 2.0 client credentials, with rotation and a narrow scope. Never a person's password.
  • SPA + same-site API: either one; sessions if there is a single origin, tokens if there are several clients.

Escena Viva is an API with a static front-end and mobile-app ambitions. That is why it will end up on JWT, but with the cookie brought back where it really matters (the refresh token).

  1. Cookies, properly explained

Everything that comes later rests on cookies, so it is worth understanding them in detail. A cookie is a name/value pair that the server asks the browser to store and resend automatically.

HTTP/1.1 200 OK
Set-Cookie: sid=Zk9x3Qb7...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800
Content-Type: application/json

From then on, the browser includes it on its own:

GET /api/orders HTTP/1.1
Host: api.escenaviva.test
Cookie: sid=Zk9x3Qb7...

That automatic behavior is the great advantage of cookies (nothing to program on the client) and also their great danger (they are sent even when the request originates from another website: that is CSRF).

Attribute What it does Attack it mitigates
HttpOnly JavaScript cannot read it (document.cookie does not see it) XSS: even if a script is injected, it cannot exfiltrate the cookie
Secure Only travels over HTTPS Network interception (open Wi-Fi, proxy)
SameSite=Strict Not sent on any request originated by another site CSRF (maximum protection; breaks navigation from external links)
SameSite=Lax Sent on top-level GET navigations, not on POST or background requests CSRF in 95% of cases, without breaking inbound links
SameSite=None Always sent; requires Secure None: it is for cross-domain scenarios and demands explicit CSRF defense
Domain Widens the cookie to subdomains (Domain=escenaviva.test) Omitting it limits the cookie to the exact host: less reach, less risk
Path Limits which paths it is sent to (Path=/auth/refresh) Reduces the exposure of the refresh token
Max-Age / Expires Expiry. Without them the cookie is a session cookie and dies when the browser closes Limits the window of a stolen cookie
__Host- prefix The browser demands Secure, Path=/ and no Domain Stops a compromised subdomain from planting cookies on the parent domain

Three rules we will apply without exception:

  1. Every authentication cookie carries HttpOnly and Secure.
  2. SameSite=Lax by default; None only with a written justification and extra CSRF protection.
  3. The cookie never contains user data, only an opaque identifier or a signed token.

  1. The Escena Viva threat model

Threat modeling means asking, before writing any code: who is attacking, what do they want and what happens if they get it?

Attacker What they want How they would try it today Impact
Ticket scalper Hoard tickets for the Festival de Jazz Automate POST /api/purchases with no limit Sold out in seconds; reputational damage
Curious user with an open API See other people's orders GET /api/orders/ord-001, ord-002… Personal data leak; GDPR incident
Impersonator Buy in Lucía's name Send Lucía's userId in the body Improper charges, stolen tickets
A rival venue Publish or alter someone else's events POST /api/events without being the organizer of that venue Catalog sabotage
Credential automation Log in with passwords leaked from other sites Thousands of POST /auth/login Mass account compromise

And the attacks you must know by name, because the following lessons refer to them constantly:

  • XSS (Cross-Site Scripting): JavaScript is injected into a page and runs with the user's permissions. It is the reason for HttpOnly and for not storing tokens in localStorage.
  • CSRF (Cross-Site Request Forgery): another website makes your browser send an authenticated request without your knowledge, exploiting the automatic sending of cookies. It is fought with SameSite and synchronizer tokens (08-03).
  • Session fixation: the attacker plants a known session identifier before login and waits for the victim to sign in with it. Defense: regenerate the identifier on login (08-03).
  • Session hijacking: stealing an already valid session (through XSS, an insecure network or careless logging). Defense: HttpOnly, Secure, short expiry, rotation.
  • Brute force: trying many passwords against one account. Defense: slow hashing and attempt limits (08-02, 08-06).
  • Credential stuffing: trying e-mail/password pairs leaked from other services against ours. Defense: rate limiting, anomaly detection, checking against breached-password lists, and MFA.

  1. Why you never roll your own cryptography

The rule, with no nuance: do not design cryptography and do not implement cryptographic primitives. Use libraries that the community has reviewed for years.

The reasons are not about humility, they are technical:

  • An algorithm can be correct and still leak information through its execution time, its memory consumption or its error messages.
  • The details that decide security are invisible: padding, initialization vectors, modes of operation, randomness generation.
  • Broken cryptography does not fail visibly: it keeps returning perfect-looking results until the day somebody breaks it.

The same goes for homemade token schemes. The classic temptation is to store something like user=lucia|role=administrator|expires=... in the cookie and sign it "with a hash of the secret and the concatenated data". That particular design is vulnerable to length extension, to separator confusion and to non-constant-time comparisons. JWT exists precisely because this problem is already solved, with a public specification and audited libraries.

What we will do with node:crypto is what it is designed for: generating random bytes (randomBytes), hashing single-use tokens (createHash) and comparing in constant time (timingSafeEqual, which we already met in Module 3 with Buffers, and which finally makes sense here).

  1. The design chosen for Escena Viva

These are the module's decisions, written down now so that the five lessons that follow are their implementation:

Decision Choice Reason
Credential store The Mongoose User model (M7) with passwordHash and select: false The model already exists; it was only missing the password
Password hashing bcrypt with a measured cost factor Mature standard, no problematic native dependencies
Server session express-session + Passport local, taught and wired up It is the conceptual foundation and the best option for many apps
Official API authentication Access JWT (15 min) + refresh token in an httpOnly cookie Static front-end and a future mobile client
Authorization RBAC with the roles already modeled + resource ownership Three clear roles; ownership covers what RBAC does not
Access policy Centralized in src/authorization/policy.js It can be tested on its own (M9) and does not scatter into if statements

And the resulting route map:

Zone Routes Who gets in
Public GET /api/events, GET /api/events/:id, GET /api/sessions Anyone
Authentication POST /auth/register, /auth/login, /auth/refresh, /auth/logout Anyone (with strict limits)
Authenticated POST /api/purchases, GET /api/orders, GET /api/orders/:id Attendee (only their own)
Organizer POST /api/events, PATCH /api/events/:id/publish, GET /api/venues/:venue/sales Organizer of that venue
Administration PATCH /api/users/:id/role, PATCH /api/tickets/:code/cancel Administrator

The flow we will implement between 08-02 and 08-04:

sequenceDiagram
  participant C as Client (front-end)
  participant A as Escena Viva API
  participant D as Database

  C->>A: POST /auth/register {email, password, name}
  A->>A: Validate with zod and normalize the e-mail
  A->>A: bcrypt.hash(password, cost)
  A->>D: Create User {email, passwordHash, role: 'attendee'}
  A-->>C: 201 {id, email, name, role}  (no hash)

  C->>A: POST /auth/login {email, password}
  A->>D: Find user by email (+select passwordHash)
  A->>A: bcrypt.compare (constant time)
  alt Correct credentials
    A->>D: Store the hash of the refresh token
    A-->>C: 200 {accessToken} + Set-Cookie: ev.refresh (HttpOnly)
  else Incorrect credentials
    A-->>C: 401 generic, identical message
  end

  C->>A: GET /api/orders (Authorization: Bearer accessToken)
  A->>A: Verify signature, exp, iss, aud
  A->>D: Query orders filtering by req.user.id
  A-->>C: 200 [own orders]

Notice the last step: the query filters by the authenticated user, it does not compare afterwards. That is the difference between a secure API and one with IDOR.

  1. Personal data and GDPR

Authenticating means storing personal data, and that has legal consequences on top of the technical ones.

  • Minimization: store only what is essential. Selling tickets needs an e-mail, a name and a role. No national ID, no phone number, no date of birth "just in case".
  • Purpose: data collected to sell tickets is not used for marketing without separate consent.
  • Right to erasure: there must be a real way to delete an account. Careful: sold tickets usually carry accounting obligations, so the usual approach is to anonymize the user and keep the order.
  • Passwords: a bcrypt hash is personal data in practice. Treat it as such.
  • Logs: logs containing e-mails and IP addresses are personal data too; they need a retention policy.
  • Breach notification: if credentials leak, there is a legal duty to notify within a deadline. Having the incident planned in writing, before it happens, is part of the job.

In Escena Viva we use fictional e-mails on the .test domain precisely so that we never handle real data during the course.

Common Mistakes and Tips

  • Believing that 401 means "no permission". It means "not authenticated" (the specification's name is unfortunate). No permission is 403. Confusing them makes clients redirect an already signed-in user back to the login page.
  • Storing the role on the client and trusting it. Hiding a button in the front-end is not authorization: the request can be sent anyway with curl.
  • Using localStorage for the token because "it's more convenient". A single XSS turns it into a silent identity theft. We look at this in detail in 08-04.
  • Adding SameSite=None to "fix" a CORS problem. It is usually the symptom of a badly thought-out domain design, and it opens the door to CSRF.
  • Thinking TLS solves authentication. HTTPS protects the transport; it does not say who you are or what you can do.
  • Tip: write the threat model before the code, even if it is ten lines in a file. That is what stops you from implementing defenses for attacks that do not affect you while forgetting the one that does.
  • Tip: always distinguish a credential from an identifier. If one piece of data is enough to act on someone's behalf, it is a credential and deserves credential-grade protection.

Exercises

Exercise 1: classify the requests

For each of these Escena Viva situations, say whether the failure is one of authentication or authorization, and which HTTP code applies:

  1. Someone calls POST /api/purchases with no credential at all.
  2. Marc, with a valid session, requests GET /api/orders/ord-042, which belongs to Lucía.
  3. The organizer of Sala Bóveda tries to publish evt-001, which belongs to Teatro Almendra.
  4. An expired access token is sent.
  5. An attendee tries PATCH /api/users/:id/role.

Exercise 2: choose a strategy

Justify in two or three sentences which strategy from the table in section 4 you would choose for:

  1. An internal Escena Viva administration panel, served by the application itself with HTML templates.
  2. A future Escena Viva mobile app for validating tickets at the door of Auditorio Ribera.
  3. A billing service that calls GET /api/venues/:venue/sales every night.

Exercise 3: the Set-Cookie header

Write the complete Set-Cookie header for the Escena Viva refresh token knowing that: the API lives at api.escenaviva.test, the token must only be sent to /auth/refresh, it lasts 30 days, it must not be readable by JavaScript, it travels only over HTTPS, and we do not want it sent from other sites. Explain each attribute.

Solutions

Exercise 1

Case Type Code
1 Authentication (there is no identity) 401
2 Authorization (valid identity, someone else's resource) 403, or 404 so as not to reveal existence
3 Authorization (correct role, wrong ownership) 403
4 Authentication (the credential is no longer valid) 401, stating that it expired so the client refreshes
5 Authorization (insufficient role) 403

Case 3 is especially instructive: the organizer role is correct, and the operation must still be denied. RBAC alone is not enough; you also need to check ownership of the resource (08-05).

Exercise 2

  1. Server sessions with a cookie. It is a single origin, the server renders HTML, the browser manages the cookie with no code of our own, and revocation is immediate: if you fire an administrator, you delete their session and they are locked out instantly.
  2. Short-lived access JWT + refresh token stored in the operating system's secure store. Cookies do not fit outside the browser, and the ticket validator needs to work with intermittent connectivity; a signed token with a short expiry is verified without hitting the database on every scan.
  3. API key (or OAuth 2.0 client credentials) with read-only scope over sales, rotatable and revocable independently. It is not a person: it should have neither a password nor a session.

Exercise 3

Set-Cookie: ev.refresh=<token>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=2592000
  • HttpOnly: document.cookie cannot see it, so an XSS cannot exfiltrate it.
  • Secure: HTTPS only; never in the clear over the network.
  • SameSite=Strict: not sent on requests originated by other sites, which kills CSRF against the refresh endpoint.
  • Path=/auth/refresh: it is not attached to normal API calls, reducing its exposure in logs and proxies.
  • Max-Age=2592000: 30 days in seconds; an explicit expiry instead of a session cookie.
  • No Domain: the cookie stays bound to the exact host api.escenaviva.test, without spreading to subdomains.

Conclusion

We now have the map. Authenticating is proving who you are; authorizing is deciding what you can do; confusing them is the cause of the most expensive holes. HTTP is stateless, so every request must carry a proof of identity, and the strategies for achieving that — Basic, sessions, JWT, API keys, OAuth — each have their own territory. Cookies, with their HttpOnly, Secure and SameSite attributes, are the infrastructure almost everything rests on, and each attribute neutralizes one specific attack. We know the Escena Viva threat model, we know we are not going to invent cryptography, and we have the design written down: sessions to understand, JWT with a refresh cookie to ship, RBAC plus resource ownership to authorize.

One thing is missing before anything else: there has to be a password to verify. The User model has been sitting there since lesson 07-02 with its role field and no password field at all, deliberately. In the next lesson, User Registration and Password Hashing, we fill that gap: why a password is never stored in the clear or with SHA-256, what bcrypt really does, how its cost factor is chosen, how to write a POST /auth/register that does not leak which e-mails exist, and how to build verification and recovery tokens properly, which is where most projects fall apart.

Node.js Course: From Beginner to Advanced

Module 1: Introduction to Node.js

Module 2: Core Concepts

Module 3: File System and I/O

Module 4: HTTP and Web Servers

Module 5: NPM and Package Management

Module 6: The Express.js Framework

Module 7: Databases and ORMs

Module 8: Authentication and Authorization

Module 9: Testing and Debugging

Module 10: Advanced Topics

Module 11: Deployment and DevOps

Module 12: Real-World Projects

© Copyright 2026. All rights reserved