Menu
CoddyTech

انتهاء صلاحية الرمز

جزء من قسم المصادقة والأمان في رحلة Node.js على Coddy. الدرس 12 من 17.

لا يستطيع الخادم استرجاع الرمز: فلا شيء مخزّن، لذا يظل الرمز المسروق صالحًا حتى انتهاء صلاحيته. اجعل مدة صلاحية الرموز قصيرة، بالدقائق لا بالأيام، ودع العميل يطلب رمزًا جديدًا.

يرفض jwtVerify بالفعل أي رمز تجاوزت صلاحية exp فيه. ويطلق errors.JWTExpired، لذا يمكنك تمييز الرمز منتهي الصلاحية عن الرمز المزوّر وإبلاغ المستخدم بذلك: فالعميل الذي يقرأ انتهت صلاحية الرمز يعرف أن عليه تسجيل الدخول مجددًا.

import { errors, jwtVerify } from 'jose';

try {
    await jwtVerify(inviteToken, inviteKey);
} catch (err) {
    if (err instanceof errors.JWTExpired) {
        return res.status(410).json({ error: 'This invite has run out' });
    }
    return res.status(400).json({ error: 'Not a real invite' });
}
challenge icon

التحدي

سهل
  • تنتهي صلاحية الرموز الصادرة من POST /token بعد 15 دقيقة.
  • يردّ الرمز المنتهي الصلاحية بالرمز 401 { "error": "Token expired" }.
  • أما كل رمز آخر غير صالح أو مفقود، فيظلّ الردّ عليه بالرمز 401 { "error": "Log in first" }.

احصل على رمز واقرأ قيمة exp فيه باستخدام GET /inspect.

جرّب بنفسك

import express from 'express';
import { SignJWT, decodeJwt, jwtVerify } 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 });
});

// يتحقق من الرمز في ترويسة Authorization، أو يردّ بالرمز 401.
async function requireUser(req, res, next) {
    const token = (req.get('authorization') ?? '').replace(/^Bearer /, '');
    let payload;
    try {
        ({ payload } = await jwtVerify(token, secret));
    } catch {
        return res.status(401).json({ error: 'Log in first' });
    }
    req.user = { id: payload.sub, username: payload.username };
    next();
}

app.get('/me', requireUser, (req, res) => {
    res.json(req.user);
});

// يعرض محتوى الرمز: 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');
});

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