Menu
CoddyTech

Sessões de login

Parte da seção Autenticação e segurança do Journey de Node.js da Coddy. Lição 10 de 24.

O login responde com 200, mas a próxima solicitação já esqueceu quem fez login. Uma sessão mantém essa informação. No login, o servidor cria um identificador que ninguém consegue adivinhar, armazena a qual usuário ele pertence e o envia como cookie. Cada solicitação posterior traz o identificador de volta, e o servidor o consulta.

O identificador precisa ser aleatório. Um cookie contendo o nome de usuário permitiria que qualquer pessoa digitasse Cookie: sid=ada e se passasse por Ada. randomBytes(24) gera 24 bytes aleatórios, ou seja, 192 bits, que ninguém consegue adivinhar. toString('base64url') os representa como 32 letras, dígitos, - e _, caracteres seguros em um cookie. Defina o cookie como httpOnly para que os scripts da página não possam lê-lo.

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

// em uma requisição posterior
const cart = await carts.findOne({ _id: parseCookies(req.get('cookie')).cart });
challenge icon

Desafio

Fácil

Esta etapa abre o projeto da API de notas privadas preparado para o curso, em vez dos arquivos do seu projeto de Lembrando com Cookies (incluindo server.js). Seu trabalho anterior continua na própria lição. Leia os arquivos fornecidos antes de editar server.js. Esta cópia contém as rotas de cadastro e login para notas privadas.

O login funciona, mas a próxima solicitação já esqueceu quem fez login. Faça o servidor se lembrar, com uma sessão: um id aleatório salvo no servidor e enviado ao navegador como um cookie.

  1. Em server.js, importe randomBytes de node:crypto. Importe sessions de ./db.js, junto com notes e users. O arquivo bloqueado db.js já tem uma coleção sessions.
  2. Em POST /login, depois que a senha for verificada, crie um id de sessão: 24 bytes aleatórios escritos como texto base64url. Isso corresponde a 32 caracteres entre letras, dígitos, - e _, como oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN. Crie um novo id a cada login.
  3. Salve a sessão em sessions como um documento com dois campos:
    • _id: o id da sessão.
    • userId: o _id do usuário que fez login.
  4. Envie o id ao navegador como um cookie chamado sid, com a opção httpOnly, para que os scripts da página não possam lê-lo. O login ainda responde com {"username": "ada"}.
  5. Adicione GET /me. Ele lê o cookie sid, encontra a sessão com esse _id e, em seguida, encontra o usuário dessa sessão. A resposta é {"username": "ada"}. Quando não há um cookie sid, ou nenhuma sessão tem esse id, a resposta tem status 401 e {"error": "Log in first"}. Quem digitar um cookie manualmente, como sid=ada, deve receber esse status 401.

Experimente: Pressione “Jogar”. Envie POST /signup com {"username": "ada", "password": "correct horse"}. Envie GET /me: status 401 e {"error": "Log in first"}. Envie POST /login com o mesmo corpo usado no cadastro: status 200. A aba Cabeçalhos da resposta mostra Set-Cookie: sid=...; Path=/; HttpOnly, com seu id aleatório no lugar dos pontos. Envie GET /me novamente. O navegador envia o cookie de volta, então você recebe {"username": "ada"}. Abra Dados: sessions contém um documento, com seu id como _id e o _id de ada como userId.

Experimente você mesmo

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

// Conta quantas vezes este navegador fez uma requisição 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 enquanto, qualquer pessoa pode ler e escrever notas.
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 as lições de Autenticação e segurança

Pratique por conta própria: Playground de Express