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.
Desafio
FácilEste 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.
- Em
server.js, emGET /notes/:id, encontre a nota usando juntos o_ide oowner. O proprietário deve ser o usuário conectado,req.user.id. - Faça o mesmo em
DELETE /notes/:id, para que ninguém possa excluir uma nota que não seja sua. - 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 /signupe, em seguida,POST /login, ambos com{"username": "ada", "password": "correct horse"}. - Envie
POST /notescom{"text": "Safe code 4417"}e copie o_idda resposta.GET /notes/seguido desse id retorna a nota. - Cadastre-se e faça login com
{"username": "bob", "password": "correct horse"}. EnvieGET /notes/seguido do id de ada: status 404 e{"error": "Note not found"}. EnvieDELETE /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
DELETEpara 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');
});
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
1Senhas
Bytes e texto hexadecimalDerivando uma chave de senhaUm novo saltNunca armazene senhasComparando bytes da chaveVerificando uma senhaRegras de cadastro4Proprietários e Segurança
Somente suas anotaçõesFunções de administradorPratique por conta própria: Playground de Express