Menu
CoddyTech

Cerrar sesión

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

Limpiar la cookie solo le pide al navegador que olvide el id. Quien haya copiado el id, desde un ordenador compartido o una cookie robada, todavía conserva una clave que funciona. Cerrar sesión también debe eliminar la sesión en el servidor.

res.clearCookie(name) le indica al navegador que elimine una cookie. El estado 204 Sin contenido significa que funcionó y que no hay nada que enviar de vuelta: res.status(204).end().

Aquí tienes una ruta relacionada que cierra la sesión de un usuario en todos los dispositivos a la vez:

app.post('/logout-everywhere', requireUser, async (req, res) => {
    await sessions.deleteMany({ userId: new ObjectId(req.user.id) });
    res.clearCookie('sid');
    res.status(204).end();
});
challenge icon

Desafío

Fácil

Añade una forma de cerrar sesión. Borrar la cookie no basta por sí solo, porque cualquiera que haya copiado el id de sesión podría seguir usándolo. Así que el servidor también olvida la sesión.

  1. En server.js, añade POST /logout. No necesita cuerpo.
  2. Lee la cookie sid. Si existe, elimina de sessions la sesión que tenga ese _id.
  3. Indícale al navegador que olvide la cookie sid.
  4. Responde con el estado 204 y sin cuerpo. 204 Sin contenido significa que funcionó y que no hay nada que enviar. Responde con 204 incluso si la solicitud no tenía la cookie sid.

Pruébalo: Pulsa «Ejecutar». Envía POST /signup y después POST /login, ambos con {"username": "ada", "password": "correct horse"}. GET /me devuelve {"username": "ada"}. Envía POST /logout: estado 204 y cuerpo vacío. Envía GET /me otra vez: estado 401 y {"error": "Log in first"}. Abre Datos: sessions está vacío. Envía POST /logout una vez más: sigue respondiendo con 204.

Pruébalo tú mismo

import express from 'express';
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());

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 });
    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 };
    next();
}

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

// Cuenta cuántas veces este navegador ha solicitado GET /visit.
app.get('/visit', (req, res) => {
    const cookies = parseCookies(req.get('cookie'));
    const visits = Number(cookies.visits ?? 0) + 1;
    res.cookie('visits', String(visits));
    res.json({ visits });
});

// 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.listen(3000, () => {
    console.log('Vault is running on port 3000');
});

Todas las lecciones de Autenticación y seguridad

Practica por tu cuenta: Playground de Express