Rôles d’administration
Fait partie de la section Authentification et sécurité du Journey Node.js de Coddy. Leçon 19 sur 24.
GET /admin vérifie uniquement que quelqu’un est connecté, donc tous les membres peuvent y accéder. Un rôle enregistré sur l’utilisateur, comme 'member' ou 'admin', indique ce qu’il peut faire. Une fabrique de middleware, c’est-à-dire une fonction qui renvoie un middleware, vérifie un rôle pour chaque route :
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);Le serveur définit toujours le rôle. Lors de l’inscription, seuls les champs attendus sont copiés depuis req.body, donc un visiteur qui envoie "role": "admin" n’obtient rien de cette façon.
Défi
FacileGET /admin indique combien de comptes existent, et actuellement, tous les membres connectés peuvent le voir. Seuls les administrateurs devraient y avoir accès. Chaque utilisateur reçoit un rôle défini par le serveur, et la route le vérifie.
- Dans
server.js, dansPOST /signup, enregistrez chaque nouvel utilisateur avec"role": "member". Ne récupérez jamais le rôle dans le corps de la requête : un visiteur qui envoie"role": "admin"devient quand même un membre. - Écrivez une fonction
requireRole(role)qui renvoie un middleware. Ce middleware vérifiereq.user.role:- Si la valeur n’est pas
role, répondez avec le statut 403 et{"error": "Not allowed"}. 403 Forbidden signifie que le serveur sait qui vous êtes, mais que vous n’avez pas le droit de faire cela. 401 signifie qu’il ne sait pas qui vous êtes. - Si la valeur est
role, appeleznext().
- Si la valeur n’est pas
- Ajoutez
requireRole('admin')àGET /admin, aprèsrequireUser. Cette route litreq.user, que seulrequireUserdéfinit.
Le serveur crée déjà un compte administrateur au démarrage. Son nom d’utilisateur est admin et son mot de passe est admin-pass-for-tests, qui provient de la variable d’environnement ADMIN_PASSWORD.
Essayez : appuyez sur Run. Envoyez GET /admin : statut 401 et {"error": "Log in first"}. Maintenant, une utilisatrice appelée mallory essaie de se donner le statut d’administratrice. Envoyez POST /signup avec {"username": "mallory", "password": "correct horse", "role": "admin"}, puis POST /login avec {"username": "mallory", "password": "correct horse"}. Envoyez GET /admin : statut 403 et {"error": "Not allowed"}, car le champ supplémentaire ne lui a pas conféré le statut d’administratrice. Envoyez POST /login avec {"username": "admin", "password": "admin-pass-for-tests"}, puis GET /admin : {"users": 2}.
Essayez vous-même
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());
// Le seul compte administrateur. Son mot de passe provient de l’environnement.
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 });
});
// Recherche l’utilisateur associé au cookie de session ou renvoie 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();
});
// Vos notes sous forme de page.
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>`);
});
// Notes : chaque utilisateur ne lit et n’écrit que les siennes.
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();
});
// Pour les administrateurs : le nombre de comptes existants.
app.get('/admin', requireUser, async (req, res) => {
res.json({ users: await users.countDocuments() });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
Toutes les leçons de Authentification et sécurité
1Mots de passe
Octets et texte hexadécimalDériver une clé de mot de passeUn nouveau selNe stockez jamais les mots de passeComparer les octets de cléVérifier un mot de passeRègles d’inscription4Propriétaires et sécurité
Vos notes uniquementRôles d’administrationEntraînez-vous par vous-même : Playground Express