Menu
CoddyTech

Solo tus notas

Parte de la sección Autenticación y seguridad del Journey de Node.js de Coddy. Lección 18 de 24.

Iniciar sesión demuestra quién eres, no que una nota sea tuya. GET /notes/:id busca una nota solo por su id, así que cualquier persona que haya iniciado sesión y tenga un id, ya sea de un enlace compartido o de un registro, puede leer esa nota. Comprobar quién es el propietario se llama autorización.

Incluye al propietario en la propia consulta para que la base de datos nunca devuelva el documento de otra persona. Responde con 404, no con 403, cuando no se encuentre: 403 le indicaría al visitante que la nota existe.

const order = await orders.findOne({
    _id: new ObjectId(req.params.id),
    customer: req.user.id,
});
if (!order) {
    return res.status(404).json({ error: 'Order not found' });
}

Toda consulta que acceda a los datos de un usuario necesita el filtro: lecturas, actualizaciones y eliminaciones.

challenge icon

Desafío

Fácil

Este capítulo retoma el inicio de sesión con sesiones del capítulo 2, y el servidor ha crecido. Ahora tiene una página en GET /, rutas para una nota en GET /notes/:id y DELETE /notes/:id, y una cuenta de administrador. Las dos rutas /notes/:id tienen una vulnerabilidad: buscan una nota solo por su id. Cualquier usuario que haya iniciado sesión y conozca un id puede leer o eliminar la nota de otra persona. Corrígelo.

  1. En server.js, en GET /notes/:id, busca la nota usando tanto su _id como su owner. El propietario debe ser el usuario que ha iniciado sesión, req.user.id.
  2. Haz lo mismo en DELETE /notes/:id, para que nadie pueda eliminar una nota que no le pertenece.
  3. Cuando no hay ninguna nota que coincida, ambas rutas ya responden con el estado 404 y {"error": "Note not found"}. Conserva esa respuesta también para la nota de otra persona. Un 403 le diría al visitante que la nota existe.

Pruébalo: Pulsa «Ejecutar».

  • Envía POST /signup y luego POST /login, ambos con {"username": "ada", "password": "correct horse"}.
  • Envía POST /notes con {"text": "Safe code 4417"} y copia el _id de la respuesta. GET /notes/ seguido de ese id devuelve la nota.
  • Regístrate e inicia sesión con {"username": "bob", "password": "correct horse"}. Envía GET /notes/ seguido del id de ada: estado 404 y {"error": "Note not found"}. Envía DELETE /notes/ seguido del mismo id: también 404.
  • Vuelve a iniciar sesión como ada, con el cuerpo de la primera etapa. Su nota sigue ahí. Ahora envía DELETE: estado 204.

Pruébalo tú mismo

import express from 'express';
import { ObjectId } from 'mongodb';
import { randomBytes } from 'node:crypto';
import { parseCookies } from './cookies.js';
import { notes, sessions, users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';

const app = express();
app.use(express.json());

// La única cuenta de administrador. Su contraseña proviene del entorno.
await users.insertOne({ username: 'admin', role: 'admin', passwordHash: await hashPassword(process.env.ADMIN_PASSWORD) });

app.post('/signup', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const password = String(req.body.password ?? '');
    if (!username || password.length < 8) {
        return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
    }
    if (await users.findOne({ username })) {
        return res.status(409).json({ error: 'That username is taken' });
    }
    await users.insertOne({ username, passwordHash: await hashPassword(password) });
    res.status(201).json({ username });
});

app.post('/login', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const user = await users.findOne({ username });
    if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
        return res.status(401).json({ error: 'Wrong username or password' });
    }
    const sid = randomBytes(24).toString('base64url');
    await sessions.insertOne({ _id: sid, userId: user._id, createdAt: new Date() });
    res.cookie('sid', sid, {
        httpOnly: true,
        sameSite: 'lax',
        secure: true,
        maxAge: 24 * 60 * 60 * 1000,
    });
    res.json({ username: user.username });
});

// Busca al usuario de la cookie de sesión o responde con 401.
async function requireUser(req, res, next) {
    const { sid } = parseCookies(req.get('cookie'));
    const session = sid && (await sessions.findOne({ _id: sid }));
    const user = session && (await users.findOne({ _id: session.userId }));
    if (!user) {
        return res.status(401).json({ error: 'Log in first' });
    }
    req.user = { id: user._id.toHexString(), username: user.username, role: user.role };
    next();
}

app.get('/me', requireUser, (req, res) => {
    res.json({ username: req.user.username });
});

app.post('/logout', async (req, res) => {
    const { sid } = parseCookies(req.get('cookie'));
    if (sid) {
        await sessions.deleteOne({ _id: sid });
    }
    res.clearCookie('sid');
    res.status(204).end();
});

// Tus notas en una página.
app.get('/', requireUser, async (req, res) => {
    const list = await notes.find({ owner: req.user.id }).toArray();
    const items = list.map((note) => `<li>${note.text}</li>`).join('');
    res.send(`<!doctype html><title>Vault</title><h1>${req.user.username}'s notes</h1><ul>${items}</ul>`);
});

// Notas: cada usuario solo lee y escribe las suyas.
app.get('/notes', requireUser, async (req, res) => {
    res.json(await notes.find({ owner: req.user.id }).toArray());
});

app.post('/notes', requireUser, async (req, res) => {
    const note = { owner: req.user.id, text: String(req.body.text ?? '') };
    await notes.insertOne(note);
    res.status(201).json(note);
});

app.get('/notes/:id', requireUser, async (req, res) => {
    if (!ObjectId.isValid(req.params.id)) {
        return res.status(404).json({ error: 'Note not found' });
    }
    const note = await notes.findOne({ _id: new ObjectId(req.params.id) });
    if (!note) {
        return res.status(404).json({ error: 'Note not found' });
    }
    res.json(note);
});

app.delete('/notes/:id', requireUser, async (req, res) => {
    if (!ObjectId.isValid(req.params.id)) {
        return res.status(404).json({ error: 'Note not found' });
    }
    const { deletedCount } = await notes.deleteOne({ _id: new ObjectId(req.params.id) });
    if (!deletedCount) {
        return res.status(404).json({ error: 'Note not found' });
    }
    res.status(204).end();
});

// Para administradores: cuántas cuentas hay.
app.get('/admin', requireUser, async (req, res) => {
    res.json({ users: await users.countDocuments() });
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Autenticación y seguridad

Practica por tu cuenta: Playground de Express