Ada satu fitur "Sign in with Google" di sebuah platform booking yang saya bangun, dan secara fungsional ia bekerja sempurna. User klik tombol, popup Google muncul, dan beberapa detik kemudian mereka sudah masuk dengan nama dan email yang benar. Tidak ada error, tidak ada warning. Yang membuat saya berhenti adalah ketika saya membaca ulang endpoint loginnya dan menyadari bahwa server tidak pernah benar-benar bertanya ke Google apakah token ini asli. Ia hanya membaca isinya.
Inti masalahnya ada di potongan kecil ini, yang menerima ID token dari klien lalu langsung mempercayai isinya:
// Server menerima id_token dari frontend, lalu memecahnya
function login_with_google( $id_token ) {
$parts = explode( '.', $id_token );
$payload = json_decode( base64_decode( $parts[1] ), true );
// Langsung dipercaya
$email = $payload['email'];
$sub = $payload['sub'];
$user = get_user_by( 'email', $email );
wp_set_auth_cookie( $user->ID );
return $user;
}Tidak ada error string yang muncul karena memang tidak ada yang gagal. Justru itu yang menakutkan. Saya membuktikannya dengan menyusun token palsu sendiri secara manual lalu mengirimnya ke endpoint itu:
# header dan payload dikarang sendiri, signature diisi sampah
HEADER=$(printf '{"alg":"RS256","typ":"JWT"}' | base64)
PAYLOAD=$(printf '{"email":"admin@old-site.com","sub":"123"}' | base64)
FAKE="$HEADER.$PAYLOAD.bukan-signature-asli"
curl -X POST https://old-site.com/wp-json/auth/google \
-d "id_token=$FAKE"
# Hasilnya: saya login sebagai admin@old-site.comKenapa ini terjadi
Kuncinya ada pada satu kesalahpahaman yang sangat umum: men-decode JWT bukan berarti memverifikasinya. Sebuah JWT itu, sampai Anda memverifikasi tanda tangannya, hanyalah JSON yang dibungkus base64url. Tiga bagiannya, header, payload, dan signature, dipisahkan titik. Bagian payload bisa dibaca siapa pun, bahkan tanpa kunci apa pun, dengan satu kali base64_decode. Itu by design. Bagian yang membuktikan token ini benar-benar dikeluarkan oleh Google adalah signature di bagian ketiga, yang ditandatangani memakai private key Google.
Kode di atas membaca payload dan langsung percaya pada klaim email dan sub di dalamnya, tanpa pernah memeriksa signature itu sama sekali. Akibatnya, siapa pun bisa mengarang payload berisi email korban, menempelkan signature asal-asalan, dan server akan menelannya bulat-bulat. Inilah definisi auth bypass kritis: penyerang bisa mengaku menjadi user mana pun yang emailnya mereka ketahui, termasuk admin. Tombol "Sign in with Google" berubah menjadi "sign in as anyone".
Yang membuat bug ini licin adalah ia tidak pernah memunculkan gejala selama dipakai jujur. Token asli dari Google punya payload yang benar, jadi alur normal selalu lolos. Bug-nya baru terlihat saat ada yang sengaja mengirim token bohong, dan tidak ada satu pun pemeriksaan yang menahannya.
Perbaikannya
Aturannya sederhana: jangan pernah percaya satu klaim pun sebelum token diverifikasi, dan verifikasi itu wajib terjadi di server. Ada empat hal yang harus dicek sebelum email dipetakan ke user lokal.
- Signature harus valid terhadap public key Google (JWKS / sertifikat publik Google). Ini membuktikan token benar dikeluarkan Google dan tidak diubah.
audharus sama persis dengan OAuth client ID milik aplikasi Anda. Ini memastikan token memang ditujukan untuk aplikasi Anda, bukan aplikasi lain.issharusaccounts.google.comatauhttps://accounts.google.com. Ini memastikan penerbitnya memang Google.expharus masih di masa depan, alias belum kedaluwarsa.
Dan satu hal penting: jangan menulis verifikasi JWT dari nol. Pakai library verifikasi resmi dari Google (token-info / verifier mereka) yang sudah menangani pengambilan dan rotasi kunci JWKS dengan benar. Versi yang sudah diperbaiki kira-kira seperti ini:
function login_with_google( $id_token ) {
// Pakai verifier resmi Google, bukan decode manual
$client = new Google_Client( [ 'client_id' => GOOGLE_CLIENT_ID ] );
// verifyIdToken memverifikasi signature, aud, iss, dan exp sekaligus.
// Mengembalikan payload HANYA jika semua valid; selain itu false.
$payload = $client->verifyIdToken( $id_token );
if ( ! $payload ) {
return new WP_Error( 'invalid_token', 'Token Google tidak valid.', [ 'status' => 401 ] );
}
// Pemeriksaan eksplisit sebagai pengaman tambahan
$valid_iss = in_array(
$payload['iss'],
[ 'accounts.google.com', 'https://accounts.google.com' ],
true
);
if ( $payload['aud'] !== GOOGLE_CLIENT_ID || ! $valid_iss ) {
return new WP_Error( 'invalid_token', 'Audience/issuer tidak cocok.', [ 'status' => 401 ] );
}
// Baru sekarang email boleh dipercaya
$email = $payload['email'];
$user = get_user_by( 'email', $email );
if ( ! $user ) {
return new WP_Error( 'no_user', 'User tidak ditemukan.', [ 'status' => 404 ] );
}
wp_set_auth_cookie( $user->ID );
return $user;
}Perhatikan: email yang dipakai diambil dari payload yang sudah terverifikasi, bukan dari parameter yang dikirim klien. Ini poin yang sering terlewat. Jangan pernah menerima email sebagai query param atau field body yang bisa diset bebas oleh klien, karena itu sama saja membuka pintu belakang yang baru.
Pelajaran
Sebuah ID token membuktikan identitas hanya setelah Anda memverifikasi signature, audience, issuer, dan expiry-nya. Men-decode JWT itu trivial, bisa dilakukan siapa pun, dan tidak membuktikan apa-apa. Yang membuktikan adalah verifikasi. Melewatkan langkah verifikasi mengubah login Google yang terlihat profesional menjadi gerbang terbuka yang menerima siapa pun yang berani mengarang email orang lain. Kalau Anda sedang menyentuh alur OAuth atau JWT mana pun, tanyakan satu pertanyaan ini sebelum lanjut: di baris kode mana persisnya signature ini diverifikasi? Kalau Anda tidak bisa menunjuknya, berarti belum ada.
