Menu
CoddyTech

Admin-Rollen

Teil des Abschnitts Authentifizierung und Sicherheit der Node.js-Journey von Coddy. Lektion 19 von 24.

GET /admin prüft nur, ob jemand angemeldet ist, daher wird es allen Mitgliedern angezeigt. Eine auf dem Benutzer gespeicherte Rolle, etwa 'member' oder 'admin', legt fest, was sie tun dürfen. Eine Middleware-Factory, also eine Funktion, die eine Middleware zurückgibt, prüft für jede Route eine Rolle:

function requirePlan(plan) {
    return (req, res, next) => {
        if (req.user.plan !== plan) {
            return res.status(402).json({ error: `Needs the ${plan} plan` });
        }
        next();
    };
}

app.get('/exports', requireUser, requirePlan('pro'), sendExports);

Eine Rolle wird immer vom Server festgelegt. Bei der Registrierung werden aus req.body nur die erwarteten Felder übernommen, daher erhält ein Besucher, der "role": "admin" sendet, dadurch keine zusätzlichen Berechtigungen.

challenge icon

Aufgabe

Einfach

GET /admin zeigt, wie viele Konten existieren, und derzeit kann jedes angemeldete Mitglied das sehen. Nur Administratoren sollten das können. Jeder Benutzer erhält eine vom Server festgelegte Rolle, und die Route überprüft sie.

  1. Speichere in server.js bei POST /signup jeden neuen Benutzer mit "role": "member". Übernimm die Rolle niemals aus dem Anforderungstext: Ein Besucher, der "role": "admin" sendet, wird trotzdem Mitglied.
  2. Schreibe eine Funktion requireRole(role), die eine Middleware zurückgibt. Diese Middleware überprüft req.user.role:
    • Wenn der Wert nicht role entspricht, antworte mit Status 403 und {"error": "Not allowed"}. 403 Forbidden bedeutet, dass der Server weiß, wer du bist, du dies aber nicht tun darfst. Bei 401 weiß der Server nicht, wer du bist.
    • Wenn der Wert role entspricht, rufe next() auf.
  3. Füge requireRole('admin') bei GET /admin nach requireUser ein. Die Funktion liest req.user, das nur von requireUser gesetzt wird.

Der Server erstellt beim Start bereits ein Administratorkonto. Sein Benutzername lautet admin, und sein Passwort ist admin-pass-for-tests, das aus der Umgebungsvariable ADMIN_PASSWORD stammt.

Probiere es aus: Drücke auf „Spielen“. Sende GET /admin: Status 401 und {"error": "Log in first"}. Nun versucht eine Benutzerin namens mallory, sich selbst zur Administratorin zu machen. Sende POST /signup mit {"username": "mallory", "password": "correct horse", "role": "admin"} und dann POST /login mit {"username": "mallory", "password": "correct horse"}. Sende GET /admin: Status 403 und {"error": "Not allowed"}, denn das zusätzliche Feld hat sie nicht zur Administratorin gemacht. Sende POST /login mit {"username": "admin", "password": "admin-pass-for-tests"} und dann GET /admin: {"users": 2}.

Probier es selbst

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

// Das einzige Admin-Konto. Sein Passwort stammt aus den Umgebungsvariablen.
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 });
});

// Ermittelt den Benutzer anhand des Session-Cookies oder antwortet mit 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();
});

// Deine Notizen als Webseite.
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>`);
});

// Notizen: Jeder Benutzer liest und schreibt nur seine eigenen.
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), owner: req.user.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), owner: req.user.id });
    if (!deletedCount) {
        return res.status(404).json({ error: 'Note not found' });
    }
    res.status(204).end();
});

// Für Admins: Wie viele Konten es gibt.
app.get('/admin', requireUser, async (req, res) => {
    res.json({ users: await users.countDocuments() });
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});

Alle Lektionen in Authentifizierung und Sicherheit

Übe selbstständig: Express-Playground