Protokol

OAuth 2.0 dan OpenID Connect

Panduan lengkap OAuth 2.0 dan OpenID Connect — authorization code flow, PKCE, implicit flow, token refresh, scopes, claims, JWT, dan implementasi keamanan modern untuk web, mobile, dan SPA

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

RoleDeskripsiContoh
Resource OwnerPengguna yang memiliki dataAnda (pemilik akun Google)
ClientAplikasi yang ingin mengakses resourceApp "PrintFoto" yang ingin akses Google Photos
Authorization ServerServer yang mengeluarkan token setelah user approveaccounts.google.com
Resource ServerServer yang menyimpan resource (API)photos.google.com/api

Mengapa OAuth, Bukan Password Sharing?

Text
# ❌ 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 TypeClient TypeUse CaseRekomendasi
Authorization CodeServer-side appsWeb app dengan backend✅ Sangat direkomendasikan
Authorization Code + PKCESPA, MobilePublic clients tanpa secret✅ Wajib untuk public clients
Client CredentialsMachine-to-machineService-to-service tanpa user✅ Untuk M2M
Device CodeInput-constrained devicesSmart TV, CLI tools✅ Untuk devices
Refresh TokenSemuaPerpanjang session tanpa re-login✅ Best practice
Implicit (deprecated)SPA lamaSPA tanpa backend❌ Deprecated — gunakan Auth Code + PKCE
Resource Owner Password (deprecated)First-party appsApp 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

Text
# 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

JavaScript
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

Text
# 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).

Text
# 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?".

AspekOAuth 2.0OpenID Connect
TujuanAuthorization (akses)Authentication (identitas)
TokenAccess TokenAccess Token + ID Token
ScopeCustom scopesopenid, profile, email
Endpoints/authorize, /token+ /userinfo, /.well-known/openid-configuration
ID TokenTidak adaJWT dengan claims user
DiscoveryTidak standarOIDC Discovery (well-known)
Contoh providerGitHub OAuthGoogle, Auth0, Keycloak, Okta

7. JWT Deep Dive

Text
# 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

⚠️ Security Checklist
  • 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

Quiz Pemahaman

Pertanyaan 1: Apa perbedaan utama OAuth 2.0 dan OpenID Connect?

a) OAuth 2.0 untuk authorization, OIDC untuk authentication
b) Sama saja
c) OAuth 2.0 lebih aman
d) OIDC lebih lama

Pertanyaan 2: Apa fungsi PKCE?

a) Mengenkripsi access token
b) Mencegah authorization code interception
c) Mempercepat token issuance
d) Mengganti refresh token

Pertanyaan 3: Berapa lama access token sebaiknya valid?

a) 30 hari
b) 1 tahun
c) 5-15 menit
d) Selamanya

Pertanyaan 4: Di mana sebaiknya refresh token disimpan di SPA?

a) localStorage
b) sessionStorage
c) HttpOnly, Secure, SameSite cookie
d) JavaScript variable

Pertanyaan 5: Grant type apa yang digunakan untuk machine-to-machine communication?

a) Authorization Code
b) Implicit
c) Client Credentials
d) Device Code
🔍 Zoom
100%
🎨 Tema