Menu
CoddyTech

Menandatangani Token

Termasuk bagian Autentikasi dan Keamanan dalam perjalanan Node.js di Coddy. Pelajaran 15 dari 24.

Sebuah token hanya bernilai jika tidak ada orang lain yang bisa membuatnya. Dengan HS256, jose mencampur header dan payload dengan rahasia yang hanya diketahui server, lalu melakukan hash pada hasilnya. Hash itu adalah tanda tangan. Tanpa rahasia tersebut, tidak ada yang bisa membuat token dengan tanda tangan yang cocok.

Rahasia berasal dari variabel lingkungan, process.env.JWT_SECRET, bukan dari kode: kode dibagikan, disalin, dan di-commit. Pelajaran ini menetapkan JWT_SECRET untukmu. jose menginginkannya dalam bentuk 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 menerima teks, dan setExpirationTime menerima rentang waktu seperti '30m' atau '2d'.

challenge icon

Tantangan

Mudah

Aplikasi yang memanggil API-mu, seperti aplikasi ponsel, tidak menyimpan cookie seperti yang dilakukan browser. Berikan token sebagai gantinya: JWT bertanda tangan yang dikirim kembali bersama setiap permintaan. POST /token adalah cara login untuk aplikasi tersebut.

  1. Di server.js, impor SignJWT dari jose bersama decodeJwt. Sebelum rute, ubah process.env.JWT_SECRET menjadi byte dengan TextEncoder, seperti yang ditunjukkan pelajaran ini, karena jose menandatangani dengan byte. Pelajaran ini menetapkan JWT_SECRET untukmu.
  2. Tambahkan POST /token. Endpoint ini menerima body yang sama seperti login, {"username": "ada", "password": "correct horse"}, lalu memeriksa nama pengguna dan kata sandi dengan cara yang sama seperti POST /login. Jika keduanya salah, kirim respons dengan status 401 dan {"error": "Wrong username or password"}.
  3. Jika keduanya benar, tanda tangani token dan kirim respons yang berisi token tersebut sebagai {"token": "..."}. Token harus:
    • menggunakan algoritma HS256, ditandatangani dengan byte JWT_SECRET;
    • menyimpan klaim username di payload-nya;
    • menggunakan _id pengguna dalam bentuk teks sebagai subject-nya. Gunakan toHexString(), karena jose menolak subject yang bukan teks;
    • kedaluwarsa satu jam setelah dibuat.

    Token dari pelajaran ini terlihat seperti ini: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkYSIsInN1YiI6IjY2ZjFjMmE5ZTRiMGExYjJjM2Q0ZTVmNiIsImlhdCI6MTc5MzM2MTYwMCwiZXhwIjoxNzkzMzY1MjAwfQ.EdCrjAKSjZowcjE3-IjKttBLVK1VKKbL9DPnJDLdgug

Coba: Tekan Jalankan. Kirim POST /signup dengan {"username": "ada", "password": "correct horse"}. Kirim POST /token dengan body yang sama: status 200 dan {"token": "..."}. Salin token tersebut lalu kirim GET /inspect?token= diikuti token itu. Kamu akan melihat sesuatu seperti {"username": "ada", "sub": "66f1c2a9e4b0a1b2c3d4e5f6", "iat": 1793361600, "exp": 1793365200}: exp adalah 3600 detik setelah iat. Kirim POST /token dengan {"username": "ada", "password": "wrong horse"}: status 401.

Coba sendiri

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

// Menampilkan isi 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' });
    }
});

// Halaman admin. Jangan percaya klaim apa pun sebelum tanda tangannya diperiksa.
app.get('/admin', (req, res) => {
    res.status(403).json({ error: 'Admins only' });
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});

Semua pelajaran di Autentikasi dan Keamanan

Latihan sendiri: Playground Express