Menu
CoddyTech

Ký 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 15 trên 24.

Một token chỉ có giá trị nếu không ai khác có thể tạo ra token đó. Với HS256, jose kết hợp header và payload với một bí mật chỉ máy chủ biết, rồi băm kết quả. Mã băm đó là chữ ký. Không có bí mật này, không ai có thể tạo ra chữ ký khớp.

Bí mật này lấy từ biến môi trường, process.env.JWT_SECRET, không lấy từ code: code được chia sẻ, sao chép và commit. Bài học này thiết lập JWT_SECRET cho bạn. jose cần giá trị này ở dạng byte.

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 nhận văn bản, còn setExpirationTime nhận khoảng thời gian như '30m' hoặc '2d'.

challenge icon

Thử thách

Dễ

Các ứng dụng gọi API của bạn, chẳng hạn như ứng dụng trên điện thoại, không lưu cookie như trình duyệt. Thay vào đó, hãy cấp cho chúng một token: một JWT đã ký mà chúng gửi lại cùng mỗi yêu cầu. POST /token là cách đăng nhập dành cho các ứng dụng đó.

  1. Trong server.js, nhập SignJWT từ jose cùng với decodeJwt. Phía trên các route, chuyển process.env.JWT_SECRET thành byte bằng TextEncoder, như bài học hướng dẫn, vì jose ký bằng byte. Bài học này đã thiết lập JWT_SECRET cho bạn.
  2. Thêm POST /token. Route này nhận cùng phần thân như đăng nhập, {"username": "ada", "password": "correct horse"}, rồi kiểm tra tên người dùng và mật khẩu giống như POST /login. Nếu thông tin không đúng, hãy trả về trạng thái 401 cùng {"error": "Wrong username or password"}.
  3. Nếu thông tin đúng, hãy ký một token và trả về token đó dưới dạng {"token": "..."}. Token phải:
    • dùng thuật toán HS256, được ký bằng các byte của JWT_SECRET;
    • chứa claim username trong payload;
    • dùng _id của người dùng dưới dạng văn bản làm subject. Hãy dùng toHexString(), vì jose từ chối subject không phải văn bản;
    • hết hạn sau khi được tạo một giờ.

    Token trong bài học này có dạng như sau: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkYSIsInN1YiI6IjY2ZjFjMmE5ZTRiMGExYjJjM2Q0ZTVmNiIsImlhdCI6MTc5MzM2MTYwMCwiZXhwIjoxNzkzMzY1MjAwfQ.EdCrjAKSjZowcjE3-IjKttBLVK1VKKbL9DPnJDLdgug

Thử nhé: Nhấn Run. Gửi POST /signup với {"username": "ada", "password": "correct horse"}. Gửi POST /token với cùng phần thân: trạng thái 200 và {"token": "..."}. Sao chép token rồi gửi GET /inspect?token=, theo sau là token đó. Bạn sẽ thấy nội dung tương tự như {"username": "ada", "sub": "66f1c2a9e4b0a1b2c3d4e5f6", "iat": 1793361600, "exp": 1793365200}: exp muộn hơn iat 3600 giây. Gửi POST /token với {"username": "ada", "password": "wrong horse"}: trạng thái 401.

Tự làm thử

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

// 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ỳ thông tin nào trong token 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');
});

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

Tự luyện tập: Playground Express