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.
Desafío
FácilEste 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.
- En
server.js, enGET /notes/:id, busca la nota usando tanto su_idcomo suowner. El propietario debe ser el usuario que ha iniciado sesión,req.user.id. - Haz lo mismo en
DELETE /notes/:id, para que nadie pueda eliminar una nota que no le pertenece. - 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 /signupy luegoPOST /login, ambos con{"username": "ada", "password": "correct horse"}. - Envía
POST /notescon{"text": "Safe code 4417"}y copia el_idde la respuesta.GET /notes/seguido de ese id devuelve la nota. - Regístrate e inicia sesión con
{"username": "bob", "password": "correct horse"}. EnvíaGET /notes/seguido del id de ada: estado 404 y{"error": "Note not found"}. EnvíaDELETE /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');
});
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