PASETO (Platform-Agnostic SEcurity TOkens) adalah token seperti JWT, tapi tanpa pilihan yang bisa disalahgunakan. Versi dan tujuannya tertulis di depan token, dan tiap versi cuma punya satu algoritma. Bayangin surat resmi sekolah. JWT seperti surat yang kop-nya bilang "periksa pakai cap yang tertulis di sini", jadi pemeriksa harus percaya pada tulisan di surat itu sendiri. PASETO seperti surat dengan kop baku: kantor penerima cuma menerima satu jenis kop, dan cuma punya satu cara memeriksanya.
🧪 Tebakan cepat: token
v4.publicmilik Siti dipegang orang lain. Orang itu bisa membaca isinya, atau isinya terenkripsi? Tebak dulu, jawabannya ada di bagian "local dan public".
Anatomi token
Ini token yang dibuat Lab untuk Siti, berumur 15 menit:
v4.public.eyJzdWIiOiIxIiwicm9sZSI6InN0dWRlbnQiLCJpc3MiOiJodHRwczovL3BvcnRhbC5zZWtvbGFoLnRlc3QiLCJpYXQiOiIyMDI2LTEwLTA5VDA3OjAwOjAwKzA3OjAwIiwiZXhwIjoiMjAyNi0xMC0wOVQwNzoxNTowMCswNzowMCJ9y47T1burF-vLqDJwGaXbnP7sN7E_sYVtwW7Y73tB6W8O9NJXk05KwvRu7V32USFicnoyaV8LqvfOhLLvpTEyCQ
| Bagian | Isinya |
|---|---|
v4 |
versi. Versi menentukan algoritmanya, token gak bisa memilih |
public |
tujuan (purpose): ditandatangani, isinya bisa dibaca |
| sisanya | base64url dari pesan JSON (132 byte) yang langsung disambung tanda tangan Ed25519 (64 byte) |
Pesannya:
{"sub":"1","role":"student","iss":"https://portal.sekolah.test","iat":"2026-10-09T07:00:00+07:00","exp":"2026-10-09T07:15:00+07:00"}
Beda kecil dengan JWT: di PASETO, iat dan exp ditulis sebagai tanggal ISO 8601, bukan detik Unix.
Kenapa gak ada header alg
Di JWT, header token sendiri yang bilang algoritmanya ("alg": "HS256"). Server yang lengah ikut percaya, dan dari situ lahir bug terkenal seperti alg: none. Sisi serangannya dibahas di lesson JWT Security. PASETO membuang pilihan itu. Ini yang disebut opinionated: keputusan penting sudah dibuat oleh spesifikasinya.
v4.publicselalu Ed25519, tanda tangan digital dengan sepasang kunci.v4.localselalu XChaCha20 untuk enkripsi, ditambah BLAKE2b untuk memastikan isinya gak diubah.
Server dikonfigurasi "aku cuma menerima v4.public dengan kunci publik ini". Token dengan awalan lain ditolak sebelum isinya dibaca sama sekali.
local dan public
v4.local |
v4.public |
|
|---|---|---|
| Kunci | 1 secret key untuk membuat dan memeriksa (symmetric) | private key untuk menandatangani, public key untuk memeriksa (asymmetric) |
| Isinya | terenkripsi, client gak bisa membaca | cuma ditandatangani, siapa pun bisa membaca, gak ada yang bisa mengubah |
| Cocok untuk | satu aplikasi yang membuat dan memeriksa sendiri | banyak layanan yang memeriksa token dari satu server login |
Jadi isi token v4.public Siti bisa dibaca siapa pun yang memegangnya, persis seperti payload JWT. Itu jawaban tebakannya. Yang membuat isinya rahasia cuma local.
Membuat dan memeriksa v4.public
Ed25519 sudah ada di Web Crypto browser modern, jadi Lab membuat token v4.public sungguhan. Tanda tangannya gak dihitung dari pesan saja, tapi dari PAE (pre-authentication encoding): awalan v4.public., pesan, footer, dan implicit assertion, masing-masing didahului panjangnya. Jadi awalannya ikut terkunci oleh tanda tangan.
const enc = new TextEncoder();
const b64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
function le64(n) { // panjang sebagai 8 byte little-endian
const out = new Uint8Array(8);
for (let i = 0; i < 8; i++) { out[i] = n % 256; n = Math.floor(n / 256); }
return out;
}
function pae(...pieces) { // jumlah potongan, lalu panjang + isi tiap potongan
const parts = [le64(pieces.length)];
for (const piece of pieces) parts.push(le64(piece.length), piece);
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
let at = 0;
for (const p of parts) { out.set(p, at); at += p.length; }
return out;
}
async function signV4Public(claims, privateKey) {
const h = enc.encode("v4.public.");
const m = enc.encode(JSON.stringify(claims));
const empty = new Uint8Array(0); // footer dan implicit assertion kosong
const sig = new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, pae(h, m, empty, empty)));
const body = new Uint8Array(m.length + 64);
body.set(m);
body.set(sig, m.length);
return "v4.public." + b64url(body);
}
Server memeriksanya berurutan, dan menolak dengan 401 di langkah pertama yang gagal:
- Token diawali persis
v4.public., satu-satunya format yang server terima. - 64 byte terakhir adalah tanda tangan.
crypto.subtle.verify("Ed25519", publicKey, sig, pae(h, m, empty, empty))harustrue. explebih lambat dari jam server.isssama denganhttps://portal.sekolah.test.
v4.local butuh XChaCha20 dan BLAKE2b, yang belum ada di Web Crypto. Karena itu Lab cuma memodelkan token local, dan membuat token public secara sungguhan.
Di Laravel: yang dipakai BelajarWeb.dev
API BelajarWeb.dev memakai PASETO v4.local dengan library paragonie/paseto, karena aplikasi yang sama yang membuat dan memeriksa token-nya. Versi ringkas dari PasetoService-nya:
$key = new SymmetricKey(base64_decode(config('auth.paseto.key')), new Version4);
$token = (new Builder)
->setKey($key)
->setVersion(new Version4)
->setPurpose(Purpose::local())
->setIssuedAt(now()->toDateTimeImmutable())
->setExpiration(now()->addDays(90)->toDateTimeImmutable())
->setIssuer(config('app.url'))
->setSubject((string) $user->id)
->setClaims(['jti' => $tokenId])
->toString(); // "v4.local...."
$claims = Parser::getLocal($key, ProtocolCollection::v4())
->addRule(new IssuedBy(config('app.url')))
->parse($incoming) // melempar exception kalau rusak, kedaluwarsa, atau iss-nya beda
->getClaims();
- Kuncinya dari environment (
PASETO_KEY), gak pernah ditulis di kode. Parsersudah memeriksaexpdengan sendirinya, jadi gak perlu cek manual.jti(id unik token) disimpan di tabelapi_tokens. Logout menghapus barisnya, jadi token itu langsung gak berlaku walaupunexp-nya masih jauh. Token stateless ditambah daftar token aktif: bisa dicabut kapan saja.- Umurnya 90 hari, dan diperpanjang otomatis lewat header
X-New-Tokendi 7 hari terakhir.
PASETO atau JWT?
- Kamu memegang dua sisinya (API internal, aplikasi mobile milik sendiri, tim kecil, gak butuh login pihak ketiga): PASETO. Lebih sedikit yang bisa salah dikonfigurasi.
- Harus bicara dengan sistem lain: login Google atau SSO (ID token-nya JWT), atau partner yang library-nya cuma mengenal JWT: JWT.
PASETO bukan standar RFC. Spesifikasinya ada di paseto.io, dan library-nya lebih sedikit daripada JWT.
⚠️ Jebakan yang sering ketemu
- Mengira isi
publicterenkripsi. Cumalocalyang terenkripsi. Payloadpublicjangan berisi rahasia. - Membagi kunci
localke layanan yang cuma perlu memeriksa. Pemegang kunci itu juga bisa membuat token. Pakaipublic, bagikan kunci publiknya saja. - Menerima beberapa versi "biar kompatibel". Itu mengembalikan masalah pilihan algoritma. Terima satu versi dan satu purpose.
- Lupa bahwa token stateless gak bisa dicabut. Simpan
jtiseperti BelajarWeb.dev, atau buat umurnya pendek.
TL;DR: PASETO = versi.purpose.isi, dengan satu algoritma per versi dan tanpa header alg. v4.local memakai satu secret key dan isinya terenkripsi; v4.public ditandatangani Ed25519, isinya bisa dibaca siapa pun, dan diperiksa dengan kunci publik. Server menerima satu format saja, memeriksa tanda tangan atas PAE, lalu exp (tanggal ISO) dan iss. Pilih PASETO kalau kamu memegang dua sisinya, JWT kalau harus bicara dengan sistem lain. BelajarWeb.dev memakai v4.local plus tabel jti supaya logout langsung mencabut token.