מהו JWT
חלק מהיחידה אימות ואבטחה במסלול ה-Node.js של Coddy. שיעור 14 מתוך 24.
בהפעלה נדרשת גישה למסד נתונים בכל בקשה. JWT (אסימון אינטרנט של JSON) מכיל בעצמו את העובדות: שלושה חלקים המחוברים בנקודות. כל חלק כתוב ב-base64url, שיטה לכתיבת בתים באמצעות אותיות, ספרות, - ו-_. הכותרת מציינת את האלגוריתם, המטען מכיל את ההצהרות (sub מציין של מי האסימון, ו-exp מציין מתי הוא פג), והחתימה מוכיחה שהשרת יצר אותו.
Base64url הוא קידוד, לא הצפנה. כל מי שמחזיק באסימון יכול לקרוא את המטען שלו:
const payload = token.split('.')[1];
JSON.parse(Buffer.from(payload, 'base64url').toString());
// { sub: '42', name: 'Lin', exp: 1767225600 }decodeJwt(token) מתוך jose, חבילת ה-JWT, עושה את אותו הדבר. היא קוראת את המטען בלי לבדוק את החתימה, וזורקת שגיאה כשהטקסט אינו JWT. לכן לעולם אל תכניסו סוד למטען, ולעולם אל תחליטו דבר על סמך הצהרה ש-decodeJwt החזירה לכם: כל אחד יכול לכתוב כל הצהרה.
אתגר
קלבפרק הזה נוסיף דרך שנייה להתחבר באמצעות אסימונים, עבור אפליקציות שפונות ל־API שלך. נתחיל מעותק קטן יותר של השרת, עם הרשמה והתחברות, אבל בלי הפעלות או פתקים. אסימון JWT נושא בתוכו את העובדות שלו, וכל אחד יכול לקרוא אותן. נבנה נתיב שמציג מה אסימון אומר, ונסגור דף ניהול שסומך על כל טענה של אסימון.
- ב־
server.js, כתובGET /inspect. הנתיב קורא אסימון ממחרוזת השאילתה, כמו ב־GET /inspect?token=eyJ..., ומחזיר את הטענות שבתוכו כ־JSON. קרא אותן באמצעותdecodeJwt, שכבר מיובאת. עבור אסימון שהמטען שלו הוא{"sub": "lin", "plan": "free"}, הנתיב מחזיר בדיוק{"sub": "lin", "plan": "free"}. decodeJwtזורקת שגיאה כשהטקסט אינו JWT. תפוס את השגיאה והחזר תגובה עם סטטוס 400 ועם{"error": "Not a JWT"}. לדוגמה, הבקשהGET /inspect?token=helloמקבלת את תגובת 400 הזאת.- בדוק את
GET /admin. הוא קורא את האסימון באמצעותdecodeJwtומציג סוד כשהאסימון אומר"admin": true. אבלdecodeJwtאף פעם לא בודקת את החתימה, ולכן כל אחד יכול להקליד אסימון שאומר"admin": true. השרת הזה עדיין לא יכול לבדוק חתימות. לכן הגדר אתGET /adminכך שיחזיר סטטוס 403 ואת{"error": "Admins only"}לכל בקשה, בלי קשר לאסימון שהוא מקבל. 403 Forbidden פירושו "אסור לך לראות את זה". בפרק 4 נפתח שוב דף ניהול, הפעם למנהלי מערכת אמיתיים בלבד.
נסה זאת: לחץ על Run. סרגל הבקשות מתחיל ב־GET /admin?token= ואחריו האסימון הזה: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtYWxsb3J5IiwiYWRtaW4iOnRydWV9.c2lnbmF0dXJl תוקף בשם mallory כתב אותו בעצמו, בלי מפתח סודי. שלח אותו לפני שתשנה משהו: גרסת ההתחלה מחזירה {"secret": "The vault key is 42"}. אחרי השינוי, לחץ שוב על Run ושלח את אותה בקשה: סטטוס 403 ו־{"error": "Admins only"}. שלח GET /inspect?token= ואחריו אותו אסימון: {"sub": "mallory", "admin": true}, שנקרא בלי מפתח. שלח GET /inspect?token=hello: סטטוס 400 ו־{"error": "Not a JWT"}.
נסו בעצמכם
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) => {
res.status(501).json({ error: 'Not written yet' });
});
// עמוד הניהול.
app.get('/admin', (req, res) => {
const claims = decodeJwt(String(req.query.token ?? ''));
if (claims.admin === true) {
return res.json({ secret: 'The vault key is 42' });
}
res.status(403).json({ error: 'Admins only' });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה אימות ואבטחה
תרגלו בעצמכם: Express אונליין