OAuth #
Sebelum OAuth ada, cara paling umum untuk mengizinkan aplikasi pihak ketiga mengakses data user adalah dengan memberikan username dan password langsung. User harus percaya bahwa aplikasi itu akan menyimpan credential mereka dengan aman, tidak menggunakannya untuk hal lain, dan tidak pernah bocor. Ini adalah kepercayaan yang terlalu besar untuk diberikan kepada pihak yang tidak dikenal. OAuth hadir untuk memecahkan masalah fundamental ini: bagaimana mengizinkan akses terbatas ke resource user tanpa pernah membagikan password. Memahami OAuth dengan benar — bukan sekadar “cara pakai Google Login” — adalah fondasi untuk membangun sistem otorisasi yang aman, scalable, dan bisa diaudit. Artikel ini membahas OAuth dari masalah yang ia pecahkan, empat grant type utama dengan diagram lengkap, PKCE, perbedaan OAuth 1.0 vs 2.0, hubungannya dengan OpenID Connect, dan anti-pattern yang paling sering ditemukan.
Masalah yang Dipecahkan OAuth #
Bayangkan user ingin menggunakan aplikasi manajemen kalender pihak ketiga yang perlu mengakses Google Calendar mereka. Sebelum OAuth, satu-satunya cara adalah:
// Cara lama (sebelum OAuth):
1. User memberikan username dan password Google ke aplikasi kalender
2. Aplikasi menyimpan credential tersebut untuk akses berulang
3. Aplikasi login sebagai user untuk mengakses calendar
Masalah:
✗ User tidak bisa memberi akses hanya ke calendar — akses ke semua layanan Google
✗ Tidak ada cara mencabut akses tanpa ganti password Google
✗ Jika aplikasi kalender bocor, password Google ikut bocor
✗ Tidak ada cara membuktikan siapa yang sebenarnya melakukan aksi
// Dengan OAuth:
1. Aplikasi kalender redirect ke Google OAuth
2. User login langsung di Google (credential tidak pernah ke aplikasi)
3. User menyetujui: "Izinkan aplikasi ini mengakses Google Calendar kamu"
4. Google memberikan access token terbatas ke aplikasi
5. Aplikasi menggunakan token untuk akses calendar — tidak bisa akses Gmail, Drive, dll
Keunggulan:
✓ Credential tidak pernah dibagikan ke aplikasi pihak ketiga
✓ Akses terbatas (scoped) hanya ke calendar
✓ User bisa mencabut akses kapan saja dari pengaturan Google
✓ Token bisa di-expire otomatis
Empat Peran dalam OAuth #
OAuth 2.0 mendefinisikan empat peran yang harus dipahami sebelum melihat bagaimana flow-nya bekerja.
flowchart LR
RO["Resource Owner\n(User)\nPemilik data yang\ningin diakses"]
C["Client\n(Aplikasi)\nAplikasi yang ingin\nmengakses data user"]
AS["Authorization Server\n(Auth Server)\nMenerbitkan token\nsetelah user consent"]
RS["Resource Server\n(API)\nMenyimpan data\ndan memvalidasi token"]
RO -->|"Consent"| AS
C -->|"Minta otorisasi"| AS
AS -->|"Access token"| C
C -->|"Request + token"| RS
RS -->|"Data"| C
style RO fill:#27AE60,color:#fff,stroke:#1E8449
style C fill:#2980B9,color:#fff,stroke:#1A5276
style AS fill:#E67E22,color:#fff,stroke:#D35400
style RS fill:#8E44AD,color:#fff,stroke:#6C3483Resource Owner adalah user — pemilik data yang ingin diakses oleh aplikasi pihak ketiga. Ia yang memberikan consent.
Client adalah aplikasi yang ingin mengakses data. Bisa berupa web app, mobile app, atau server-side application. Client harus terdaftar di Authorization Server dan memiliki client_id.
Authorization Server adalah server yang menangani autentikasi user, menampilkan consent screen, dan menerbitkan token. Bisa berupa layanan seperti Google, GitHub, atau auth server internal.
Resource Server adalah API yang menyimpan data yang ingin diakses. Ia memvalidasi access token di setiap request. Authorization Server dan Resource Server bisa berada di infrastruktur yang sama atau berbeda.
OAuth 2.0 Grant Types #
OAuth 2.0 mendefinisikan beberapa grant type — cara berbeda untuk mendapatkan access token tergantung pada konteks aplikasi. Memilih grant type yang salah adalah sumber security issue yang umum.
flowchart TD
Start["Tipe aplikasi apa?"]
Q1{"Apakah ada\nuser yang hadir?"}
Q2{"Jenis client?"}
Q3{"Butuh info\nidentitas user?"}
AuthCode["Authorization Code + PKCE\n→ Paling aman\n→ Untuk web & mobile dengan user"]
ClientCred["Client Credentials\n→ Untuk machine-to-machine\n→ Tidak ada user"]
OIDC["Authorization Code + PKCE\n+ OpenID Connect\n→ Untuk login dan identitas user"]
Device["Device Authorization\n→ Untuk smart TV, CLI\n→ Tidak punya browser"]
Start --> Q1
Q1 -->|"Ada user"| Q2
Q1 -->|"Tidak ada user\n(service-to-service)"| ClientCred
Q2 -->|"Web app atau\nmobile app"| Q3
Q2 -->|"Device tanpa\nbrowser"| Device
Q3 -->|"Hanya otorisasi"| AuthCode
Q3 -->|"Butuh identitas\n(login)"| OIDC
style AuthCode fill:#27AE60,color:#fff
style ClientCred fill:#2980B9,color:#fff
style OIDC fill:#8E44AD,color:#fff
style Device fill:#E67E22,color:#fffAuthorization Code Flow + PKCE #
Ini adalah flow yang paling umum dan paling direkomendasikan untuk aplikasi yang melibatkan user — baik web app, mobile app, maupun SPA. PKCE (Proof Key for Code Exchange) adalah extension wajib untuk aplikasi yang tidak bisa menyimpan client_secret dengan aman (SPA, mobile).
sequenceDiagram
participant U as User / Browser
participant C as Client App
participant AS as Authorization Server
participant RS as Resource Server
Note over C: Generate code_verifier (random string)\ncode_challenge = SHA256(code_verifier)
C->>U: Redirect ke Authorization Server
Note over U: GET /authorize?<br/>response_type=code<br/>&client_id=app123<br/>&redirect_uri=https://app.com/callback<br/>&scope=read:profile read:calendar<br/>&state=random-csrf-token<br/>&code_challenge=abc123<br/>&code_challenge_method=S256
U->>AS: Buka halaman login dan consent
AS->>U: Tampilkan: "App ini minta akses ke profil dan calendar kamu"
U->>AS: User login dan klik "Izinkan"
AS->>U: Redirect ke callback URL
Note over U: GET /callback?code=AUTH_CODE&state=random-csrf-token
U->>C: Authorization code diterima
Note over C: Verifikasi state === csrf token yang disimpan
C->>AS: POST /token<br/>{ code, client_id, redirect_uri,<br/> code_verifier }
Note over AS: Verifikasi code_verifier terhadap code_challenge
AS-->>C: { access_token, refresh_token, expires_in }
C->>RS: GET /api/profile<br/>Authorization: Bearer <access_token>
RS-->>C: Data profil userMengapa PKCE penting: tanpa PKCE, authorization code yang di-intercept di redirect URI bisa langsung ditukar dengan access token. Dengan PKCE, interceptor tidak bisa menyelesaikan pertukaran karena tidak punya code_verifier yang hanya ada di client original.
Parameter penting dalam Authorization Code + PKCE:
code_verifier: string random 43-128 karakter (dibuat client, disimpan sementara)
code_challenge: BASE64URL(SHA256(code_verifier)) — dikirim ke auth server di awal
Saat tukar code → token, client kirim code_verifier asli
Auth server verifikasi: SHA256(code_verifier) === code_challenge yang disimpan
→ Hanya client yang membuat request awal yang bisa selesaikan flow
state: random string untuk mencegah CSRF
Client simpan state → kirim ke auth server → verifikasi saat callback
Jika state mismatch → tolak request (kemungkinan CSRF attack)
scope: hanya minta yang benar-benar dibutuhkan
"read:profile read:calendar" — bukan "all" atau "*"
Client Credentials Flow #
Untuk komunikasi machine-to-machine — tidak ada user yang terlibat. Digunakan saat satu service perlu mengakses API service lain atas namanya sendiri, bukan atas nama user.
sequenceDiagram
participant S as Service A (Client)
participant AS as Authorization Server
participant RS as Resource Server (Service B)
Note over S,AS: Tidak ada user — service authenticate dirinya sendiri
S->>AS: POST /token<br/>{ grant_type: client_credentials,<br/> client_id, client_secret,<br/> scope: "read:orders" }
AS->>AS: Verifikasi client credentials
AS-->>S: { access_token, expires_in }
S->>RS: GET /api/orders<br/>Authorization: Bearer <access_token>
RS-->>S: Data ordersKapan menggunakan Client Credentials:
✓ Service A perlu mengambil data dari Service B secara otomatis
✓ Background job yang perlu akses API
✓ CI/CD pipeline yang perlu deploy atau notifikasi
✓ Microservice-to-microservice di mana tidak ada user yang hadir
Jangan gunakan Client Credentials:
✗ Saat ada user yang hadir — gunakan Authorization Code
✗ Saat ingin bertindak atas nama user tertentu
Device Authorization Flow #
Untuk perangkat yang tidak punya browser atau input yang terbatas — smart TV, CLI tool, printer.
Device Authorization Flow:
1. Device request device_code dari Authorization Server
POST /device_authorization { client_id, scope }
Response: { device_code, user_code: "ABCD-1234", verification_uri: "example.com/activate" }
2. Device tampilkan instruksi ke user:
"Buka example.com/activate di browser kamu dan masukkan kode: ABCD-1234"
3. Device mulai polling Authorization Server
POST /token { grant_type: device_code, device_code }
4. User buka browser (di HP atau komputer lain), masukkan kode, login, dan consent
5. Polling device mendapat response:
{ access_token, refresh_token, expires_in }
OAuth 1.0 vs OAuth 2.0 — Perbedaan Filosofi #
Banyak yang mengira OAuth 2.0 adalah “versi yang lebih aman” dari OAuth 1.0. Ini salah kaprah — keduanya adalah filosofi yang berbeda.
OAuth 1.0 — Keamanan di level request:
Setiap request ditandatangani secara kriptografis
Consumer secret + token secret digunakan untuk membuat signature
Aman BAHKAN TANPA HTTPS (secara teori)
Sangat kompleks untuk diimplementasikan dengan benar
Saat ini hampir tidak ada yang menggunakannya untuk use case baru
OAuth 2.0 — Keamanan di level transport:
HTTPS adalah satu-satunya lapisan keamanan
Request tidak ditandatangani — token cukup sebagai bukti otorisasi
Jauh lebih sederhana dan fleksibel
Standar industri saat ini
Tapi: keamanannya sangat bergantung pada implementasi yang benar
Kenapa OAuth 1.0 hampir tidak digunakan lagi:
✗ Implementasi sangat kompleks (banyak detail signature yang bisa salah)
✗ Debug lebih sulit
✗ Tidak mendukung flow modern (mobile, SPA, device)
✗ Ekosistem library sudah sangat minimal
Situasi di mana OAuth 1.0 masih ditemukan:
→ Twitter API v1 (sebelum migrasi ke OAuth 2.0)
→ Beberapa legacy enterprise integration
→ Jika kamu integrate dengan sistem lama yang masih mewajibkannya
OAuth vs OpenID Connect — Perbedaan yang Sering Dikacaukan #
Ini adalah salah satu kebingungan paling umum:
OAuth 2.0:
Menjawab: "Bolehkah aplikasi ini mengakses resource X?"
Tentang: OTORISASI (authorization)
Output: access token untuk akses API
Tidak menjawab: "Siapa user ini?"
OpenID Connect (OIDC):
Dibangun di atas OAuth 2.0
Menambahkan: ID token (JWT) yang berisi informasi identitas user
Menjawab: "Siapa user ini dan bolehkah aplikasi ini login sebagai mereka?"
Tentang: AUTENTIKASI (authentication) + otorisasi
Output: access token + ID token
Kapan menggunakan mana:
Hanya butuh akses API (baca data, tulis data): → OAuth 2.0
Butuh login user dan tahu siapa mereka: → OpenID Connect (OAuth 2.0 + OIDC)
flowchart LR
subgraph OAuth["OAuth 2.0 saja"]
OC["Client App"]
OAS["Auth Server"]
ORS["Resource API"]
OC -->|"Minta akses"| OAS
OAS -->|"access_token"| OC
OC -->|"access_token"| ORS
ORS -->|"Data"| OC
end
subgraph OIDC["OAuth 2.0 + OpenID Connect"]
IC["Client App"]
IAS["Auth Server\n(juga Identity Provider)"]
IRS["Resource API"]
IC -->|"Minta akses\n+ scope: openid"| IAS
IAS -->|"access_token\n+ id_token (JWT)\n+ userinfo endpoint"| IC
IC -->|"access_token"| IRS
IRS -->|"Data"| IC
endScope — Prinsip Least Privilege #
Scope mendefinisikan batas akses yang diberikan ke client. Ini adalah implementasi prinsip least privilege dalam OAuth.
// ANTI-PATTERN: Scope terlalu luas
scope = "all" atau scope = "*"
→ Aplikasi kalender punya akses ke email, dokumen, payment, semua layanan
→ Jika aplikasi compromise, seluruh akun user terekspos
// BENAR: Request hanya scope yang benar-benar dibutuhkan
scope = "read:calendar write:calendar"
→ Aplikasi hanya bisa baca dan tulis calendar
→ Tidak bisa akses email, dokumen, atau layanan lain
// Contoh scope yang well-designed:
Baca profil: "profile:read" atau "openid profile"
Baca email: "email:read" atau "email"
Akses calendar: "calendar:read calendar:write"
Baca orders: "orders:read"
Semua orders: "orders:*" ← mungkin terlalu luas, pertimbangkan lebih granular
// Cara meminta scope di prompt consent:
"Aplikasi ini meminta izin untuk:
✓ Melihat nama dan foto profil kamu
✓ Melihat dan mengedit acara kalender kamu
✗ Mengakses email kamu (tidak diminta)"
→ User tahu persis apa yang diberikan
Token Management yang Aman #
Access Token #
Karakteristik yang direkomendasikan:
Format: JWT (self-contained) atau opaque (random string)
Durasi: 5–60 menit (tergantung sensitivitas)
Penyimpanan (web): memory — bukan localStorage
Penyimpanan (mobile): secure storage
Validasi di Resource Server:
Jika JWT: verifikasi signature + exp + iss + aud
Jika opaque: introspect ke Authorization Server (POST /introspect)
Refresh Token #
Karakteristik:
Durasi: lebih panjang (hari sampai minggu)
Penyimpanan web: HttpOnly Secure SameSite cookie
Penyimpanan mobile: Keychain (iOS) / Keystore (Android)
Rotation: wajib diimplementasikan (lihat artikel JWT untuk detail)
Revocation: bisa dicabut saat logout atau suspicious activity
Anti-Pattern OAuth yang Harus Dihindari #
Menggunakan Implicit Flow #
// ✗ DEPRECATED: Implicit Flow (jangan digunakan)
GET /authorize?response_type=token&client_id=...
→ Access token langsung dikembalikan di URL fragment
→ Token bisa bocor via Referer header, browser history, atau log server
→ Tidak mendukung refresh token
// ✓ BENAR: Authorization Code + PKCE untuk SPA dan mobile
GET /authorize?response_type=code&code_challenge=...
→ Authorization code dikembalikan, ditukar di server-side
→ Lebih aman, mendukung refresh token
Tidak Memvalidasi State Parameter #
// ✗ Anti-pattern: Tidak validasi state
function handleCallback(code, state) {
// Langsung tukar code dengan token tanpa cek state
exchangeCode(code)
}
// Rentan CSRF — attacker bisa trick user untuk authorize request attacker
// ✓ BENAR: Selalu validasi state
function handleCallback(code, state) {
const savedState = sessionStorage.getItem('oauth_state')
if (state !== savedState) {
throw new Error('State mismatch — possible CSRF attack')
}
exchangeCode(code)
}
Menyimpan client_secret di Frontend #
// ✗ BAHAYA: client_secret ada di JavaScript frontend
const response = await fetch('/token', {
body: JSON.stringify({
client_id: 'my-app',
client_secret: 'super-secret-123', // EXPOSED di source code
code: authCode
})
})
// Siapapun yang inspect source code bisa steal client_secret
// ✓ BENAR: Token exchange dilakukan di server-side
// Frontend kirim auth code ke backend
// Backend (server) yang punya client_secret dan melakukan token exchange
// Frontend tidak pernah menyentuh client_secret
// Untuk SPA dan mobile yang tidak bisa punya client_secret:
// Gunakan PKCE — dirancang untuk public client tanpa client_secret
Request Scope Berlebihan #
// ✗ Meminta semua permission yang mungkin dibutuhkan suatu saat
scope = "read:profile write:profile read:contacts write:contacts
read:calendar write:calendar read:email send:email
read:documents write:documents payment:read payment:write"
→ User curiga dan mungkin menolak consent
→ Security surface area sangat luas jika terjadi token theft
// ✓ Minta scope secara incremental sesuai kebutuhan nyata
Saat login awal: scope = "read:profile"
Saat user buka fitur kalender: scope = "read:calendar write:calendar"
Saat user buka fitur kontak: scope = "read:contacts"
→ User consent lebih terfokus
→ Minimal scope = minimal damage jika token bocor
Menggunakan OAuth sebagai Authentication Saja #
// ✗ Salah kaprah yang umum:
"Kita sudah implement OAuth, jadi user sudah authenticated"
OAuth hanya menjawab: "Apakah aplikasi ini diizinkan mengakses resource?"
Bukan: "Siapa user ini?"
Masalah: access token dari OAuth tidak secara langsung membuktikan identitas user
dengan cara yang terstandarkan
// ✓ Gunakan OpenID Connect untuk authentication:
scope = "openid profile email"
→ Response berisi ID token (JWT) dengan informasi identitas user
→ ID token diverifikasi untuk authentication
→ Access token digunakan untuk otorisasi (akses API)
OAuth 2.0 bukan protokol autentikasi — ia adalah framework otorisasi. Banyak implementasi “login dengan OAuth” sebenarnya menggunakan OpenID Connect (OIDC) yang dibangun di atas OAuth 2.0. Jika kamu ingin tahu “siapa user ini”, kamu butuh ID token dari OIDC, bukan hanya access token dari OAuth.
Membangun Authorization Server Sendiri vs Menggunakan Provider #
Membangun Authorization Server sendiri adalah tugas yang sangat kompleks dan berisiko tinggi jika dilakukan sembarangan. Pertimbangkan trade-off ini dengan serius.
Gunakan existing provider (Auth0, Keycloak, Okta, Supabase Auth) jika:
✓ Tidak ada kebutuhan kustomisasi yang sangat spesifik
✓ Tim tidak punya expertise di security dan OAuth implementation
✓ Time-to-market adalah prioritas
✓ Mau memanfaatkan fitur yang sudah mature (MFA, social login, audit log)
Pertimbangkan bangun sendiri jika:
✓ Ada regulatory requirement yang mengharuskan data di on-premise
✓ Kebutuhan kustomisasi yang tidak bisa dipenuhi provider
✓ Tim punya keahlian yang cukup dan bandwidth untuk maintenance
✓ Skala yang membutuhkan kontrol penuh
Jika membangun sendiri, library yang direkomendasikan:
Go: golang.org/x/oauth2, ory/fosite
Node.js: node-oidc-provider
Java: Spring Security OAuth, Keycloak (self-hosted)
Python: authlib
Checklist Implementasi OAuth yang Aman #
FLOW SELECTION:
□ Menggunakan Authorization Code + PKCE (bukan Implicit Flow)
□ Client Credentials hanya untuk machine-to-machine tanpa user
□ Device Authorization untuk perangkat tanpa browser
PARAMETER KEAMANAN:
□ state parameter digunakan dan divalidasi (CSRF protection)
□ code_verifier dan code_challenge diimplementasikan (PKCE)
□ redirect_uri terdaftar di Authorization Server dan divalidasi ketat
□ client_secret tidak ada di frontend code (hanya di server)
SCOPE:
□ Scope paling minimal yang cukup untuk fitur
□ Scope di-request secara incremental jika memungkinkan
□ Consent screen menjelaskan scope dengan bahasa yang user paham
□ Tidak ada wildcard scope atau "all access"
TOKEN MANAGEMENT:
□ Access token berumur pendek (≤60 menit)
□ Refresh token disimpan di HttpOnly Secure cookie (web) atau secure storage (mobile)
□ Token rotation diimplementasikan untuk refresh token
□ Logout endpoint mencabut refresh token
AUTENTIKASI vs OTORISASI:
□ OpenID Connect digunakan jika butuh identitas user (scope: openid)
□ ID token dan access token dibedakan penggunaannya
□ ID token tidak digunakan sebagai access token (dan sebaliknya)
RESOURCE SERVER:
□ Setiap request memvalidasi access token
□ Scope di token divalidasi sesuai endpoint yang diakses
□ Token expiry divalidasi
MONITORING:
□ Login events di-log (berhasil dan gagal)
□ Token issuance di-log
□ Suspicious activity (banyak failed attempts) di-alert
Ringkasan #
- OAuth adalah framework otorisasi, bukan autentikasi — ia menjawab “bolehkah aplikasi ini mengakses resource X”, bukan “siapa user ini”. Untuk autentikasi dan identitas user, gunakan OpenID Connect yang dibangun di atas OAuth 2.0.
- Authorization Code + PKCE adalah pilihan default untuk semua aplikasi dengan user — baik web app, SPA, maupun mobile. PKCE menggantikan kebutuhan client_secret untuk public clients dan melindungi dari authorization code interception.
- Implicit Flow sudah deprecated — jangan gunakan. Authorization Code + PKCE lebih aman dan mendukung refresh token.
- Client Credentials untuk machine-to-machine — saat tidak ada user yang hadir, service authenticate dirinya sendiri menggunakan client_id dan client_secret.
- State parameter wajib untuk CSRF protection — buat random string, simpan di session, kirim di authorization request, verifikasi saat callback. Tanpa ini, CSRF attack bisa memaksa user mengotorisasi request attacker.
- Scope adalah implementasi least privilege — request hanya scope yang benar-benar dibutuhkan saat ini. Scope berlebihan memperluas damage jika token bocor dan membuat user ragu untuk memberikan consent.
- client_secret tidak boleh ada di frontend — untuk SPA dan mobile yang tidak bisa menyimpan secret dengan aman, gunakan PKCE. Token exchange selalu dilakukan di server-side.
- OAuth 1.0 tidak perlu dipilih untuk use case baru — kompleksitasnya sangat tinggi dan tidak menawarkan manfaat nyata di era HTTPS ubiquitous. Gunakan OAuth 2.0.
- OAuth 2.0 keamanannya bergantung pada implementasi yang benar — tidak seperti OAuth 1.0 yang ada request signing, OAuth 2.0 sangat bergantung pada HTTPS, validasi parameter, dan storage yang aman.
- Pertimbangkan menggunakan Auth Provider yang sudah mature — membangun Authorization Server sendiri sangat kompleks dan berisiko. Auth0, Keycloak, atau Supabase Auth sudah menangani banyak edge case keamanan yang mudah terlewatkan saat bangun dari scratch.