Menu
CoddyTech

Xác minh token

Thuộc phần Xác thực và bảo mật trong hành trình Node.js của Coddy. Bài học 16 trên 24.

Một client API gửi token trong header ở mỗi request: Authorization: Bearer <token>. Đọc token bằng decodeJwt sẽ chấp nhận bất kỳ token nào mà bất kỳ ai nhập vào. jwtVerify(token, key) tính lại chữ ký bằng secret của bạn và phát sinh lỗi nếu chữ ký không khớp, token đã hết hạn hoặc token không phải JWT. Bạn chỉ có thể tin cậy payload sau khi hàm này trả về.

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

Thử thách

Dễ

Bây giờ hãy chấp nhận các token mà bạn ký. Ứng dụng gửi token trong header ở mỗi yêu cầu, còn máy chủ kiểm tra chữ ký trước khi tin bất cứ nội dung nào bên trong.

  1. Trong server.js, nhập jwtVerify từ jose.
  2. Viết middleware requireUser(req, res, next). Middleware này đọc header Authorization. Header đó chứa từ Bearer, một dấu cách, rồi đến token. Lấy token ra khỏi header.
  3. Kiểm tra token bằng jwtVerify, sử dụng cùng khóa mà bạn dùng để ký. jwtVerify sẽ báo lỗi khi chữ ký không khớp, khi token hết hạn hoặc khi văn bản hoàn toàn không phải token. Trong tất cả các trường hợp đó, cũng như khi không có header, hãy trả về trạng thái 401 và {"error": "Log in first"}.
  4. Khi token hợp lệ, gán cho req.user một đối tượng có id, lấy từ claim sub của token, và username, lấy từ claim username của token. Sau đó gọi next().
  5. Thêm GET /me với requireUser. Endpoint này trả về req.user: {"id": "66f1c2a9e4b0a1b2c3d4e5f6", "username": "ada"}.

Thử nhé: Nhấn Chạy. Gửi GET /me: trạng thái 401 và {"error": "Log in first"}. Gửi POST /signup, sau đó gửi POST /token, cả hai đều dùng {"username": "ada", "password": "correct horse"}, rồi sao chép token. Trong trường Headers, thêm header có tên Authorization với giá trị gồm Bearer, một dấu cách và token của bạn. Gửi GET /me: trạng thái 200, kèm id của bạn và "username": "ada". Bây giờ, thay đổi một chữ cái ở phần giữa của token, nằm giữa hai dấu chấm, rồi gửi lại GET /me: trạng thái 401 vì chữ ký không còn khớp.

Tự làm thử

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

// Hiển thị nội dung của 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' });
    }
});

// Trang quản trị. Không thể tin bất kỳ claim nào trước khi kiểm tra chữ ký của nó.
app.get('/admin', (req, res) => {
    res.status(403).json({ error: 'Admins only' });
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Xác thực và bảo mật

Tự luyện tập: Playground Express