1. OAuth 2.0 Fundamentals
OAuth 2.0 (RFC 6749) adalah standar authorization yang memungkinkan aplikasi pihak ketiga mengakses resource pengguna tanpa harus mengetahui password mereka. OAuth 2.0 bukan protokol autentikasi — itu adalah protokol otorisasi.
Roles dalam OAuth 2.0
| Role | Deskripsi | Contoh |
|---|---|---|
| Resource Owner | Pengguna yang memiliki data | Anda (pemilik akun Google) |
| Client | Aplikasi yang ingin mengakses resource | App "PrintFoto" yang ingin akses Google Photos |
| Authorization Server | Server yang mengeluarkan token setelah user approve | accounts.google.com |
| Resource Server | Server yang menyimpan resource (API) | photos.google.com/api |
Mengapa OAuth, Bukan Password Sharing?
# ❌ TANPA OAuth — Password Sharing (BURUK!): # # User → "PrintFoto" app → credentials ke Google Photos API # → App punya FULL ACCESS ke akun Anda # → Tidak bisa revoke tanpa ganti password # → App bisa simpan password Anda # → Jika app di-hack, password Anda bocor # ✅ DENGAN OAuth — Delegated Access (BENAR!): # # 1. User → "PrintFoto" → redirect ke Google # 2. Google tampilkan consent screen: # "PrintFoto ingin mengakses: # ✅ Lihat foto Anda # ❌ Hapus foto Anda (tidak diminta)" # 3. User approve → Google berikan authorization code # 4. "PrintFoto" tukar code → access token # 5. "PrintFoto" gunakan token untuk akses API # → App hanya bisa akses yang di-approve # → User bisa revoke kapan saja # → Password TIDAK PERNAH diketahui app
2. Grant Types (Flows)
| Grant Type | Client Type | Use Case | Rekomendasi |
|---|---|---|---|
| Authorization Code | Server-side apps | Web app dengan backend | ✅ Sangat direkomendasikan |
| Authorization Code + PKCE | SPA, Mobile | Public clients tanpa secret | ✅ Wajib untuk public clients |
| Client Credentials | Machine-to-machine | Service-to-service tanpa user | ✅ Untuk M2M |
| Device Code | Input-constrained devices | Smart TV, CLI tools | ✅ Untuk devices |
| Refresh Token | Semua | Perpanjang session tanpa re-login | ✅ Best practice |
| Implicit (deprecated) | SPA lama | SPA tanpa backend | ❌ Deprecated — gunakan Auth Code + PKCE |
| Resource Owner Password (deprecated) | First-party apps | App yang sangat trusted | ❌ Deprecated |
3. Authorization Code Flow + PKCE
Ini adalah flow yang paling aman dan direkomendasikan untuk sebagian besar aplikasi. PKCE (Proof Key for Code Exchange) (RFC 7636) menambahkan lapisan keamanan untuk mencegah authorization code interception.
Alur Lengkap
# Authorization Code Flow + PKCE:
# === STEP 1: Client generate PKCE parameters ===
code_verifier = random(43-128 chars, [A-Z][a-z][0-9]-._~
code_challenge = BASE64URL(SHA256(code_verifier))
code_challenge_method = "S256"
# === STEP 2: Client redirect user ke Authorization Server ===
GET https://auth.example.com/authorize?
response_type=code&
client_id=my-app-123&
redirect_uri=https://myapp.com/callback&
scope=openid profile email&
state=xyzRandom123&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
# === STEP 3: User login dan approve consent ===
# Browser menampilkan halaman login + consent screen
# === STEP 4: Authorization Server redirect ke callback ===
HTTP 302 → https://myapp.com/callback?
code=AUTH_CODE_HERE&
state=xyzRandom123
# === STEP 5: Client tukar authorization code + code_verifier ===
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=https://myapp.com/callback&
client_id=my-app-123&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
# === STEP 6: Authorization Server verifikasi dan kirim tokens ===
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4=",
"id_token": "eyJhbGciOiJSUzI1NiIs..." ← OIDC
}
# === STEP 7: Client gunakan access token ===
GET https://api.example.com/users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Implementasi di Node.js
const crypto = require('crypto');
const express = require('express');
const axios = require('axios');
const app = express();
const config = {
authUrl: 'https://auth.example.com/authorize',
tokenUrl: 'https://auth.example.com/token',
clientId: 'my-app-123',
redirectUri: 'https://myapp.com/callback',
scope: 'openid profile email'
};
// Generate PKCE parameters
function generatePKCE() {
const verifier = crypto.randomBytes(32)
.toString('base64url')
.slice(0, 128);
const challenge = crypto.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
// STEP 1: Redirect ke authorization server
app.get('/login', (req, res) => {
const { verifier, challenge } = generatePKCE();
const state = crypto.randomBytes(16).toString('hex');
// Simpan di session
req.session = { codeVerifier: verifier, state };
const authUrl = new URL(config.authUrl);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', config.clientId);
authUrl.searchParams.set('redirect_uri', config.redirectUri);
authUrl.searchParams.set('scope', config.scope);
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
res.redirect(authUrl.toString());
});
// STEP 2: Callback — tukar code dengan tokens
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Validasi state (CSRF protection)
if (state !== req.session.state) {
return res.status(403).send('State mismatch — possible CSRF');
}
// Tukar authorization code + code_verifier
const response = await axios.post(config.tokenUrl, new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: config.redirectUri,
client_id: config.clientId,
code_verifier: req.session.codeVerifier
}), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
const { access_token, refresh_token, id_token, expires_in } = response.data;
// Simpan tokens di session/secure cookie
req.session.accessToken = access_token;
req.session.refreshToken = refresh_token;
req.session.expiresAt = Date.now() + (expires_in * 1000);
// Parse ID token untuk user info
const userInfo = parseJWT(id_token);
console.log('User logged in:', userInfo.name, userInfo.email);
res.redirect('/dashboard');
});
// STEP 3: Refresh token saat expired
async function refreshAccessToken(refreshToken) {
const response = await axios.post(config.tokenUrl, new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: config.clientId
}));
return response.data;
}
app.listen(3000);
4. Access Token & Refresh Token
# Token Types dalam OAuth 2.0:
# ACCESS TOKEN:
# - Short-lived (5 menit - 1 jam)
# - Digunakan untuk akses resource server
# - Bisa berupa JWT (self-contained) atau opaque (perlu introspection)
# - Jika bocor → dampak terbatas (expired cepat)
# REFRESH TOKEN:
# - Long-lived (hari - minggu - bulan)
# - Digunakan untuk mendapatkan access token baru
# - HARUS disimpan aman (HttpOnly cookie, secure storage)
# - Bisa di-revoke oleh authorization server
# - Hanya dikirim ke token endpoint (TIDAK ke resource server)
# ID TOKEN (OIDC only):
# - JWT yang berisi info user (claims)
# - BUKAN untuk autentikasi API — untuk identitas user
# - Dikirim ke client untuk mengetahui SIAPA user
# Token Lifecycle:
#
# User Login → [Access Token + Refresh Token + ID Token]
# │
# ├─ Access Token valid → Gunakan untuk API calls
# │
# ├─ Access Token expired → Gunakan Refresh Token
# │ │
# │ └─ POST /token { grant_type: "refresh_token" }
# │ │
# │ ├─ Berhasil → [New Access Token + New Refresh Token]
# │ └─ Gagal → User harus login ulang
# │
# └─ Refresh Token expired/revoked → User login ulang
5. Scopes & Claims
Scopes mendefinisikan level akses yang diminta oleh client. Claims adalah informasi yang dikembalikan di token (biasanya di ID token JWT).
# Common OAuth 2.0 Scopes:
scope=read → Read access
scope=write → Write access
scope=admin → Admin access
scope=profile → Nama, foto, dll
scope=email → Alamat email
scope=openid → OIDC — minta ID token
scope=offline_access → Minta refresh token
# Scopes di Authorization Request:
scope=openid profile email offline_access
# Custom Scopes (per aplikasi):
scope=read:orders → Baca order
scope=write:orders → Tulis order
scope=read:users → Baca user
scope=admin:users → Admin user management
# === Claims di ID Token (JWT) ===
{
"iss": "https://auth.example.com", // Issuer
"sub": "user-12345", // Subject (user ID)
"aud": "my-app-123", // Audience (client ID)
"exp": 1720000000, // Expiration
"iat": 1719996400, // Issued at
"name": "John Doe", // Custom claim
"email": "john@example.com",
"email_verified": true,
"picture": "https://example.com/photo.jpg",
"locale": "id-ID"
}
6. OpenID Connect (OIDC)
OpenID Connect adalah layer autentikasi di atas OAuth 2.0. Jika OAuth 2.0 menjawab "apa yang boleh diakses?", OIDC menjawab "siapa yang mengakses?".
| Aspek | OAuth 2.0 | OpenID Connect |
|---|---|---|
| Tujuan | Authorization (akses) | Authentication (identitas) |
| Token | Access Token | Access Token + ID Token |
| Scope | Custom scopes | openid, profile, email |
| Endpoints | /authorize, /token | + /userinfo, /.well-known/openid-configuration |
| ID Token | Tidak ada | JWT dengan claims user |
| Discovery | Tidak standar | OIDC Discovery (well-known) |
| Contoh provider | GitHub OAuth | Google, Auth0, Keycloak, Okta |
7. JWT Deep Dive
# JWT (JSON Web Token) Structure:
# header.payload.signature
# Setiap bagian di-encode base64url
# === HEADER ===
{"alg": "RS256", "typ": "JWT", "kid": "key-2024-01"}
# === PAYLOAD (Claims) ===
{
"iss": "https://auth.example.com",
"sub": "user-12345",
"aud": "my-app-123",
"exp": 1720000000,
"iat": 1719996400,
"scope": "openid profile email",
"name": "John Doe",
"email": "john@example.com"
}
# === SIGNATURE ===
RSASHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)
# Verifikasi:
# 1. Ambil public key dari JWKS endpoint (/jwks)
# 2. Verify signature menggunakan public key
# 3. Cek exp > now (belum expired)
# 4. Cek iss == expected issuer
# 5. Cek aud == expected audience
8. Security Best Practices
- SELALU gunakan HTTPS — token di URL/header rentan sniffing
- Gunakan PKCE untuk semua public clients (SPA, mobile)
- Validasi state parameter — mencegah CSRF attacks
- Jangan simpan tokens di localStorage — rentan XSS
- Gunakan HttpOnly, Secure, SameSite cookies untuk refresh tokens
- Token expiry pendek — access token 5-15 menit
- Implementasi token rotation — refresh token sekali pakai
- Validasi JWT signature — jangan hanya decode
- Minimal scopes — minta hanya yang diperlukan