Menu
CoddyTech

Sesiones de inicio de sesión

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

El inicio de sesión responde 200, pero la siguiente solicitud ha olvidado quién inició sesión. Una sesión lo recuerda. Al iniciar sesión, el servidor crea un id que nadie puede adivinar, guarda a qué usuario pertenece y lo envía como una cookie. Cada solicitud posterior devuelve el id, y el servidor lo busca.

El id debe ser aleatorio. Una cookie que contenga el nombre de usuario permitiría que cualquiera escribiera Cookie: sid=ada y se hiciera pasar por Ada. randomBytes(24) proporciona 24 bytes aleatorios, 192 bits, que nadie puede adivinar. toString('base64url') los escribe como 32 letras, dígitos, - y _, caracteres que son seguros en una cookie. Configura la cookie como httpOnly para que los scripts de la página no puedan leerla.

const cartId = randomBytes(24).toString('base64url');
await carts.insertOne({ _id: cartId, items: [] });
res.cookie('cart', cartId, { httpOnly: true });

// en una solicitud posterior
const cart = await carts.findOne({ _id: parseCookies(req.get('cookie')).cart });
challenge icon

Desafío

Fácil

Este paso abre el proyecto de API de notas privadas preparado para el curso, en lugar de los archivos de tu proyecto de Recordar con cookies (incluido server.js). Tu trabajo anterior sigue en su propia lección. Lee los archivos proporcionados antes de editar server.js. Esta copia contiene las rutas de registro e inicio de sesión para las notas privadas.

El inicio de sesión funciona, pero la siguiente solicitud ya ha olvidado quién inició sesión. Haz que el servidor lo recuerde mediante una sesión: un id aleatorio guardado en el servidor y enviado al navegador como una cookie.

  1. En server.js, importa randomBytes desde node:crypto. Importa sessions desde ./db.js junto a notes y users. El archivo bloqueado db.js ya tiene una colección sessions.
  2. En POST /login, después de comprobar la contraseña, crea un id de sesión: 24 bytes aleatorios escritos como texto base64url. Son 32 caracteres: letras, dígitos, - y _, como oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN. Crea un id nuevo en cada inicio de sesión.
  3. Guarda la sesión en sessions como un documento con dos campos:
    • _id: el id de sesión.
    • userId: el _id del usuario que inició sesión.
  4. Envía el id al navegador como una cookie llamada sid, con la opción httpOnly, para que los scripts de la página no puedan leerla. El inicio de sesión sigue respondiendo con {"username": "ada"}.
  5. Añade GET /me. Lee la cookie sid, busca la sesión con ese _id y, después, busca al usuario de esa sesión. Responde con {"username": "ada"}. Si no hay una cookie sid o ninguna sesión tiene ese id, responde con el estado 401 y {"error": "Log in first"}. Quien escriba una cookie a mano, como sid=ada, debe recibir ese 401.

Pruébalo: Pulsa Run. Envía POST /signup con {"username": "ada", "password": "correct horse"}. Envía GET /me: estado 401 y {"error": "Log in first"}. Envía POST /login con el mismo cuerpo que en el registro: estado 200. La pestaña Headers de la respuesta muestra Set-Cookie: sid=...; Path=/; HttpOnly, con tu id aleatorio en lugar de los puntos. Vuelve a enviar GET /me. El navegador envía la cookie de vuelta, así que recibes {"username": "ada"}. Abre Data: sessions contiene un documento, con tu id como su _id y el _id de ada como su userId.

Pruébalo tú mismo

import express from 'express';
import { parseCookies } from './cookies.js';
import { notes, 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' });
    }
    res.json({ username: user.username });
});

// Cuenta cuántas veces este navegador ha llamado a 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. Por ahora, cualquiera puede leerlas y escribirlas.
app.get('/notes', async (req, res) => {
    res.json(await notes.find().toArray());
});

app.post('/notes', async (req, res) => {
    const note = { 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