Menu
CoddyTech

אימות אסימונים

חלק מהיחידה אימות ואבטחה במסלול ה-Node.js של Coddy. שיעור 16 מתוך 24.

לקוח 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();
}
challenge icon

אתגר

קל

עכשיו נקבל את האסימונים שעליהם חתמת. אפליקציה שולחת את האסימון שלה בכותרת בכל בקשה, והשרת בודק את החתימה לפני שהוא סומך על תוכן כלשהו שבתוכה.

  1. ב־server.js, ייבא את jwtVerify מתוך jose.
  2. כתוב תוכנת ביניים בשם requireUser(req, res, next). היא קוראת את הכותרת Authorization. הכותרת מכילה את המילה Bearer, רווח, ואז את האסימון. חלץ ממנה את האסימון.
  3. בדוק את האסימון באמצעות jwtVerify, עם אותו מפתח שבו אתה משתמש לחתימה. jwtVerify זורקת שגיאה כשהחתימה אינה תואמת, כשהאסימון פג תוקף או כשהטקסט אינו אסימון כלל. בכל המקרים האלה, וגם כשאין כותרת, החזר תגובה עם סטטוס 401 ועם {"error": "Log in first"}.
  4. כשהאסימון תקף, הגדר את req.user כאובייקט עם id, שנלקח מהתביעה sub שבאסימון, ועם username, שנלקח מהתביעה username שבו. לאחר מכן קרא ל־next().
  5. הוסף את GET /me עם requireUser. הוא מחזיר את req.user: {"id": "66f1c2a9e4b0a1b2c3d4e5f6", "username": "ada"}.

נסה זאת: לחץ על Run. שלח GET /me: סטטוס 401 ו־{"error": "Log in first"}. שלח POST /signup, ואז POST /token, בשניהם עם {"username": "ada", "password": "correct horse"}, והעתק את האסימון. בשדה הכותרות, הוסף כותרת בשם Authorization, שהערך שלה הוא Bearer, רווח והאסימון שלך. שלח GET /me: סטטוס 200, עם המזהה שלך ועם "username": "ada". עכשיו שנה אות אחת בחלק האמצעי של האסימון, בין שתי הנקודות, ושלח שוב GET /me: סטטוס 401, כי החתימה כבר אינה תואמת.

נסו בעצמכם

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 });
});

// מציג את תוכן הטוקן: 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');
});
quiz iconבחנו את עצמכם

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

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

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