חתימה על אסימונים
חלק מהיחידה אימות ואבטחה במסלול ה-Node.js של Coddy. שיעור 15 מתוך 24.
לאסימון יש ערך רק אם אף אחד אחר לא יכול ליצור אחד. עם HS256, jose מערבבת את הכותרת ואת המטען עם סוד שרק השרת מכיר, ומחשבת גיבוב של התוצאה. הגיבוב הזה הוא החתימה. בלי הסוד, אף אחד לא יכול ליצור חתימה תואמת.
הסוד מגיע ממשתנה סביבה, process.env.JWT_SECRET, ולא מהקוד: הקוד משותף, מועתק ונשמר במאגר. השיעור הזה מגדיר עבורך את JWT_SECRET. jose מצפה לקבל אותו בתור בתים.
const key = new TextEncoder().encode(process.env.INVITE_SECRET);
const invite = await new SignJWT({ room: 'design' })
.setProtectedHeader({ alg: 'HS256' })
.setSubject(guestId)
.setIssuedAt()
.setExpirationTime('2d')
.sign(key);setSubject מקבלת טקסט, ו-setExpirationTime מקבלת פרק זמן כמו '30m' או '2d'.
אתגר
קלאפליקציות שקוראות ל-API שלך, כמו אפליקציה לטלפון, לא שומרות עוגיות כמו דפדפן. במקום זאת, תן להן אסימון: JWT חתום שהן שולחות בחזרה עם כל בקשה. POST /token הוא נתיב ההתחברות של האפליקציות האלה.
- ב-
server.js, ייבא אתSignJWTמ-joseלצדdecodeJwt. מעל הנתיבים, המר אתprocess.env.JWT_SECRETלבתים באמצעותTextEncoder, כפי שמוצג בשיעור, כיjoseחותם באמצעות בתים. השיעור הזה מגדיר עבורך אתJWT_SECRET. - הוסף את
POST /token. הוא מקבל את אותו גוף בקשה כמו נתיב ההתחברות,{"username": "ada", "password": "correct horse"}, ובודק את שם המשתמש והסיסמה באותו אופן שבוPOST /loginעושה זאת. אם הם שגויים, החזר תגובה עם סטטוס 401 ועם{"error": "Wrong username or password"}. - אם הם נכונים, חתום על אסימון והחזר אותו כך:
{"token": "..."}. האסימון חייב:- להשתמש באלגוריתם HS256, ולחתום באמצעות הבתים של
JWT_SECRET; - להכיל את התביעה
usernameבמטען שלו; - להשתמש ב-
_idשל המשתמש כמחרוזת בתור הנושא שלו. השתמש ב-toHexString(), כיjoseלא מקבל נושא שאינו מחרוזת; - לפוג שעה אחת לאחר יצירתו.
אסימון מהשיעור הזה נראה כך:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkYSIsInN1YiI6IjY2ZjFjMmE5ZTRiMGExYjJjM2Q0ZTVmNiIsImlhdCI6MTc5MzM2MTYwMCwiZXhwIjoxNzkzMzY1MjAwfQ.EdCrjAKSjZowcjE3-IjKttBLVK1VKKbL9DPnJDLdgug - להשתמש באלגוריתם HS256, ולחתום באמצעות הבתים של
נסה זאת: לחץ על Run. שלח POST /signup עם {"username": "ada", "password": "correct horse"}. שלח POST /token עם אותו גוף בקשה: סטטוס 200 ו-{"token": "..."}. העתק את האסימון ושלח GET /inspect?token= ואחריו האסימון. תראה משהו כמו {"username": "ada", "sub": "66f1c2a9e4b0a1b2c3d4e5f6", "iat": 1793361600, "exp": 1793365200}: exp הוא 3600 שניות אחרי iat. שלח POST /token עם {"username": "ada", "password": "wrong horse"}: סטטוס 401.
נסו בעצמכם
import express from 'express';
import { decodeJwt } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
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 });
});
// מציג את תוכן הטוקן: 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');
});
כל השיעורים ביחידה אימות ואבטחה
תרגלו בעצמכם: Express אונליין