אימות אסימונים
חלק מהיחידה אימות ואבטחה במסלול ה-Node.js של Coddy. שיעור 11 מתוך 17.
לקוח API שולח את האסימון שלו בכותרת בכל בקשה: Authorization: Bearer <token>. קריאתו באמצעות decodeJwt הייתה מקבלת כל אסימון שמישהו הקליד. jwtVerify(token, key) מחשבת מחדש את החתימה באמצעות הסוד שלך וזורקת שגיאה כשהיא אינה תואמת, כשהאסימון פג תוקף או כשהוא כלל אינו JWT. רק אחרי שהיא מחזירה ערך, אפשר לבטוח ב-payload.
async function requireDevice(req, res, next) {
const token = (req.get('authorization') ?? '').replace(/^Bearer /, '');
try {
const { payload } = await jwtVerify(token, deviceKey);
req.device = payload.sub;
} catch {
return res.status(401).json({ error: 'Unknown device' });
}
next();
}אתגר
קל- כתבו
requireUser: אמתו את אסימוןAuthorization: BearerבאמצעותjwtVerifyוהגדירו אתreq.user = { id, username }מתוך הטענות שלו, או השיבו 401 עם{ "error": "Log in first" }. GET /meמשתמש בו ומשיב אתreq.user.
שלחו GET /me ללא אסימון. קבלו אחד באמצעות POST /token, הוסיפו את הכותרת Authorization: Bearer <token> ושלחו שוב GET /me. לאחר מכן שנו אות אחת באסימון ושלחו אותו פעם נוספת.
נסו בעצמכם
import express from 'express';
import { SignJWT, decodeJwt } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const app = express();
app.use(express.json());
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' });
}
res.json({ username: user.username });
});
app.post('/token', 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 token = await new SignJWT({ username: user.username })
.setProtectedHeader({ alg: 'HS256' })
.setSubject(user._id.toHexString())
.setIssuedAt()
.setExpirationTime('1h')
.sign(secret);
res.json({ token });
});
// מציג את תוכן ה-token: GET /inspect?token=...
app.get('/inspect', (req, res) => {
try {
res.json(decodeJwt(String(req.query.token ?? '')));
} catch {
res.status(400).json({ error: 'Not a JWT' });
}
});
// דף הניהול. אין לתת אמון באף טענה לפני בדיקת החתימה שלה.
app.get('/admin', (req, res) => {
res.status(403).json({ error: 'Admins only' });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.