Menu
CoddyTech

Somente suas anotações

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

Estar conectado comprova quem você é, não que uma nota pertence a você. GET /notes/:id encontra uma nota apenas pelo seu id, então qualquer pessoa conectada que tenha um id, por meio de um link compartilhado ou de um registro, consegue ler essa nota. Verificar quem é o proprietário é chamado de autorização.

Inclua o proprietário na própria consulta, para que o banco de dados nunca retorne o documento de outra pessoa. Responda com 404, não com 403, quando não for encontrado: 403 informaria ao visitante que a 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 acessa os dados de um usuário precisa do filtro: leituras, atualizações e exclusões.

challenge icon

Desafio

Fácil

Este capítulo retoma o login com sessão do capítulo 2, e o servidor cresceu. Agora ele tem uma página em GET /, rotas para uma nota em GET /notes/:id e DELETE /notes/:id, além de uma conta de administrador. As duas rotas /notes/:id têm uma falha: elas encontram uma nota apenas pelo id. Qualquer usuário conectado que saiba um id pode ler ou excluir a nota de outra pessoa. Corrija isso.

  1. Em server.js, em GET /notes/:id, encontre a nota usando juntos o _id e o owner. O proprietário deve ser o usuário conectado, req.user.id.
  2. Faça o mesmo em DELETE /notes/:id, para que ninguém possa excluir uma nota que não seja sua.
  3. Quando nenhuma nota corresponder, as duas rotas já respondem com o status 404 e {"error": "Note not found"}. Mantenha essa resposta também para a nota de outra pessoa. Um 403 informaria ao visitante que a nota existe.

Teste: Pressione Executar.

  • Envie POST /signup e, em seguida, POST /login, ambos com {"username": "ada", "password": "correct horse"}.
  • Envie POST /notes com {"text": "Safe code 4417"} e copie o _id da resposta. GET /notes/ seguido desse id retorna a nota.
  • Cadastre-se e faça login com {"username": "bob", "password": "correct horse"}. Envie GET /notes/ seguido do id de ada: status 404 e {"error": "Note not found"}. Envie DELETE /notes/ seguido do mesmo id: também 404.
  • Faça login como ada novamente, com o corpo dela da primeira etapa. A nota dela ainda estará lá. Use DELETE para excluí-la agora: status 204.

Experimente você mesmo

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

// A única conta de administrador. Sua senha vem do ambiente.
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 });
});

// Encontra o usuário associado ao cookie de sessão ou responde com 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();
});

// Suas notas em uma 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 usuário lê e escreve apenas as suas próprias.
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: quantas contas existem.
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 iconTeste seus conhecimentos

Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.

Todas as lições de Autenticação e segurança

Pratique por conta própria: Playground de Express