Menu
CoddyTech

Masa Kedaluwarsa Token

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

Server tidak bisa menarik kembali token: tidak ada yang disimpan, jadi token yang dicuri tetap berfungsi sampai kedaluwarsa. Buat token berumur singkat, hitungan menit, bukan hari, dan biarkan client meminta token baru.

jwtVerify sudah menolak token yang nilai exp-nya sudah lewat. exp adalah waktu yang dihitung dalam detik sejak 1 Januari 1970. Fungsi ini melempar errors.JWTExpired. err instanceof errors.JWTExpired bernilai true untuk jenis error tersebut, jadi kamu bisa membedakan token yang kedaluwarsa dari token palsu dan menjelaskannya: client yang membaca Token kedaluwarsa tahu untuk login lagi.

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

Tantangan

Mudah

Server tidak bisa menarik kembali token, jadi token yang dicuri akan tetap berfungsi sampai kedaluwarsa. Buat token berumur pendek, dan beri tahu aplikasi dengan jelas saat tokennya kedaluwarsa, agar aplikasi tahu untuk masuk lagi.

  1. Di server.js, dalam POST /token, buat token kedaluwarsa 15 menit setelah dibuat, bukan satu jam.
  2. Di requireUser, periksa error yang dilempar jwtVerify. Untuk token yang kedaluwarsa, jose melempar errors.JWTExpired. Impor errors dari jose. Jika error-nya jenis tersebut, kirim respons dengan status 401 dan {"error": "Token expired"}.
  3. Setiap masalah lainnya tetap mendapat status 401 dan {"error": "Log in first"}: tidak ada header, token yang diubah, atau token yang ditandatangani dengan kunci lain.

Coba: Tekan Jalankan. Kirim POST /signup, lalu POST /token, keduanya dengan {"username": "ada", "password": "correct horse"}. Kirim GET /inspect?token= diikuti token baru: exp sekarang 900 detik setelah iat. Keduanya menghitung detik sejak 1 Januari 1970. Berikutnya, ini token yang ditandatangani dengan secret server ini, yang kedaluwarsa pada 1 Januari 2026: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkYSIsInN1YiI6IjY2ZjFjMmE5ZTRiMGExYjJjM2Q0ZTVmNiIsImlhdCI6MTc2NzIyNTYwMCwiZXhwIjoxNzY3MjI2NTAwfQ.PL8VCHpGNwBJ-7EawZ60veEEE-ZyosNC7xYQQpgT2uE Di kolom Headers, atur Authorization menjadi Bearer, satu spasi, lalu token tersebut, kemudian kirim GET /me: status 401 dan {"error": "Token expired"}. Ubah satu huruf di bagian tengahnya lalu kirim lagi: status 401 dan {"error": "Log in first"}. Token yang diubah gagal dalam pemeriksaan tanda tangan sebelum tanggalnya dibaca.

Coba sendiri

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

// Memeriksa token di header Authorization, atau mengembalikan 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);
});

// 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