Menu
CoddyTech

Solo le tue note

Fa parte della sezione Autenticazione e sicurezza del percorso Node.js di Coddy. Lezione 18 di 24.

Aver effettuato l'accesso dimostra chi sei, non che una nota sia tua. GET /notes/:id trova una nota basandosi solo sul suo id, quindi chiunque abbia effettuato l'accesso e abbia un id, tramite un link condiviso o un log, può leggere quella nota. Verificare chi è il proprietario si chiama autorizzazione.

Inserisci il proprietario direttamente nella query, così il database non restituirà mai il documento di qualcun altro. Rispondi con 404, non con 403, quando non viene trovato: 403 rivelerebbe al visitatore che la nota esiste.

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

Ogni query che accede ai dati di un utente deve includere il filtro: letture, aggiornamenti ed eliminazioni.

challenge icon

Sfida

Facile

Questo capitolo riprende l'accesso alla sessione del capitolo 2 e il server è cresciuto. Ora ha una pagina su GET /, route per una nota su GET /notes/:id e DELETE /notes/:id e un account amministratore. Le due route /notes/:id presentano una falla: trovano una nota basandosi solo sul suo id. Qualsiasi utente che abbia effettuato l'accesso e conosca un id può leggere o eliminare la nota di qualcun altro. Risolvi il problema.

  1. In server.js, in GET /notes/:id, trova la nota usando insieme il suo _id e il suo owner. Il proprietario deve essere l'utente che ha effettuato l'accesso, req.user.id.
  2. Fai lo stesso in DELETE /notes/:id, così nessuno potrà eliminare una nota che non gli appartiene.
  3. Quando non corrisponde nessuna nota, entrambe le route rispondono già con lo stato 404 e {"error": "Note not found"}. Mantieni questa risposta anche per la nota di qualcun altro. Una risposta 403 rivelerebbe al visitatore che la nota esiste.

Prova: premi «Esegui».

  • Invia POST /signup, poi POST /login, entrambi con {"username": "ada", "password": "correct horse"}.
  • Invia POST /notes con {"text": "Safe code 4417"} e copia il _id dalla risposta. GET /notes/ seguito da quell'id restituisce la nota.
  • Registrati ed effettua l'accesso con {"username": "bob", "password": "correct horse"}. Invia GET /notes/ seguito dall'id di ada: stato 404 e {"error": "Note not found"}. Invia DELETE /notes/ seguito dallo stesso id: anche in questo caso, 404.
  • Accedi di nuovo come ada, usando i dati del primo passaggio. La sua nota è ancora lì. Ora esegui DELETE su di essa: stato 204.

Provalo tu

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

// L'unico account admin. La sua password proviene dall'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 });
});

// Trova l'utente associato al cookie di sessione oppure risponde 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, 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();
});

// Le tue note in una pagina.
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>`);
});

// Note: ogni utente legge e scrive solo le proprie.
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();
});

// Per gli amministratori: quanti account ci sono.
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 iconMettiti alla prova

Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.

Tutte le lezioni di Autenticazione e sicurezza

Esercitati da solo: Playground Express