← Kembali ke Blog

Amankan Remote MCP Server dengan OAuth 2.1

Remote MCP server kamu cuma nerima satu bearer token, dan token itu sama untuk semua client. Token tersebut jadi seluruh batas keamananmu. Siapa pun yang bisa baca token itu dari file config, log, atau screenshot di tiket support bisa memanggil semua tool yang kamu buka, dan kamu tidak bisa bilang user mana yang melakukan apa.

Spec authorization MCP punya jalan keluarnya, dan kamu tidak perlu bikin sistem login sendiri. Remote MCP server itu OAuth 2.1 resource server: dia memvalidasi token yang diterbitkan sistem lain, dan tidak pernah melihat password. Yang susah bukan konsepnya. Yang susah adalah beberapa MUST di sisi resource server yang sering dilewatkan.

Panduan ini jalan lewat checklist sisi server pakai FastMCP 2.11+ dan menunjukkan bentuk HTTP mentahnya, jadi langkah yang sama tetap berlaku kalau kamu pakai TypeScript SDK atau menulis server sendiri.

Yang perlu kamu siapkan

  • Python 3.10+ dan FastMCP 2.11 ke atas (pip install "fastmcp>=2.11")
  • Authorization server OAuth 2.1: Keycloak, Auth0, WorkOS AuthKit, atau OIDC provider apa pun yang sudah kamu jalankan
  • URL HTTPS publik untuk server. Tiap endpoint authorization wajib HTTPS, dan redirect URI harus HTTPS atau localhost.
  • MCP Inspector untuk uji coba (npx @modelcontextprotocol/inspector)

Tentukan transport dulu

Spec tegas soal pemisahan ini. Server STDIO SHOULD NOT memakai flow ini dan seharusnya ambil credential dari environment. Server HTTP SHOULD mengikutinya. Kalau server kamu jalan sebagai subprocess lokal di dalam editor orang, API key dari env var adalah jawaban yang benar dan artikel ini tidak berlaku untukmu.

Begitu server bisa diakses lewat Streamable HTTP oleh client yang tidak kamu kontrol, model env var ambruk. Kamu tidak bisa membagi satu secret ke semua user, dan kamu tidak bisa mencabutnya untuk satu orang tanpa merusak yang lain.

Kenali tiga perannya

Memahami peran ini adalah separuh desainnya.

  • MCP server: OAuth 2.1 resource server. Dia memvalidasi token dan menyajikan tool. Dia tidak pernah menerbitkan token.
  • MCP client: OAuth 2.1 client, bertindak atas nama user.
  • Authorization server: sistem yang mengautentikasi user dan menerbitkan token. Bisa milikmu sendiri atau IdP eksternal.

Kalau kamu mulai merender form login di dalam MCP server, berarti kamu sudah menggabungkan resource server dengan authorization server dan sebentar lagi kamu mengimplementasi OAuth dengan cara yang salah. Serahkan login ke IdP.

Langkah 1: Validasi token, bukan sekadar keberadaannya

Pemeriksaan paling penting adalah validasi audience. Token yang bertanda tangan sah belum tentu token yang sah untuk server kamu. Kalau server menerima token apa pun yang signature-nya bisa diverifikasi, token yang diterbitkan untuk layanan lain bisa langsung masuk. Spec menjadikannya MUST: server harus memastikan token itu diterbitkan khusus untuknya.

Di FastMCP, pasang verifier ke server:

# server.py
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier

verifier = JWTVerifier(
    jwks_uri=os.environ["JWKS_URI"],        # https://auth.example.com/.well-known/jwks.json
    issuer=os.environ["TOKEN_ISSUER"],      # https://auth.example.com
    audience=os.environ["TOKEN_AUDIENCE"],  # resource URI server ini
    required_scopes=["files:read"],
)

mcp = FastMCP(name="Files Server", auth=verifier)

@mcp.tool
def read_note(path: str) -> str:
    return open(path).read()

JWTVerifier mengambil public key dari endpoint JWKS lalu memeriksa signature, iss, aud, dan exp di tiap request. Key bisa dirotasi tanpa ubah config karena verifier membaca ulang endpoint JWKS. Kalau provider-mu menerbitkan opaque token, bukan JWT, FastMCP punya introspection verifier yang memanggil endpoint introspection RFC 7662 milik provider, dengan konsekuensi satu network call per request.

Dua ketentuan spec layak kamu tegakkan saat review. Token cuma boleh lewat header Authorization: Bearer, di tiap request, dan tidak boleh muncul di query string. Token yang valid tapi tidak punya scope untuk sebuah operasi dapat 403, bukan 401.

Langkah 2: Publish Protected Resource Metadata

Bagian inilah yang bikin semuanya jalan tanpa konfigurasi di sisi client. MCP client yang cuma tahu URL-mu memulai dengan mengirim request tanpa token. Server membalas 401 dengan header WWW-Authenticate yang menunjuk ke dokumen Protected Resource Metadata (RFC 9728):

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"

Client mengambil URL itu dan menerima dokumen JSON kecil:

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["files:read", "files:write"],
  "bearer_methods_supported": ["header"]
}

resource adalah canonical URI server ini, dan authorization_servers menyebut IdP yang bisa menerbitkan token untuknya. Mempublikasikan dokumen ini adalah MUST untuk MCP server. Tanpa itu, 401 tanpa metadata cuma bilang client butuh token tanpa memberi tahu dari mana, dan itu memaksa konfigurasi per server ke tiap user.

RemoteAuthProvider milik FastMCP menyiapkan endpoint ini untukmu dengan IdP yang mendukung DCR. JWTVerifier biasa cuma memberi validasi token tanpa endpoint discovery OAuth, cukup untuk layanan internal yang client-nya sudah tahu cara ambil token, tapi tidak cukup untuk server publik.

Perhatikan aturan path-nya karena sering bikin salah. URI well-known dibentuk dengan menyisipkan /.well-known/oauth-protected-resource setelah scheme dan authority, dan untuk server berbasis path disisipkan sebelum path-nya. https://mcp.example.com/server/mcp disajikan di https://mcp.example.com/.well-known/oauth-protected-resource/server/mcp. Pakai helper dari framework-mu, jangan rakit string-nya sendiri.

Langkah 3: Pilih jalur registrasi client

Client butuh client ID sebelum bisa menjalankan flow authorization. Revisi spec 2026-07-28 mengubah urutan prioritasnya, dan di sinilah tutorial lama menyesatkan:

  1. Client ID Metadata Documents. Client menaruh dokumen JSON di URL HTTPS lalu memakai URL itu sebagai client_id-nya. Status SHOULD di spec saat ini.
  2. Pre-registration. Kamu memberi client credential tetap.
  3. Dynamic Client Registration (RFC 7591). Masih jalan, tapi sekarang ditandai deprecated dan dipertahankan untuk authorization server yang belum bisa CIMD.

Kalau IdP-mu mendukung DCR atau CIMD, RemoteAuthProvider FastMCP memberi alur otomatisnya. GitHub, Google, dan Azure tidak mendukung DCR dan butuh registrasi app manual. Untuk itu, OAuthProxy menyajikan permukaan yang kompatibel DCR ke MCP client sambil memakai credential tetapmu ke upstream. Itu bedanya antara "jalan dengan satu klik" dan "jalan".

Langkah 4: Jangan teruskan token ke upstream

Masalah confused deputy ada di sini. Kalau MCP server-mu memanggil API upstream, misalnya Git host di balik tool git, dia adalah OAuth client terpisah ke API itu dan butuh token sendiri dari authorization server upstream. Meneruskan token client ke upstream adalah MUST NOT di spec. Upstream akan mempercayai token yang diteruskan seolah berasal dari server-mu, dan satu client yang dibobol bisa bertindak dengan jangkauan server-mu sementara jejak audit menunjuk ke pihak yang salah.

Langkah 5: Tangani kekurangan scope dengan step-up

Beri scope minimum di awal lalu tambah belakangan. Saat token valid tapi tidak punya scope untuk sebuah operasi, balas 403 dengan scope yang kurang, bukan 401:

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
                         scope="files:write",
                         resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         error_description="File write permission required for this operation"

Client akan re-authorize dengan gabungan scope sebelumnya dan scope baru, lalu mengulang request. Sertakan juga scope yang dibutuhkan di challenge 401 pertama, supaya client meminta tepat sesuai kebutuhannya. Memisahkan files:read dari files:write inilah yang mencegah agent perangkum diam-diam menghapus data, dan client diharapkan membatasi serta melacak percobaan step-up supaya loop yang rusak tidak berputar selamanya.

Uji pakai Inspector

Jalankan server, lalu arahkan Inspector ke sana:

npx @modelcontextprotocol/inspector
# masukkan https://mcp.example.com/mcp di UI

Kamu harusnya melihat 401, lalu lompatan discovery ke IdP-mu, lalu layar consent. Kegagalan yang sering muncul adalah host yang cuma menerima URL dan headers map, yang berhenti di langkah browser. Itu keterbatasan client, bukan bug server, dan justru karena itu endpoint metadata penting: host yang patuh menjalankan flow hanya bermodal URL-mu.

Verifikasi dua kasus gagal secara manual:

# tanpa token -> 401 dengan WWW-Authenticate
curl -i -X POST https://mcp.example.com/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# dokumen protected resource metadata
curl -s https://mcp.example.com/.well-known/oauth-protected-resource | jq

Kapan pakai ini vs alternatifnya

Pendekatan Pakai saat Perhatikan
API key dari env var Server STDIO, internal antar service Satu key satu batas, tanpa audit per user
OAuth 2.1 dengan validasi JWT Server publik remote dengan IdP yang sudah ada Validasi audience tetap wajib, signature valid saja belum cukup
OAuth 2.1 dengan introspection Provider menerbitkan opaque token Satu network call per request, jadi pool HTTP client-mu
OAuth server penuh di dalam MCP server Kebutuhan air-gapped atau compliance khusus Kamu sekarang memiliki UI login, penyimpanan token, revocation, dan CVE-nya

Langkah lanjut

Pakai MultiAuth saat satu server harus menerima client interaktif sekaligus token mesin: dia menggabungkan OAuth proxy dengan JWT verifier dan mengambil sumber pertama yang valid. Tambahkan audit logging terstruktur atas keputusan validator supaya penolakan jadi sinyal, bukan stack trace yang tidak dibaca siapa pun. Baca halaman security best practices bersamaan dengan halaman authorization, karena token passthrough dan validasi audience adalah titik awal insiden nyata.

Referensi

Butuh Bantuan Implementasi?

Saya membantu tim mendesain dan membangun infrastruktur cloud scalable, pipeline DevOps, dan sistem production-grade.

Konsultasi Gratis