OAuth 2.0 adalah cara sebuah aplikasi mendapat izin terbatas untuk memakai akunmu di layanan lain, tanpa pernah tahu password-mu. Bayangin kunci valet mobil: kamu gak menyerahkan kunci utama ke petugas parkir, tapi kunci khusus yang cuma bisa menyalakan mesin dan membuka pintu, gak bisa membuka bagasi. Di OAuth, kunci khusus itu namanya access token, dan batas "cuma mesin dan pintu" itu namanya scope.
🧪 Tebakan cepat: di sepanjang alur yang dibahas di sini, apakah access token pernah lewat address bar browser? Tebak dulu, jawabannya ada di bagian "Kenapa code dulu, baru token".
Contohnya: Portal Sekolah ke Google Calendar
Pak Andi, guru di Portal Sekolah, ingin jadwal ujian otomatis masuk ke Google Calendar-nya. Portal Sekolah butuh izin menulis acara di kalender itu, tapi gak boleh tahu password Google Pak Andi. Empat pihak terlibat:
| Peran di OAuth | Di contoh ini |
|---|---|
| Resource owner: pemilik data yang memberi izin | Pak Andi |
| Client: aplikasi yang minta izin | server Portal Sekolah |
| Authorization server: yang menanyakan izin dan memberi token | Google (accounts.google.com, oauth2.googleapis.com) |
| Resource server: yang menyimpan datanya | Google Calendar API |
Sebelum mulai, Portal Sekolah mendaftar di Google Cloud Console dan mendapat client_id (identitas aplikasi, boleh terlihat publik) dan client_secret (rahasia, cuma disimpan di server). Saat mendaftar, Portal Sekolah juga mencatat redirect_uri, alamat tempat Google mengembalikan Pak Andi: https://portal.sekolah.test/oauth/google/callback. Scope yang diminta: https://www.googleapis.com/auth/calendar.events, artinya cuma acara kalender, bukan email atau Drive.
Alur yang dipakai bernama Authorization Code + PKCE, delapan langkah, persis seperti di Lab. Kodenya ditulis dalam JavaScript supaya bisa dijalankan di Lab dan di Node. Di Laravel, Socialite mengerjakan langkah yang sama (lesson Social Login).
Langkah 1-2: state, PKCE, lalu URL authorize
Pak Andi menekan "Hubungkan Google Calendar". Server Portal menyiapkan dua nilai acak dan menyimpannya di session Pak Andi:
state: penanda alur ini. Nanti dicocokkan di callback, untuk memastikan callback itu memang lanjutan dari alur yang dimulai Pak Andi.code_verifier: rahasia sekali pakai untuk PKCE (Proof Key for Code Exchange, dibaca "pixy"). Yang dikirim ke Google cuma sidik jarinya,code_challenge= base64url(SHA-256(verifier)).
function base64url(bytes) {
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
async function startLogin(session) {
session.state = base64url(crypto.getRandomValues(new Uint8Array(16)));
session.verifier = base64url(crypto.getRandomValues(new Uint8Array(32))); // 43 karakter
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(session.verifier));
const params = new URLSearchParams({
response_type: "code",
client_id: "1234567890-portal.apps.googleusercontent.com",
redirect_uri: "https://portal.sekolah.test/oauth/google/callback",
scope: "https://www.googleapis.com/auth/calendar.events",
state: session.state,
code_challenge: base64url(new Uint8Array(digest)),
code_challenge_method: "S256",
});
return "https://accounts.google.com/o/oauth2/v2/auth?" + params; // jawab dengan 302 ke sini
}
crypto.getRandomValuesmengisi byte acak yang aman untuk keamanan. 32 byte jadi 43 karakter base64url.crypto.subtle.digest("SHA-256", ...)menghitung sidik jari verifier.S256memberi tahu Google cara menghitungnya.base64urladalah base64 yang aman untuk URL:+jadi-,/jadi_, dan=di ujung dibuang.URLSearchParamsmeng-encode tiap nilai, jadi:jadi%3Adan/jadi%2F. Jangan merangkai URL dengan+dan string mentah.response_type: "code": minta authorization code, bukan token.
Dengan nilai contoh dari RFC 7636 yang dipakai Lab, verifier dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk menghasilkan challenge E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM, dan URL-nya jadi:
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=1234567890-portal.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Fportal.sekolah.test%2Foauth%2Fgoogle%2Fcallback&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.events&state=af0ifjsldkj&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256
Langkah 3-5: persetujuan dan callback
Di Google, Pak Andi login (password-nya diketik di Google, bukan di Portal) dan melihat layar persetujuan: Portal Sekolah ingin melihat dan mengedit acara kalendernya. Dia menekan Izinkan. Google membuat authorization code dan mencatatnya bersama code_challenge, lalu mengembalikan browser ke redirect_uri:
https://portal.sekolah.test/oauth/google/callback?state=af0ifjsldkj&code=SplxlOBeZQQYbYS6WxSbIA
Kalau Pak Andi menekan Batal, callback-nya membawa ?error=access_denied&state=af0ifjsldkj. Itu jawaban yang sah, bukan crash: client harus menanganinya.
async function handleCallback(session, callbackUrl) {
const query = new URL(callbackUrl).searchParams;
const { state, verifier } = session;
delete session.state; // sekali pakai
delete session.verifier;
if (!state || query.get("state") !== state) throw new Error("state tidak cocok");
if (query.get("error")) return { error: query.get("error") }; // access_denied: Pak Andi menekan Batal
const res = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "authorization_code",
code: query.get("code"),
redirect_uri: "https://portal.sekolah.test/oauth/google/callback",
client_id: "1234567890-portal.apps.googleusercontent.com",
client_secret: process.env.GOOGLE_CLIENT_SECRET, // cuma ada di server
code_verifier: verifier,
}),
});
return res.json();
}
new URL(...).searchParamsmembacastate,code, atauerrordari URL callback.- State dan verifier diambil lalu dihapus dari session: tiap alur cuma boleh selesai sekali.
- State gak cocok atau gak ada: tolak, jangan lanjut. Cek ini, juga
redirect_uriyang harus persis sama, dibahas dari sisi serangan di lesson OAuth Security Pitfalls di track Security.
Langkah 6-7: tukar code jadi token
Request fetch tadi dikirim dari server Portal langsung ke Google, gak lewat browser. Body-nya membawa code, client_secret, dan code_verifier. Google menghitung SHA-256 dari verifier itu dan membandingkannya dengan challenge yang dicatat di langkah 3. Cocok, Google menjawab:
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"expires_in": 3599,
"scope": "https://www.googleapis.com/auth/calendar.events",
"token_type": "Bearer"
}
Langkah 8: Portal memanggil POST https://www.googleapis.com/calendar/v3/calendars/primary/events dengan header Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA, dan jadwal "Ujian Matematika 8A" masuk ke kalender Pak Andi. Token ini berlaku sekitar 1 jam (3599 detik), dan cuma untuk scope kalender.
Kenapa code dulu, baru token
Yang lewat browser (langkah 2 sampai 5) cuma URL: alamat authorize dan callback berisi code. URL bisa tersimpan di history, log server, atau header Referer. Karena itu yang dikirim lewat URL adalah authorization code, yang sekali pakai, berumur pendek, dan gak berguna tanpa client_secret dan code_verifier. Access token-nya sendiri datang sebagai jawaban request langkah 6, server ke server, jadi gak pernah muncul di URL atau history browser. Itu jawaban tebakannya: gak pernah.
Alur lama, Implicit Flow (response_type=token), menaruh access token langsung di URL. Alur itu sudah gak dianjurkan. Sekarang semua client, termasuk SPA dan aplikasi mobile, memakai authorization code + PKCE.
📌 Grant type lain
| Grant type | Dipakai untuk |
|---|---|
| Authorization Code + PKCE | ada user yang memberi izin: web, SPA, mobile |
| Client Credentials | mesin ke mesin, tanpa user, misalnya job malam yang memanggil API lain |
| Device Code | perangkat tanpa keyboard (TV, konsol): layar menampilkan kode, user menyetujui dari HP |
Login dengan Google, yang memakai alur yang sama ditambah identitas user, dibahas di lesson berikutnya, Social Login.
⚠️ Jebakan yang sering ketemu
- Membuat
statetapi gak mengeceknya di callback. Sama saja dengan gak punyastate. - Merangkai URL dengan string mentah.
redirect_uriyang gak di-encode bisa terpotong. PakaiURLSearchParams. - Menukar code di browser dengan
client_secret. Rahasia di kode frontend bukan rahasia lagi. Tukar di server, atau, untuk aplikasi tanpa server, pakai PKCE tanpa secret. - Minta scope berlebihan. Butuh acara kalender, minta
calendar.events, bukan seluruh kalender atau Drive.
TL;DR: OAuth 2.0 memberi aplikasi izin terbatas (scope) tanpa password. Alurnya authorization code + PKCE: client membuat state dan code_verifier, mengirim code_challenge = base64url(SHA-256(verifier)) di URL authorize yang dibangun dengan URLSearchParams, user menyetujui di provider, callback membawa code dan state (cocokkan state, tangani error=access_denied), lalu server menukar code + verifier + client_secret jadi access token, server ke server. Yang diterima client lewat browser cuma authorization code, jadi token gak pernah muncul di URL. Untuk mesin ke mesin: Client Credentials. Implicit Flow sudah usang.