Menu
CoddyTech

Apa Itu JWT

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

Sebuah sesi memerlukan pencarian di database pada setiap permintaan. JWT (JSON Web Token) membawa informasinya sendiri: tiga bagian yang dipisahkan oleh titik. Setiap bagian ditulis dalam base64url, cara menuliskan byte menggunakan huruf, angka, -, dan _. Header menyebutkan algoritmanya, payload memuat klaim (sub untuk menunjukkan token milik siapa, exp untuk menunjukkan kapan token kedaluwarsa), dan tanda tangan membuktikan bahwa server yang membuatnya.

Base64url adalah pengodean, bukan enkripsi. Siapa pun yang memegang token bisa membaca payload-nya:

const payload = token.split('.')[1];
JSON.parse(Buffer.from(payload, 'base64url').toString());
// { sub: '42', name: 'Lin', exp: 1767225600 }

decodeJwt(token) dari jose, paket JWT, melakukan hal yang sama. Fungsi ini membaca payload tanpa memeriksa tanda tangan, dan menimbulkan error saat teksnya bukan JWT. Jadi, jangan pernah menyimpan rahasia dalam payload, dan jangan pernah mengambil keputusan berdasarkan klaim yang diberikan oleh decodeJwt: siapa pun bisa menulis klaim apa pun.

challenge icon

Tantangan

Mudah

Bab ini menambahkan cara kedua untuk masuk, menggunakan token, bagi aplikasi yang memanggil API-mu. Bab ini dimulai dari salinan server yang lebih kecil, dengan pendaftaran dan login, tetapi tanpa sesi atau catatan. JWT membawa fakta di dalamnya, dan siapa pun bisa membacanya. Buat rute yang menampilkan isi token, dan tutup halaman admin yang memercayai apa pun yang diklaim token.

  1. Di server.js, tulis GET /inspect. Rute ini membaca token dari string kueri, seperti pada GET /inspect?token=eyJ..., lalu mengembalikan klaim di dalamnya sebagai JSON. Baca klaim tersebut dengan decodeJwt, yang sudah diimpor. Untuk token dengan payload {"sub": "lin", "plan": "free"}, rute ini mengembalikan tepat {"sub": "lin", "plan": "free"}.
  2. decodeJwt melempar error saat teks tersebut bukan JWT. Tangkap error itu dan berikan respons dengan status 400 dan {"error": "Not a JWT"}. Misalnya, GET /inspect?token=hello menghasilkan status 400 tersebut.
  3. Periksa GET /admin. Rute ini membaca token dengan decodeJwt dan menampilkan rahasia saat token menyatakan "admin": true. Namun, decodeJwt tidak pernah memeriksa tanda tangan, jadi siapa pun bisa menulis token yang menyatakan "admin": true. Server ini belum bisa memeriksa tanda tangan. Jadi, buat GET /admin memberikan respons dengan status 403 dan {"error": "Admins only"} untuk setiap permintaan, apa pun token yang diterimanya. 403 Forbidden berarti "kamu tidak boleh melihat ini". Bab 4 membuka halaman admin lagi, khusus untuk admin sungguhan.

Coba: Tekan tombol “Jalankan”. Bilah permintaan dimulai dengan GET /admin?token= diikuti token ini: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtYWxsb3J5IiwiYWRtaW4iOnRydWV9.c2lnbmF0dXJl Penyerang bernama mallory menulisnya sendiri, tanpa kunci rahasia. Kirim token itu sebelum kamu mengubah apa pun: versi awal mengembalikan {"secret": "The vault key is 42"}. Setelah perubahanmu, tekan tombol “Jalankan” lagi dan kirim permintaan yang sama: status 403 dan {"error": "Admins only"}. Kirim GET /inspect?token= diikuti token yang sama: {"sub": "mallory", "admin": true}, dibaca tanpa kunci apa pun. Kirim GET /inspect?token=hello: status 400 dan {"error": "Not a JWT"}.

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) => {
    res.status(501).json({ error: 'Not written yet' });
});

// Halaman admin.
app.get('/admin', (req, res) => {
    const claims = decodeJwt(String(req.query.token ?? ''));
    if (claims.admin === true) {
        return res.json({ secret: 'The vault key is 42' });
    }
    res.status(403).json({ error: 'Admins only' });
});

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

Pelajaran ini berisi kuis singkat. Mulai pelajarannya untuk menjawab kuis dan melacak progresmu.

Semua pelajaran di Autentikasi dan Keamanan

Latihan sendiri: Playground Express