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"}.

جرّب ذلك: اضغط «تشغيل». أرسل 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اختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

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

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