PASETO (alternatif modern JWT) — Auth

PASETO (Platform-Agnostic Security Tokens) adalah alternatif modern untuk JWT yang didesain untuk menghindari bug desain dan footgun JWT. Dibuat oleh Scott…

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.public milik 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.

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:

  1. Token diawali persis v4.public., satu-satunya format yang server terima.
  2. 64 byte terakhir adalah tanda tangan. crypto.subtle.verify("Ed25519", publicKey, sig, pae(h, m, empty, empty)) harus true.
  3. exp lebih lambat dari jam server.
  4. iss sama dengan https://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();

PASETO atau JWT?

PASETO bukan standar RFC. Spesifikasinya ada di paseto.io, dan library-nya lebih sedikit daripada JWT.

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari