Menu
CoddyTech

מהו 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 החזירה לכם: כל אחד יכול לכתוב כל הצהרה.

challenge icon

אתגר

קל

בפרק הזה נוסיף דרך שנייה להתחבר באמצעות אסימונים, עבור אפליקציות שפונות ל־API שלך. נתחיל מעותק קטן יותר של השרת, עם הרשמה והתחברות, אבל בלי הפעלות או פתקים. אסימון JWT נושא בתוכו את העובדות שלו, וכל אחד יכול לקרוא אותן. נבנה נתיב שמציג מה אסימון אומר, ונסגור דף ניהול שסומך על כל טענה של אסימון.

  1. ב־server.js, כתוב GET /inspect. הנתיב קורא אסימון ממחרוזת השאילתה, כמו ב־GET /inspect?token=eyJ..., ומחזיר את הטענות שבתוכו כ־JSON. קרא אותן באמצעות decodeJwt, שכבר מיובאת. עבור אסימון שהמטען שלו הוא {"sub": "lin", "plan": "free"}, הנתיב מחזיר בדיוק {"sub": "lin", "plan": "free"}.
  2. decodeJwt זורקת שגיאה כשהטקסט אינו JWT. תפוס את השגיאה והחזר תגובה עם סטטוס 400 ועם {"error": "Not a JWT"}. לדוגמה, הבקשה GET /inspect?token=hello מקבלת את תגובת 400 הזאת.
  3. בדוק את 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');
});
quiz iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה אימות ואבטחה

תרגלו בעצמכם: Express אונליין