Menu
CoddyTech

Flag Cookie

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

Cookie tanpa flag dikirim melalui HTTP biasa, ikut dikirim bersama permintaan yang dipicu situs lain, dan bertahan hingga browser ditutup. Empat opsi pada res.cookie memperketatnya:

  • httpOnly: skrip di halaman tidak dapat membacanya.
  • sameSite: 'lax' mencegah cookie dikirim bersama permintaan yang dikirim situs lain, kecuali saat pengguna mengikuti tautan; 'strict' juga mencegah cookie dikirim saat itu.
  • secure: cookie hanya dikirim melalui HTTPS, tidak pernah melalui HTTP biasa yang memungkinkan orang lain di jaringan membacanya.
  • maxAge: berapa lama cookie bertahan, dalam milidetik.
res.cookie('cart', cartId, {
    httpOnly: true,
    sameSite: 'strict',
    secure: true,
    maxAge: 30 * 60 * 1000, // 30 menit
});
challenge icon

Tantangan

Mudah

Perketat cookie sid, agar browser hanya mengirimkannya ke tempat yang semestinya dan melupakannya setelah satu hari.

  1. Di server.js, dalam POST /login, berikan empat opsi ini pada cookie sid:
    • httpOnly, agar skrip di halaman tidak dapat membacanya. Opsi ini sudah ada.
    • Atur sameSite ke 'lax', agar browser tidak menyertakannya dalam permintaan yang dimulai oleh situs lain, kecuali jika pengguna mengikuti tautan ke situsmu.
    • secure, agar cookie hanya dikirim melalui HTTPS.
    • maxAge selama satu hari. Nilai maxAge menggunakan milidetik, sedangkan header menampilkan waktu yang sama dalam detik, jadi satu hari ditampilkan sebagai Max-Age=86400.
  2. Proses login harus tetap berfungsi: setelah POST /login, GET /me mengembalikan pengguna.

Coba: Tekan tombol Jalankan. Kirim POST /signup, lalu POST /login, keduanya dengan {"username": "ada", "password": "correct horse"}. Buka tab Headers pada respons login. Set-Cookie akan terlihat seperti sid=oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN; Max-Age=86400; Path=/; Expires=Sat, 31 Oct 2026 12:00:00 GMT; HttpOnly; Secure; SameSite=Lax. Kirim GET /me: {"username": "ada"}.

Coba sendiri

import express from 'express';
import { randomBytes } from 'node:crypto';
import { parseCookies } from './cookies.js';
import { notes, sessions, 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' });
    }
    const sid = randomBytes(24).toString('base64url');
    await sessions.insertOne({ _id: sid, userId: user._id, createdAt: new Date() });
    res.cookie('sid', sid, { httpOnly: true });
    res.json({ username: user.username });
});

// Mencari pengguna dari cookie sesi, atau mengembalikan 401.
async function requireUser(req, res, next) {
    const { sid } = parseCookies(req.get('cookie'));
    const session = sid && (await sessions.findOne({ _id: sid }));
    const user = session && (await users.findOne({ _id: session.userId }));
    if (!user) {
        return res.status(401).json({ error: 'Log in first' });
    }
    req.user = { id: user._id.toHexString(), username: user.username };
    next();
}

app.get('/me', requireUser, (req, res) => {
    res.json({ username: req.user.username });
});

app.post('/logout', async (req, res) => {
    const { sid } = parseCookies(req.get('cookie'));
    if (sid) {
        await sessions.deleteOne({ _id: sid });
    }
    res.clearCookie('sid');
    res.status(204).end();
});

// Menghitung berapa kali browser ini memanggil GET /visit.
app.get('/visit', (req, res) => {
    const cookies = parseCookies(req.get('cookie'));
    const visits = Number(cookies.visits ?? 0) + 1;
    res.cookie('visits', String(visits));
    res.json({ visits });
});

// Catatan: setiap pengguna hanya membaca dan menulis catatannya sendiri.
app.get('/notes', requireUser, async (req, res) => {
    res.json(await notes.find({ owner: req.user.id }).toArray());
});

app.post('/notes', requireUser, async (req, res) => {
    const note = { owner: req.user.id, text: String(req.body.text ?? '') };
    await notes.insertOne(note);
    res.status(201).json(note);
});

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