Menu
CoddyTech

توقيع الرموز

جزء من قسم المصادقة والأمان في رحلة 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'.

challenge icon

التحدي

سهل

التطبيقات التي تستدعي واجهة API الخاصة بك، مثل تطبيق الهاتف، لا تحتفظ بملفات تعريف الارتباط بالطريقة التي يفعلها المتصفح. أعطها رمزًا بدلًا من ذلك: JWT موقّعًا ترسله مع كل طلب. يُستخدم POST /token لتسجيل الدخول إلى تلك التطبيقات.

  1. في server.js، استورد SignJWT من jose بجانب decodeJwt. فوق المسارات، حوّل process.env.JWT_SECRET إلى بايتات باستخدام TextEncoder، كما يوضّح الدرس، لأن jose يوقّع باستخدام البايتات. يضبط هذا الدرس JWT_SECRET نيابةً عنك.
  2. أضف POST /token. يستقبل المحتوى نفسه الذي يستقبله تسجيل الدخول، {"username": "ada", "password": "correct horse"}، ويتحقق من اسم المستخدم وكلمة المرور بالطريقة نفسها التي يتحقق بها POST /login. إذا كانا غير صحيحين، فأرسل استجابة بالحالة 401 و{"error": "Wrong username or password"}.
  3. إذا كانا صحيحين، فوقّع رمزًا وأرسله في الاستجابة على هيئة {"token": "..."}. يجب أن:
    • يستخدم الخوارزمية HS256، وأن يُوقّع باستخدام بايتات JWT_SECRET؛
    • يتضمن المطالبة username في محتواه؛
    • يكون معرّف المستخدم _id نصًا في حقل الموضوع. استخدم toHexString()، لأن jose يرفض موضوعًا ليس نصًا؛
    • تنتهي صلاحيته بعد ساعة من إنشائه.

    يبدو الرمز من هذا الدرس هكذا: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkYSIsInN1YiI6IjY2ZjFjMmE5ZTRiMGExYjJjM2Q0ZTVmNiIsImlhdCI6MTc5MzM2MTYwMCwiZXhwIjoxNzkzMzY1MjAwfQ.EdCrjAKSjZowcjE3-IjKttBLVK1VKKbL9DPnJDLdgug

جرّب ذلك: اضغط «تشغيل». أرسل POST /signup مع {"username": "ada", "password": "correct horse"}. أرسل POST /token مع المحتوى نفسه: ستكون الحالة 200 وستتلقى {"token": "..."}. انسخ الرمز وأرسل GET /inspect?token= متبوعًا به. سترى شيئًا مثل {"username": "ada", "sub": "66f1c2a9e4b0a1b2c3d4e5f6", "iat": 1793361600, "exp": 1793365200}: تكون قيمة exp بعد iat بـ3600 ثانية. أرسل 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');
});

جميع دروس المصادقة والأمان

تدرّب بنفسك: Playground لـ Express