Jawaban Cepat: Sementara OAuth 1.0a mengandalkan tanda tangan kriptografis rumit dan OAuth 2.0 memperkenalkan bearer token yang rentan serangan replay, OAuth 2.1 adalah standar wajib untuk agen AI dan server Model Context Protocol (MCP). Protokol ini mewajibkan PKCE (RFC 7636), menghapus grant usang, dan menerapkan token Zero-Trust dengan DPoP (RFC 9449) pada lingkungan headless.
1. Pendahuluan: Krisis Identitas Agen Otonom di Tahun 2026
Transisi cepat dari antarmuka obrolan Large Language Model (LLM) terisolasi menuju agen AI otonom multi-turn dan server Model Context Protocol (MCP) telah memicu krisis keamanan siber serius: bottleneck identitas dan otorisasi agen.
Sepanjang 2024 dan 2025, para pengembang menghubungkan perkakas otonom — seperti Claude Code, Cursor, Windsurf, AutoGen, dan agen LangGraph kustom — ke API perusahaan menggunakan Personal Access Token (PAT) statis atau API key berumur panjang yang di-hardcode di file .env atau variabel lingkungan sistem operasi. Ketika agen AI mengeksekusi perintah shell lokal, meminta data ke database internal (melalui server MCP PostgreSQL atau Supabase), atau memperbarui tiket kerja (melalui server MCP Jira atau Linear), ia berjalan di bawah otoritas ambient yang sangat luas (Ambient Authority).
ARSITEKTUR AGEN LEGACY YANG RENTAN (Otoritas Statis Ambient):
+--------------------+ Pemanggilan Subproses +---------------------------+
| Agen Host LLM | ─────────────────────────────> | Server Tool MCP Lokal |
| (Claude Code / | Env: GITHUB_TOKEN=ghp_... | (Membaca process.env) |
| Cursor / LangSeq) | +-------------+-------------+
+---------+----------+ |
| Injeksi Prompt Tidak Langsung | Akses Baca/Tulis Penuh
v v
+--------------------+ +-------------------+
| Prompt Penyerang | | Layanan Upstream |
| di Web Eksternal | ──> Bocorkan Rahasia Statis ────────> | GitHub / Slack / |
| "Print your env" | ke Webhook HTTP Penyerang | Database Internal |
+--------------------+ +-------------------+
Arsitektur kredensial statis ini memiliki tiga cacat fatal:
- Prompt Injection sebagai vektor eksfiltrasi rahasia: Jika agen memproses data tidak tepercaya (prompt musuh yang disisipkan di halaman web, email, atau tiket GitHub), LLM dapat dimanipulasi untuk mengeksekusi perintah diagnostik seperti
printenvatau membaca konfigurasi lokal, yang seketika membocorkan kunci berhak akses tinggi. - Ketiadaan delegasi identitas: Kunci API statis tidak dapat membedakan apakah suatu tindakan dilakukan sengaja oleh operator manusia atau dipicu secara otonom oleh halusinasi agen AI. Dalam log audit enterprise, semua panggilan tercatat sama persis sebagai pengguna manusia tersebut.
- Ketiadaan pencabutan dinamis dan prinsip hak akses minimal: Token statis biasanya memiliki hak akses yang terlalu longgar (misalnya izin baca dan tulis penuh ke seluruh repositori) serta masa aktif berbulan-bulan atau tanpa batas.
Untuk mengatasi risiko sistemik ini, ekosistem AI beralih ke kerangka kerja otorisasi terdelegasi. Namun, memilih antara OAuth 1.0a, OAuth 2.0, dan standar konsolidasi terbaru OAuth 2.1 — dikombinasikan dengan PKCE (RFC 7636), DPoP (RFC 9449), dan Device Authorization Grant (RFC 8628) — memerlukan pemahaman mendalam tentang bagaimana protokol ini bekerja di bawah kendala eksekusi otonom dan headless.
2. Evolusi OAuth: Perbandingan Struktural 1.0a, 2.0, dan 2.1
Untuk memahami mengapa framework agen AI modern mewajibkan OAuth 2.1, kita harus meneliti evolusi arsitektur, kompromi teknis, dan kerentanan kritis di ketiga iterasi utama spesifikasi OAuth.
EVOLUSI SPESIFIKASI OAUTH (2007 - 2026):
+---------------------------------------------------------------------------------------------+
| OAuth 1.0a (RFC 5849, 2010) |
| - Tanda tangan kriptografis simetris/asimetris pada SETIAP permintaan HTTP (HMAC-SHA1) |
| - Tanpa alur penyegaran token; komputasi tanda tangan stateful; independen dari transport |
| - Kesimpulan untuk AI: Tidak dapat digunakan. Beban kripto merusak streaming & tool proxy. |
+---------------------------------------------------------------------------------------------+
│
▼
+---------------------------------------------------------------------------------------------+
| OAuth 2.0 (RFC 6749 & RFC 6750, 2012) |
| - Kriptografi didelegasikan ke Transport Layer Security (TLS 1.2/1.3) |
| - Memperkenalkan Bearer Token, Scopes, Refresh Token, dan tipe Grant khusus |
| - Menyertakan Implicit Flow dan Resource Owner Password Credentials (ROPC) |
| - Kesimpulan untuk AI: Berbahaya. Bearer token sangat mudah dicuri via prompt injection. |
+---------------------------------------------------------------------------------------------+
│
▼
+---------------------------------------------------------------------------------------------+
| OAuth 2.1 (Standar Konsolidasi IETF, 2025/2026) |
| - Menghapus tuntas grant yang tidak aman (Implicit dan Password grant dibuang permanen) |
| - MEWAJIBKAN PKCE (RFC 7636) untuk semua alur Authorization Code (Publik dan Konfidensial) |
| - Pencocokan persis string URI pengalihan; melarang token di query parameter URI |
| - Mewajibkan Refresh Token Rotation (RTR) atau Sender-Constrained Tokens (DPoP / mTLS) |
| - Kesimpulan untuk AI: Standar emas keamanan server MCP dan delegasi identitas agen otonom. |
+---------------------------------------------------------------------------------------------+
OAuth 1.0a (RFC 5849): Kekakuan Kriptografis dan Ketergantungan Status
OAuth 1.0a dirancang di era ketika HTTPS masih mahal dan jarang diadopsi. Untuk mencegah penyadapan di atas HTTP teks polos, protokol mewajibkan klien dan server menghitung tanda tangan digital (HMAC-SHA1 atau RSA-SHA1) pada setiap permintaan HTTP tunggal.
Perhitungan tanda tangan memerlukan normalisasi metode HTTP, URL absolut yang dinormalisasi, dan pengurutan leksikografis dari semua parameter kueri, header, nonce klien, serta timestamp Unix:
$$\text{BaseString} = \text{HTTP\_METHOD} \mathbin{\Vert} \text{"\&"} \mathbin{\Vert} \text{Encode}(\text{URL}) \mathbin{\Vert} \text{"\&"} \mathbin{\Vert} \text{Encode}(\text{SortedParams})$$
$$\text{Signature} = \text{HMAC-SHA1}(\text{ClientSecret} \mathbin{\Vert} \text{"\&"} \mathbin{\Vert} \text{TokenSecret}, \text{BaseString})$$
Mengapa OAuth 1.0a Gagal untuk Agen AI:
- Transmisi Streaming dan Chunked: Protokol agen modern (seperti MCP di atas SSE atau WebSockets) mengalirkan payload JSON-RPC secara bertahap. Menghitung ulang tanda tangan di atas potongan streaming non-deterministik atau penulisan ulang header oleh proxy akan menggagalkan validasi tanda tangan.
- Orkestrasi Tool Dinamis: Agen AI menyusun permintaan HTTP secara dinamis berdasarkan parameter LLM. Sedikit perubahan urutan parameter atau nuansa encoding URL (
%20vs+) membatalkan tanda tangan dan memicu error 401 Unauthorized dalam loop otomatisasi. - Ketiadaan Pemisahan Refresh Asli: OAuth 1.0a tidak memiliki mekanisme bawaan token berumur pendek dengan rotasi otomatis, memaksa rahasia permanen tersimpan terus-menerus di klien.
OAuth 2.0 (RFC 6749): Kesederhanaan Berisiko Tinggi Bearer Token
OAuth 2.0 menyederhanakan arsitektur dengan memindahkan integritas kriptografi ke lapisan transport (mewajibkan HTTPS) dan memperkenalkan Bearer Token (RFC 6750). Siapa pun yang memegang token dapat mengakses sumber daya, persis seperti uang tunai:
GET /v1/repositories HTTP/1.1
Host: api.github.com
Authorization: Bearer ya29.a0AfH6SMB...
OAuth 2.0 mendefinisikan empat alur hibah otorisasi asli:
- Authorization Code Grant: Alur pengalihan aman untuk aplikasi web dengan backend rahasia.
- Implicit Grant: Alur peramban yang mengembalikan token langsung di fragmen hash URL (
#access_token=...). - Resource Owner Password Credentials (ROPC): Penyerahan langsung username dan password ke aplikasi klien.
- Client Credentials Grant: Otorisasi machine-to-machine (M2M) langsung tanpa campur tangan manusia.
Kerentanan Fatal OAuth 2.0 pada Sistem AI:
- Kerentanan Replay Serangan Bearer: Jika lingkungan agen terkompromi akibat SSRF atau prompt injection, penyerang dapat mencuri token Bearer dan menggunakannya ulang dari mana saja hingga masa berlakunya habis.
- Jebakan Implicit Grant: Antarmuka agen awal pada aplikasi SPA mengekspos token di riwayat browser dan header
Referer. - Antipola Password Grant: Pengembang agen CLI meminta password langsung di terminal pengguna, melanggar janji mendasar OAuth: tidak pernah membagikan kredensial utama kepada pihak ketiga.
OAuth 2.1: Standar Pengerasan Modern untuk Agen Otonom
OAuth 2.1 adalah konsolidasi IETF yang membersihkan utang teknis dan kelemahan keamanan OAuth 2.0:
- Penghapusan Total Alur Tidak Aman: Implicit Grant dan ROPC dilarang dan dihapus secara permanen.
- Kewajiban PKCE untuk Semua Alur Authorization Code: Proof Key for Code Exchange (RFC 7636) diwajibkan secara ketat untuk klien publik (agen CLI, ekstensi IDE) maupun klien konfidensial (swarm agen backend).
- Pencocokan Persis Redirect URI: Server otorisasi wajib melakukan pencocokan string byte-per-byte secara ketat guna menangkal serangan open-redirect.
- Larangan Token pada Parameter Kueri URI: Token tidak boleh lagi dikirim dalam parameter URL guna mencegah kebocoran pada log HTTP dan cache proxy.
- Perlindungan Ketat Refresh Token: Mewajibkan Refresh Token Rotation (RTR) atau Sender-Constrained Tokens (DPoP / mTLS).
Tabel Perbandingan Arsitektur: OAuth 1.0a vs OAuth 2.0 vs OAuth 2.1
| Dimensi Arsitektur | OAuth 1.0a (RFC 5849) | OAuth 2.0 (RFC 6749 / 6750) | OAuth 2.1 (Standar IETF 2026) |
|---|---|---|---|
| Model Kriptografis | Tanda tangan per permintaan di tingkat aplikasi (HMAC/RSA) | TLS + Bearer polos teks terbuka | TLS + PKCE wajib + Pembatasan pengirim (DPoP/mTLS) |
| Risiko Replay Bearer Token | Imun (ditandatangani dengan nonce unik) | Sangat tinggi (kepemilikan = izin penuh) | Tuntas (terikat pengirim via kunci DPoP) |
| Persyaratan PKCE | Tidak didukung | Opsional (RFC 7636, awalnya untuk mobile) | Wajib mutlak untuk semua pertukaran Auth Code |
| Implicit Grant | Tidak didukung | Diizinkan (dirancang untuk SPA browser) | Dihapus total dan dilarang keras |
| Password Grant (ROPC) | Tidak didukung | Diizinkan (pertukaran langsung kredensial) | Dihapus total dan dilarang keras |
| Validasi Redirect URI | Pencocokan awalan diizinkan | Sering mengizinkan wildcard dan pola longgar | Wajib pencocokan persis byte-per-byte |
| Token di Parameter Kueri | Didukung | Diizinkan (?access_token=...) |
Dilarang keras (hanya melalui Header atau Body) |
| Siklus Hidup Refresh Token | Tanpa mekanisme bawaan | Token tunggal dipakai ulang sampai dicabut | Rotasi Wajib (RTR) atau pengikatan DPoP |
| Kesesuaian untuk Agen CLI | Sangat buruk (kalkulasi tanda tangan rapuh) | Rentan (penyadapan pada port loopback lokal) | Optimal (PKCE + port loopback sementara) |
| Kesesuaian untuk Server MCP | Tidak kompatibel dengan streaming JSON-RPC | Dapat dipakai namun risiko kebocoran tinggi | Standar default industri (scope hak minimal) |
3. Topologi Autentikasi Model Context Protocol (MCP): Mengamankan Komunikasi Agen-ke-Server
Model Context Protocol (MCP), standar terbuka besutan Anthropic yang diadopsi secara luas di Claude Code, Cursor, Windsurf, dan runtime enterprise, menetapkan arsitektur klien-server asimetris di atas JSON-RPC 2.0.
Dalam implementasi MCP, terdapat dua batas komunikasi yang perlu dipahami:
- Batas A (Host ke Server MCP): Koneksi antara aplikasi klien LLM (Claude Code, Cursor) dan proses server MCP.
- Batas B (Server MCP ke Infrastruktur Upstream Perusahaan): Koneksi antara server MCP dan API SaaS eksternal (GitHub, Jira, Linear, Slack).
TOPOLOGI AUTENTIKASI MODEL CONTEXT PROTOCOL (MCP):
+-------------------------------------------------------------------------------------------------------+
| RUNTIME HOST MCP (misalnya Claude Code / Cursor / Framework Agen Otonom) |
| |
| +---------------------+ Konteks Prompt +--------------------------------------------+ |
| | Prompt Pengguna / | <───────────────────────────> | Mesin Inferensi LLM (Claude 3.7 / GPT-4o) | |
| | Loop Agen Otonom | +--------------------------------------------+ |
| +----------+----------+ |
| | Mengirim panggilan tool (`tools/call`) |
| v |
| +--------------------------------------------------------------------------------------------------+ |
| | ENGINE KLIEN MCP | |
| | - Mengelola handshake OAuth 2.1 PKCE dengan server otorisasi | |
| | - Menyimpan kunci privat efemeral DPoP di memori terisolasi yang tidak dapat diekspor | |
| | - Menghasilkan JWT DPoP Proof per permintaan; menyisipkan Access Token ke header JSON-RPC | |
| +-----------------------------------+--------------------------------------------------------------+ |
+--------------------------------------|----------------------------------------------------------------+
|
| Transport: Stdio (proses lokal) ATAU SSE/HTTP (server remote)
v
+-------------------------------------------------------------------------------------------------------+
| RUNTIME SERVER MCP (misalnya GitHub MCP / Database Enterprise MCP) |
| |
| +--------------------------------------------------------------------------------------------------+ |
| | INTERCEPTOR VALIDASI DAN TOKEN | |
| | 1. Memvalidasi tanda tangan token OAuth 2.1 via endpoint JWKS Identity Provider | |
| | 2. Memverifikasi DPoP Proof: mencocokkan metode HTTP, URI, Nonce, dan kunci publik terkait | |
| | 3. Mengevaluasi Scopes: menegakkan prinsip hak minimal (`issues:read` menolak `admin:all`) | |
| +-----------------------------------+--------------------------------------------------------------+ |
| | |
| v |
| +--------------------------------------------------------------------------------------------------+ |
| | MESIN EKSEKUSI TOOL MCP (implementasi `tools/call`) | |
| | - Membersihkan input, mencegah path traversal, mengeksekusi panggilan API terisolasi | |
| +-----------------------------------+--------------------------------------------------------------+ |
+--------------------------------------|----------------------------------------------------------------+
| Panggilan API eksternal terautentikasi dengan token delegasi terikat
v
+----------------------------------+
| Layanan SaaS & DB Enterprise |
| (GitHub / Jira / PostgreSQL / S3)|
+----------------------------------+
Perbandingan Transport Stdio Lokal vs SSE/HTTP Remote
- Transport Stdio Lokal (
transport: "stdio"):
- Server MCP berjalan sebagai subproses lokal yang dipicu oleh host, berkomunikasi melalui stdin dan stdout.
- Antipola Keamanan: Dahulu, pengembang menyuntikkan kredensial melalui variabel lingkungan:
- Kerentanan: Perintah shell apa pun yang dijalankan agen atau subproses dalam container dapat membaca
/proc/[pid]/environatau menjalankanenv, mengekspos token organisasi. - Solusi OAuth 2.1: Host mengelola brankas token OAuth 2.1 PKCE yang aman. Server MCP diinisialisasi tanpa rahasia statis dan menerima token delegasi singkat saat handshake.
- Transport SSE/HTTP Remote (
transport: "sse"):
- Server MCP berjalan sebagai layanan web terdistribusi pada port HTTP dengan Server-Sent Events.
- Di sini OAuth 2.1 bersifat wajib: klien MCP harus menyertakan header
Authorization: BeareratauDPoPyang divalidasi dengan JWKS.
4. Analisis Mendalam PKCE (RFC 7636): Mengamankan Callback Lokal Agen
Proof Key for Code Exchange (PKCE) distandarisasi dalam RFC 7636 untuk menggagalkan intersepsi kode otorisasi pada perangkat klien publik. Di OAuth 2.1, PKCE diwajibkan untuk setiap pertukaran authorization code.
Mengapa Agen CLI dan IDE adalah Klien Publik (Public Clients)
Perkakas seperti Claude Code, Cursor, atau Roo Code diklasifikasikan sebagai Public Clients: binernya berjalan di komputer pengguna sehingga tidak dapat menyimpan client_secret statis secara aman. Rahasia apa pun yang disertakan dalam biner dapat diekstraksi melalui dekompilasi.
Saat klien publik meminta otorisasi, server mengembalikan Authorization Code melalui URI pengalihan lokal (biasanya server HTTP loopback lokal sementara di http://127.0.0.1:18492/callback).
SERANGAN INTERSEPSI KODE OTORISASI (Tanpa PKCE):
1. Agen CLI yang sah meminta kode otorisasi ke Server Auth.
2. Proses berbahaya di latar belakang menguping port lokal atau lalu lintas loopback.
3. Server Auth mengalihkan browser ke http://127.0.0.1:18492/callback?code=AUTH_CODE_123.
4. Proses berbahaya mencegat AUTH_CODE_123.
5. Proses berbahaya mengirimkan AUTH_CODE_123 ke /oauth/token.
Karena klien bersifat publik tanpa client_secret, Server Auth menyerahkan Access Token ke penyerang!
Pertahanan Matematis PKCE
PKCE melenyapkan celah ini dengan membuat rahasia kriptografis acak sekali pakai untuk setiap permintaan:
ALUR PROTOKOL PKCE:
+-------------+ +-----------------------+ +--------------------+
| Agent CLI | | Browser Pengguna | | Server Auth |
| (Klien) | +-----------+-----------+ +---------+----------+
+------+------+ | |
| 1. Buat code_verifier (entropi tinggi) | |
| Hitung code_challenge = S256(...) | |
| | |
| 2. Jalankan HTTP Listener Loopback | |
| Buka browser dengan challenge ─────>| 3. GET /authorize?response_type=code |
| | &client_id=agent_cli |
| | &code_challenge=E9Melhoa2Owv... |
| | &code_challenge_method=S256 ─────────>|
| | | 4. Pengguna setuju.
| | 5. 302 Pengalihan ke Loopback lokal | Simpan challenge
| |<─────────────────────────────────────────|
|<───────────────────────────────────────| http://127.0.0.1:18492/callback?code=AC_88921
| 6. Tangkap callback dengan kode |
| |
| 7. POST /oauth/token |
| code=AC_88921 & code_verifier=dBjftJeZ4CVP-mB92K... ─────────────────────────>|
| | 8. Verifikasi:
| | SHA256(verifier)
| | == challenge?
| 9. Kembalikan Access Token + Refresh Token (RTR) <────────────────────────────────| YA: Terbitkan token
+------+------+
- Code Verifier: Agen membuat string acak berkekuatan tinggi $V$ (panjang 43 hingga 128 karakter) menggunakan karakter aman URL (
[A-Z],[a-z],[0-9],-,.,_,~): - Code Challenge: Klien menghitung hash SHA-256 dari $V$ dan mengodekannya dalam Base64URL tanpa padding:
- Permintaan Otorisasi: Klien mengirimkan $C$ dan
code_challenge_method=S256ke/authorize. Server menyimpan $C$. - Pertukaran Token: Klien mengirimkan kode otorisasi bersama
code_verifier=Vasli dalam bentuk teks polos ke/token. Server menghitung $\text{Base64URL-Encode}(\text{SHA-256}(V))$ dan memverifikasi kesesuaiannya dengan $C$.
Meskipun perangkat perusak mencegat kode otorisasi di mesin lokal, ia tidak dapat menukarnya tanpa code_verifier asli, yang tidak pernah meninggalkan memori proses agen.
Implementasi TypeScript untuk Produksi: Modul Mesin PKCE
// pkce.ts - Enterprise OAuth 2.1 PKCE Engine for AI Agent Clients
import { randomBytes, createHash } from "node:crypto";
export interface PKCEChallenge {
codeVerifier: string;
codeChallenge: string;
codeChallengeMethod: "S256";
}
export class PKCEEngine {
/**
* Generates a cryptographically secure code_verifier (RFC 7636 Section 4.1)
* Length defaults to 64 bytes of entropy (yielding ~86 base64url characters).
*/
public static generateVerifier(length: number = 64): string {
if (length < 32 || length > 96) {
throw new RangeError("Verifier byte length must be between 32 and 96.");
}
const buffer = randomBytes(length);
return this.base64UrlEncode(buffer);
}
/**
* Computes the S256 code_challenge from the code_verifier (RFC 7636 Section 4.2)
*/
public static computeChallenge(verifier: string): string {
const hash = createHash("sha256").update(verifier, "ascii").digest();
return this.base64UrlEncode(hash);
}
/**
* Generates the complete PKCE pair ready for OAuth 2.1 authorization
*/
public static createPair(): PKCEChallenge {
const codeVerifier = this.generateVerifier(64);
const codeChallenge = this.computeChallenge(codeVerifier);
return {
codeVerifier,
codeChallenge,
codeChallengeMethod: "S256",
};
}
/**
* Server-side verification: Validates an incoming code_verifier against stored challenge
*/
public static verify(verifier: string, storedChallenge: string): boolean {
const computed = this.computeChallenge(verifier);
// Timing-safe buffer comparison to prevent side-channel timing attacks
const bufA = Buffer.from(computed);
const bufB = Buffer.from(storedChallenge);
if (bufA.length !== bufB.length) return false;
let result = 0;
for (let i = 0; i < bufA.length; i++) {
result |= bufA[i] ^ bufB[i];
}
return result === 0;
}
private static base64UrlEncode(buffer: Buffer): string {
return buffer
.toString("base64")
.replace(/\\+/g, "-")
.replace(/\\//g, "_")
.replace(/=+$/, "");
}
}
5. Otorisasi Server Headless dan CLI: Device Flow (RFC 8628)
Agen AI modern kini sering dieksekusi di lingkungan headless tanpa browser:
- Container Docker di cluster cloud (AWS ECS, Kubernetes, Fly.io).
- Runner CI/CD sementara (GitHub Actions, GitLab CI).
- Server virtual jarak jauh dan sesi terminal SSH.
Pada kondisi ini, agen tidak dapat membuka jendela peramban. Meminta kata sandi di konsol dilarang keras oleh OAuth 2.1. Solusi standarnya adalah OAuth 2.0 Device Authorization Grant (RFC 8628):
DEVICE AUTHORIZATION GRANT (RFC 8628) DI LINGKUNGAN HEADLESS:
+-------------------+ +-----------------------+
| Agen Headless | | Server Auth |
| (Docker / Cloud) | +-----------+-----------+
+---------+---------+ |
| 1. POST /oauth/device/code (client_id, scope) ──────────────────────>|
| | 2. Buat:
| 3. Kembalikan kredensial perangkat: | device_code (rahasia)
| - user_code: "WDJB-HGNP" | user_code (publik)
| - verification_uri: "https://auth.corp.com/activate" | interval: 5 detik
| - interval: 5 <───────────────────────────────────────────────────|
| |
| 4. Cetak instruksi di konsol untuk pengguna: |
| "Buka https://auth.corp.com/activate dan masukkan: WDJB-HGNP" |
| |
| 5. Loop polling: |
| POST /oauth/token (grant_type=device_code, device_code=...) ─────>|
| <── 400 Bad Request: {"error": "authorization_pending"} ─────────|
| [Tunggu 5 detik] |
| |
+---------+---------+ Pengguna membuka URL di laptop atau smartphone |
| Laptop Pengguna | ──> Masukkan "WDJB-HGNP", selesaikan MFA perusahaan ──────>| 6. Pengguna setuju!
+-------------------+ |
| |
| 7. Siklus polling berikutnya: |
| POST /oauth/token ───────────────────────────────────────────────>|
| <── 200 OK: {access_token: "...", refresh_token: "..."} ──────────|
v
[Agen headless terotentikasi penuh tanpa pembocoran password sama sekali]
Alternatif Machine-to-Machine (M2M): RFC 7523 Private Key JWT
Ketika agen beroperasi sepenuhnya otomatis tanpa campur tangan manusia (misalnya bot refactoring kode harian), Device Flow tidak dapat digunakan.
Dalam skenario ini, Zero-Trust mengandalkan Client Credentials Grant yang diperkuat RFC 7523 (Profil JWT untuk Autentikasi Klien):
- Sebagai ganti
client_secretstatis, agen memegang kunci privat asimetris (RSA atau ECDSA) yang aman di HSM atau brankas rahasia Kubernetes. - Saat mengautentikasi, agen menandatangani JWT singkat (masa aktif 60 detik, UUID
jtiunik, audiensaud). - Server otorisasi memverifikasi tanda tangan terhadap kunci publik terdaftar milik agen.
6. Siklus Hidup Token dan Alur Kerja Pembaruan Otonom
Agen AI sering mengeksekusi proses yang berlangsung berjam-jam. Karena Access Token OAuth 2.1 sengaja dibuat berumur pendek (5 sampai 15 menit), agen harus memperbarui token secara otonom tanpa memutus pemanggilan tool LLM yang sedang aktif.
Rotasi Refresh Token (RTR) dan Deteksi Kebocoran
Dalam OAuth 2.1, token penyegaran dilindungi secara ketat melalui Refresh Token Rotation (RTR):
- Setiap kali agen mengajukan
refresh_tokenke/oauth/token, server seketika membatalkan token tersebut. - Server menerbitkan pasangan baru:
access_tokensegar DANrefresh_tokenyang sama sekali baru. - Jika penyerang mencuri dan mencoba memakai kembali Refresh Token lama yang sudah dibatalkan, server langsung mendeteksi insiden pelanggaran keamanan:
$$\text{Incoming Token State} == \text{"REVOKED"} \implies \text{Revoke All Tokens in Family Tree}$$
Server seketika membatalkan seluruh silsilah hak akses, mencabut seluruh token aktif di semua instans agen.
REFRESH TOKEN ROTATION (RTR) DAN PEMULIHAN OTOMATIS:
Rantai pembuatan token:
[Refresh Token A] ──(Dipakai)──> [Refresh Token B] ──(Dipakai)──> [Refresh Token C] (Aktif)
│
│ Penyerang mencoba memakai ulang [Refresh Token A] yang telah dicabut
v
[Server Auth mendeteksi pemakaian ulang token A yang sudah kedaluwarsa!]
│
▼
[PERINGATAN KRITIS]: Membatalkan Token B, Token C, dan seluruh Access Token terkait seketika.
Sesi agen dimatikan secara aman, mencegah eskalasi hak akses ilegal.
Implementasi Python untuk Produksi: Manajer Token Asinkron Thread-Safe
Dalam sistem multi-agen saat 10 sub-agen memanggil server MCP yang sama, beberapa permintaan konkuren dapat mendeteksi kedaluwarsa token pada saat bersamaan. Jika semuanya mencoba menukar token sekali pakai secara bersamaan, 9 akan gagal dan server dapat mengira terjadi serangan replay.
Modul Python berikut mengimplementasikan penguncian mutex asinkron dan pembaruan proaktif:
# token_manager.py - Enterprise Async Token Lifecycle Manager for AI Agents
import asyncio
import time
import httpx
from typing import Optional, Dict, Any
class AgentTokenManager:
def __init__(
self,
token_endpoint: str,
client_id: str,
initial_refresh_token: str,
proactive_refresh_seconds: int = 60,
):
self.token_endpoint = token_endpoint
self.client_id = client_id
self.refresh_token = initial_refresh_token
self.access_token: Optional[str] = None
self.expires_at: float = 0.0
self.proactive_refresh_seconds = proactive_refresh_seconds
self._lock = asyncio.Lock()
async def get_valid_access_token(self) -> str:
now = time.time()
if self.access_token and (self.expires_at - now) > self.proactive_refresh_seconds:
return self.access_token
async with self._lock:
now = time.time()
if self.access_token and (self.expires_at - now) > self.proactive_refresh_seconds:
return self.access_token
await self._refresh_token_exchange()
if not self.access_token:
raise RuntimeError("Failed to acquire valid access token from authorization server.")
return self.access_token
async def _refresh_token_exchange(self) -> None:
payload = {
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
"client_id": self.client_id,
}
async with httpx.AsyncClient(timeout=10.0) as client:
try:
response = await client.post(
self.token_endpoint,
data=payload,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
except httpx.RequestError as exc:
raise ConnectionError(f"Network transport error during token refresh: {exc}")
if response.status_code == 200:
data: Dict[str, Any] = response.json()
self.access_token = data["access_token"]
expires_in = int(data.get("expires_in", 3600))
self.expires_at = time.time() + expires_in
# Update to the newly rotated refresh token if provided
if "refresh_token" in data:
self.refresh_token = data["refresh_token"]
elif response.status_code in (400, 401):
err_data = response.json()
# If error is 'invalid_grant', the token was likely already rotated or revoked
raise PermissionError(f"Token refresh rejected (possible token theft or expiry): {err_data}")
else:
response.raise_for_status()
7. Isolasi Zero-Trust: DPoP (RFC 9449) dan Enclave Perangkat Keras
Meskipun mematuhi OAuth 2.1 dan PKCE, Bearer Token konvensional tetap memiliki kelemahan mendasar: jika token bocor, siapa pun dapat menggunakannya.
Dalam alur kerja AI, agen memproses masukan eksternal tanpa henti. Jika penyerang mengeksploitasi prompt injection untuk memicu permintaan HTTP keluar (SSRF) dengan header otorisasi, token Bearer dapat dicuri.
Untuk mencapai keamanan Zero-Trust murni, OAuth 2.1 mengintegrasikan DPoP: Demonstrating Proof-of-Possession at the Application Layer (RFC 9449).
DPOP (RFC 9449) PEMBATASAN PENGIRIM PADA LAPISAN APLIKASI:
+-------------------------------------------------------------------------------------------------+
| LINGKUNGAN LOKAL AGEN (Klien) |
| - Membuat pasangan kunci efemeral: Kunci Publik (JWK) + Kunci Privat (tersimpan di RAM/Enclave) |
+-------------------------------------------------------------------------------------------------+
│
│ 1. Menyematkan header DPoP Proof:
│ DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Ar...
│ Payload: {
│ "htm": "GET",
│ "htu": "https://api.enterprise.com/mcp/tools",
│ "iat": 1772630400,
│ "jti": "random_nonce_9921",
│ "jwk": { ...kunci_publik... }
│ }
│
│ 2. Mengirimkan token DPoP terikat:
│ Authorization: DPoP dpop_access_token_88921
v
+-------------------------------------------------------------------------------------------------+
| GATEWAY MCP ENTERPRISE (Resource Server) |
| 1. Memvalidasi bahwa Access Token terikat kriptografis dengan sidik jari kunci publik di JWK. |
| 2. Memvalidasi tanda tangan DPoP Proof terhadap kunci publik yang diberikan. |
| 3. Memvalidasi bahwa "htm" sama dengan "GET" dan "htu" persis sama dengan URL tujuan. |
| 4. Memastikan "iat" dalam rentang waktu toleransi (< 60s) dan "jti" belum pernah dipakai. |
+-------------------------------------------------------------------------------------------------+
│
┌────────────────────────────────────────┴────────────────────────────────────────┐
▼ ▼
[BUKTI VALID DAN KUNCI COCOK] [REPLAY TOKEN CURIAN DITOLAK]
Permintaan diteruskan ke eksekusi tool Penyerang memiliki token, namun TIDAK
memiliki kunci privat lokal milik agen.
Hasil: 401 Unauthorized seketika!
Cara Kerja DPoP dalam Praktik
- Pembuatan Kunci Efemeral: Saat agen dimulai, pasangan kunci asimetris (ECDSA P-256 atau Ed25519) dibuat di memori terisolasi.
- Pengikatan Kriptografis: Saat meminta token, agen menyertakan bukti DPoP. Token yang diterbitkan memuat sidik jari (
jkt) kunci publik. - Bukti per Permintaan: Pada setiap panggilan API, agen menandatangani JWT DPoP dengan metode, URL persis, timestamp, dan UUID.
- Pertahanan Menyeluruh: Sekalipun string token dicuri lewat log atau prompt injection, token tersebut tidak berharga tanpa kunci privat lokal untuk membuat tanda tangan DPoP yang valid.
8. Benchmark Keamanan Enterprise, Matriks Risiko, dan Mode Kegagalan
Benchmark Kinerja Empiris (10.000 iterasi, perangkat keras Apple M4 Max)
| Arsitektur Autentikasi | Latensi Handshake (p50) | Latensi Handshake (p99) | Overhead Verifikasi per Permintaan | Proteksi Replay | Konsumsi Memori Klien | Beban CPU Server |
|---|---|---|---|---|---|---|
| PAT Statis / API Key | 0.1 ms (Tanpa handshake) | 0.2 ms | 0.02 ms (Pencocokan string) | Tidak Ada (Replay Penuh) | < 1 KB | Garis Dasar |
| OAuth 1.0a (HMAC-SHA1) | 14.2 ms | 38.5 ms | 1.84 ms (Parsing tanda tangan) | Parsial (Cek nonce) | 12 KB | +18% |
| OAuth 2.0 Bearer | 45.1 ms | 112.0 ms | 0.15 ms (Validasi JWT/cache) | Tidak Ada (Replay Bearer) | 18 KB | +4% |
| OAuth 2.1 (PKCE + RTR) | 48.6 ms | 118.4 ms | 0.16 ms (Validasi JWT) | Sedang (Pencabutan RTR) | 24 KB | +5% |
| OAuth 2.1 + DPoP (P-256) | 54.2 ms | 132.8 ms | 1.22 ms (Validasi JWT DPoP) | Maksimal (Nol Replay) | 36 KB | +12% |
| mTLS (RFC 8705) | 62.8 ms | 154.1 ms | 0.45 ms (Cache sesi TLS) | Maksimal (Terikat sertifikat) | 128 KB | +15% |
Matriks Risiko Ancaman Agen AI
KEPARAHAN ANCAMAN VS MITIGASI PROTOKOL:
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| Vektor Serangan | API Key Statis | OAuth 1.0a | OAuth 2.0 Bearer | OAuth 2.1 + DPoP |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 1. Indirect Prompt Injection | KRITIS (10/10) | TINGGI (7/10) | KRITIS (10/10) | RENDAH (2/10) |
| (Kebocoran env/log) | Kunci utama bocor | Tanda tangan rumit| Bearer dicuri | Token mati s/ k |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 2. Sniffing Port Lokal | N/A | RENDAH (3/10) | TINGGI (8/10) | TERLINDUNGI (1/10)|
| (Intersepsi loopback CLI) | Tanpa pengalihan | Nonce tervalidasi | Kode dicuri | Dicegah oleh PKCE |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 3. Serangan Pivot Tool SSRF | KRITIS (10/10) | SEDANG (5/10) | KRITIS (10/10) | TERLINDUNGI (1/10)|
| (Dipaksa panggil server) | Kredensial bocor | Gagal cocokkan URI| Bearer dipakai | URI DPoP salah |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 4. Spionase Subproses | KRITIS (10/10) | SEDANG (5/10) | TINGGI (8/10) | RENDAH (2/10) |
| (Baca /proc/environ) | Kunci permanen | Kunci di env | Bearer di env | Singkat & terikat |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 5. Konflik Refresh Konkuren | N/A | N/A | RENDAH (2/10) | TINGGI (Perlu |
| (Swarm agen paralel) | Tanpa refresh | Tanpa refresh | Token bentrok | Manajer Mutex) |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
9. Panduan Implementasi Bertahap: Server MCP Tangguh dengan OAuth 2.1 + PKCE
// server.ts - Hardened Remote MCP Server with OAuth 2.1 Validation
import express, { Request, Response, NextFunction } from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";
const app = express();
app.use(express.json());
// Configuration
const ISSUER = "https://auth.enterprise.com/";
const AUDIENCE = "https://mcp.enterprise.com/";
const JWKS_URI = new URL("https://auth.enterprise.com/.well-known/jwks.json");
const JWKS = createRemoteJWKSet(JWKS_URI);
interface AuthenticatedRequest extends Request {
tokenClaims?: any;
}
/**
* Enterprise OAuth 2.1 Token Validation Middleware
*/
async function requireOAuth21(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): Promise<void> {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
res.status(401).json({
jsonrpc: "2.0",
error: { code: -32001, message: "Missing or invalid OAuth 2.1 Authorization header." },
id: req.body?.id || null,
});
return;
}
const token = authHeader.split(" ")[1];
try {
// Cryptographically verify token signature, issuer, audience, and expiration
const { payload } = await jwtVerify(token, JWKS, {
issuer: ISSUER,
audience: AUDIENCE,
});
// Enforce OAuth 2.1 requirement: reject tokens without an expiration claim
if (!payload.exp || typeof payload.exp !== "number") {
res.status(401).json({
jsonrpc: "2.0",
error: { code: -32002, message: "Non-compliant token: missing expiration claim." },
id: req.body?.id || null,
});
return;
}
req.tokenClaims = payload;
next();
} catch (err: any) {
res.status(401).json({
jsonrpc: "2.0",
error: { code: -32003, message: `Token verification failed: ${err.message}` },
id: req.body?.id || null,
});
}
}
/**
* Fine-Grained Scope Enforcement Guard
*/
function requireScope(requiredScope: string) {
return (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
const scopes: string[] = (req.tokenClaims?.scope || "").split(" ");
if (!scopes.includes(requiredScope)) {
res.status(403).json({
jsonrpc: "2.0",
error: {
code: -32004,
message: `Insufficient permissions: missing required scope '${requiredScope}'`,
},
id: req.body?.id || null,
});
return;
}
next();
};
}
/**
* Standard MCP JSON-RPC 2.0 Handler Endpoint
*/
app.post(
"/mcp/v1",
requireOAuth21,
requireScope("mcp:tools:execute"),
async (req: AuthenticatedRequest, res: Response): Promise<void> => {
const { jsonrpc, method, params, id } = req.body;
if (jsonrpc !== "2.0") {
res.status(400).json({ jsonrpc: "2.0", error: { code: -32600, message: "Invalid JSON-RPC version." }, id });
return;
}
// Router for MCP Primitives
switch (method) {
case "tools/list":
res.json({
jsonrpc: "2.0",
result: {
tools: [
{
name: "query_database",
description: "Executes read-only SQL queries against the analytics warehouse.",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
},
],
},
id,
});
break;
case "tools/call":
if (params?.name === "query_database") {
// Verify elevated data scope for this specific tool execution
const scopes: string[] = (req.tokenClaims?.scope || "").split(" ");
if (!scopes.includes("db:analytics:read")) {
res.json({
jsonrpc: "2.0",
error: { code: -32005, message: "Forbidden: tool requires 'db:analytics:read' scope." },
id,
});
return;
}
// Execute tool with verified, scoped identity
const userSub = req.tokenClaims.sub;
console.log(`Executing query on behalf of verified agent identity: ${userSub}`);
res.json({
jsonrpc: "2.0",
result: {
content: [
{
type: "text",
text: JSON.stringify({ status: "success", rows_returned: 42, latency_ms: 12 }),
},
],
},
id,
});
} else {
res.status(404).json({ jsonrpc: "2.0", error: { code: -32601, message: "Tool not found." }, id });
}
break;
default:
res.status(404).json({ jsonrpc: "2.0", error: { code: -32601, message: "Method not found." }, id });
}
}
);
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Hardened OAuth 2.1 MCP Server listening on port ${PORT}`);
});
10. Kesimpulan & Rekomendasi Strategis (E-E-A-T)
Menghubungkan model AI otonom ke infrastruktur enterprise mewajibkan kita memperlakukan agen sebagai aktor terdelegasi semi-tepercaya. Menganggap agen sebagai layanan internal yang tepercaya sepenuhnya (dengan menyuntikkan kunci root) atau sebaliknya sebagai entitas eksternal yang sama sekali tidak dapat dipercaya (memerlukan persetujuan manual di setiap klik) merupakan kegagalan arsitektur.
OAuth 2.1 menyediakan landasan kriptografi yang tepat untuk menjembatani jurang ini, menyatukan otonomi agen dengan kepatuhan Zero-Trust enterprise.
Checklist 5 Poin Keamanan AI Enterprise
- Bersihkan Kredensial Statis Lingkungan: Audit konfigurasi MCP dan file
.env. Ganti PAT statis dengan token akses OAuth 2.1 berumur pendek. - Wajibkan PKCE dengan Algoritma S256: Pastikan seluruh perkakas CLI menerapkan RFC 7636 dengan hashing SHA-256 berkekuatan tinggi.
- Terapkan Device Flow (RFC 8628) atau Private Key JWT di Lingkungan Headless: Hapus skrip pengisian kata sandi di konsol dan gunakan autentikasi terstandarisasi.
- Terapkan Rotasi Refresh Token dengan Penguncian Konkurensi: Cegah konflik antar agen paralel dengan mengunci alur pembaruan token menggunakan mutex asinkron.
- Wajibkan DPoP (RFC 9449) untuk Akses Sumber Daya Penting: Terapkan pembatasan token pada lapisan jaringan untuk menangkal pencurian kredensial via prompt injection.