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
- Authentication versus authorization
- Authentication factors and MFA
- The core problem: HTTP is stateless
- Strategies for carrying identity between requests
- Cookies, properly explained
- The Escena Viva threat model
- Why you never roll your own cryptography
- The design chosen for Escena Viva
- Personal data and GDPR
- Common mistakes and tips
- Exercises
- Conclusion
- 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:
- 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/:idreturns 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. - Authorizing without authenticating properly. Trusting a
rolethat arrives from the client, or auserIdin the body — exactly whatbuyTicketsdoes 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.
- 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.
- 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 requestPOST /api/purchasesarrives 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:
- Where is it stored on the client? (cookie, JavaScript memory, native store)
- How does it travel? (automatic cookie,
Authorizationheader) - 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.
- 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).
- 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/jsonFrom then on, the browser includes it on its own:
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:
- Every authentication cookie carries
HttpOnlyandSecure. SameSite=Laxby default;Noneonly with a written justification and extra CSRF protection.- The cookie never contains user data, only an opaque identifier or a signed token.
- 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
HttpOnlyand for not storing tokens inlocalStorage. - 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
SameSiteand 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.
- 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).
- 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.
- 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
localStoragefor 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=Noneto "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:
- Someone calls
POST /api/purchaseswith no credential at all. - Marc, with a valid session, requests
GET /api/orders/ord-042, which belongs to Lucía. - The organizer of Sala Bóveda tries to publish
evt-001, which belongs to Teatro Almendra. - An expired access token is sent.
- 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:
- An internal Escena Viva administration panel, served by the application itself with HTML templates.
- A future Escena Viva mobile app for validating tickets at the door of Auditorio Ribera.
- A billing service that calls
GET /api/venues/:venue/salesevery 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
- 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.
- 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.
- 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=2592000HttpOnly:document.cookiecannot 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 hostapi.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
- What Is Node.js?
- Installing and Setting Up the Environment
- Your First Node.js Program
- The Node.js REPL
- Modern JavaScript for Node.js
- The Course Project: the Escena Viva Platform
Module 2: Core Concepts
- Node.js Architecture
- The Event Loop
- Callbacks and Asynchronous Programming
- Promises and async/await
- Events and EventEmitter
- CommonJS Modules and require()
- ES Modules and Interoperability
Module 3: File System and I/O
- Reading and Writing Files
- The fs Module in Depth
- Cross-Platform Paths with the path Module
- Working with Streams
- Transform Streams and pipeline
- Buffers and Binary Data
Module 4: HTTP and Web Servers
- Creating a Simple HTTP Server
- Handling Requests and Responses
- Manual Routing
- Serving Static Files
- Receiving Data: Request Bodies and JSON
- Consuming External APIs from Node.js
Module 5: NPM and Package Management
- Introduction to NPM and package.json
- Installing and Using Packages
- Semantic Versioning and package-lock
- npm Scripts and Project Automation
- Creating and Publishing Packages
- Dependency Security and Maintenance
Module 6: The Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Routing in Express
- Middleware
- Essential Third-Party Middleware
- Input Data Validation
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- CRUD Operations
- Relationships, Population and Advanced Queries
- Using SQL Databases with Sequelize
- Migrations, Transactions and Seed Data
Module 8: Authentication and Authorization
- Introduction to Authentication
- User Registration and Password Hashing
- Sessions and Cookies with Passport.js
- Authentication with JWT
- Role-Based Access Control
- API Security Best Practices
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Test Doubles with Sinon
- Integration Testing
- Coverage and Test Automation
- Debugging Node.js Applications
Module 10: Advanced Topics
- The Cluster Module
- Worker Threads
- Caching and Job Queues with Redis
- Performance Optimization
- Building RESTful APIs
- GraphQL with Node.js
Module 11: Deployment and DevOps
- Configuration and Environment Variables
- Logging and Monitoring in Production
- Using PM2 for Process Management
- Packaging with Docker
- Deploying to Heroku and Other PaaS
- Continuous Integration and Deployment
