Naar inhoud
lightbulb Welkom op de nieuwe kennisbank | We hebben de docs volledig vernieuwd met meer dan 160 features. Bekijk wat nieuw isarrow_forward

Authenticatie voor MCP-servers

Hoe je remote MCP-servers beveiligt met OAuth 2.1, token-validatie en Streamable HTTP volgens de MCP-autorisatiespec (versie 2025-11-25).

Authenticatie in MCP: het overzicht

Het MCP-protocol zelf regelt geen inlogmechanisme op berichtniveau. Autorisatie hoort bij de transport-laag. Voor lokale stdio-servers is dat impliciet: de gebruiker die de server start, heeft de rechten en de credentials komen uit de omgeving. Zodra je server via het netwerk bereikbaar is, moet je autorisatie zelf inrichten.

De MCP-autorisatiespec (versie 2025-11-25, de actuele versie in juni 2026) bouwt op OAuth 2.1. Het belangrijkste mentale model is dit: jouw MCP-server gedraagt zich als een OAuth 2.1 resource server. Hij geeft zelf geen tokens uit, maar valideert tokens die door een aparte authorization server zijn uitgegeven (bijvoorbeeld Google, Microsoft Entra, Auth0 of je eigen identity provider). De client haalt het token op bij die authorization server en stuurt het mee in elke aanroep.

Welke aanpak past bij jouw situatie:

Strategie Wanneer Kenmerk
Statische API-key Server-to-server, geen gebruikersidentiteit Simpel, maar geen delegatie en geen audience-binding
OAuth 2.1 (resource server) Servers die namens individuele gebruikers handelen Aanbevolen voor remote productie-servers
mTLS Interne netwerken, beide kanten presenteren certificaten Hoog vertrouwen, complexere setup
info

SSE is vervangen door Streamable HTTP

De oude HTTP plus SSE-transport (SSEServerTransport) is sinds spec-versie 2025-03-26 gedeprecateerd en vervangen door Streamable HTTP (StreamableHTTPServerTransport). Nieuwe servers gebruiken Streamable HTTP. Hou je nog oude clients in de lucht, dan kun je tijdelijk beide endpoints naast elkaar serveren, maar bouw nieuw werk niet meer op SSE.

Statische API-key (alleen voor server-to-server)

Voor interne koppelingen zonder gebruikersidentiteit volstaat een vaste sleutel in de Authorization-header. Gebruik dit niet voor servers die namens gebruikers handelen, want er is geen audience-binding en geen intrekbaarheid per gebruiker.

import express from "express";
import { randomUUID } from "node:crypto";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const app = express();
app.use(express.json());

const VALID_KEYS = new Set(process.env.API_KEYS?.split(",") ?? []);

function authMiddleware(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith("Bearer ")) {
    res.status(401).json({ error: "Geen Bearer-token opgegeven" });
    return;
  }
  const token = authHeader.slice(7);
  if (!VALID_KEYS.has(token)) {
    res.status(403).json({ error: "Ongeldig token" });
    return;
  }
  next();
}

const server = new McpServer({ name: "secure-server", version: "1.0.0" });
const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
});
await server.connect(transport);

app.post("/mcp", authMiddleware, async (req, res) => {
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000);
warning

Sleutels nooit in code of repo

Sla API-keys nooit op in source code, in een .env die je commit, of in een configuratiebestand in je repository. Gebruik omgevingsvariabelen die je buiten de codebase injecteert, of een secrets manager zoals Google Secret Manager of HashiCorp Vault. Gebruik vergelijk met constante looptijd waar mogelijk, zodat je geen timing-informatie lekt.

OAuth 2.1: jouw server als resource server

Dit is de aanbevolen aanpak voor remote servers die echte gebruikers bedienen. De server geeft zelf geen inlogscherm, maar valideert het binnenkomende token en wijst de client naar de juiste authorization server. De spec vraagt een paar concrete dingen.

1. Antwoord op een ontbrekend token met een 401 plus WWW-Authenticate

Wanneer er geen geldig token is, geef je een 401 terug met een WWW-Authenticate-header die naar je Protected Resource Metadata wijst (RFC 9728). Zo weet de client zelf bij welke authorization server hij een token moet halen.

function requireToken(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith("Bearer ")) {
    res
      .status(401)
      .set(
        "WWW-Authenticate",
        'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"'
      )
      .json({ error: "Authenticatie vereist" });
    return;
  }
  next();
}

2. Publiceer Protected Resource Metadata

Serveer een well-known document dat naar je authorization server(s) verwijst. De client gebruikt dit om het OAuth-verkeer te starten.

app.get("/.well-known/oauth-protected-resource", (req, res) => {
  res.json({
    resource: "https://mcp.example.com",
    authorization_servers: ["https://auth.example.com"],
    scopes_supported: ["files:read", "files:write"],
  });
});

3. Valideer het token op audience

De spec is hier streng: je server mag een token alleen accepteren als het specifiek voor jouw server is uitgegeven. Dit voorkomt dat een token dat voor een andere dienst was bedoeld, hier wordt hergebruikt. Controleer dus de handtekening, de vervaldatum en de aud-claim (RFC 8707).

import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(
  new URL("https://auth.example.com/.well-known/jwks.json")
);

async function validateToken(token: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: "https://auth.example.com",
    audience: "https://mcp.example.com",
  });
  return payload;
}
dangerous

Geef tokens nooit ongewijzigd door

Token-passthrough is in de spec expliciet verboden. Als je MCP-server zelf een upstream-API aanroept (bijvoorbeeld Google Drive), dan handelt hij dáár als een aparte OAuth-client met een eigen, apart uitgegeven token. Stuur het token dat je van de MCP-client kreeg nooit door naar een upstream-dienst. Dat opent het confused deputy-lek, waarbij de upstream-API ten onrechte aanneemt dat het verzoek namens de juiste partij komt.

Token gebruiken in een tool-handler

Zodra het token gevalideerd is, kun je in je tool de bijbehorende gebruiker en scopes gebruiken. Hieronder vraagt de tool bestanden op namens de ingelogde gebruiker. Het upstream-token voor Google staat los van het MCP-token en haal je uit een veilige store per gebruiker.

server.registerTool(
  "list_drive_files",
  {
    title: "Drive-bestanden",
    description: "Lijst bestanden in Google Drive van de ingelogde gebruiker",
    inputSchema: { folder_id: z.string().optional() },
  },
  async ({ folder_id }, extra) => {
    const userId = extra?.authInfo?.subject;
    if (!userId) {
      throw new McpError(ErrorCode.InvalidRequest, "Niet geautoriseerd.");
    }
    const upstream = await tokenStore.get(userId);
    if (!upstream) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        "Geen Google-koppeling. Verbind eerst je account."
      );
    }
    oauth2Client.setCredentials(upstream);
    const drive = google.drive({ version: "v3", auth: oauth2Client });
    const response = await drive.files.list({
      q: folder_id ? `'${folder_id}' in parents` : undefined,
    });
    return {
      content: [{ type: "text", text: JSON.stringify(response.data.files) }],
    };
  }
);

Rate limiting toevoegen

Beperk het aantal verzoeken om misbruik en kostenexplosies te voorkomen. Sleutel bij voorkeur op de gevalideerde gebruiker, niet op het rauwe token, zodat je geen tokens in je sleutels logt.

import rateLimit from "express-rate-limit";

const limiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  message: { error: "Te veel verzoeken, wacht een minuut" },
  keyGenerator: (req) => req.headers.authorization ?? req.ip,
});

app.use("/mcp", limiter);

Audit logging

Log elke tool-aanroep voor compliance en incident-onderzoek. Log nooit het rauwe token of gevoelige argumenten; bewaar wel wie wat wanneer aanriep.

server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
  await auditLog.write({
    timestamp: new Date().toISOString(),
    tool: request.params.name,
    user: extra?.authInfo?.subject,
  });
});

Checklist voor productie

lightbulb

Voor je live gaat

Loop deze punten af voor elke remote MCP-server:

  • Server draait uitsluitend over HTTPS (Bearer-tokens over HTTP zijn triviaal te onderscheppen).
  • Tokens worden gevalideerd op handtekening, vervaltijd en audience (alleen tokens voor jouw server).
  • Een ontbrekend of ongeldig token geeft een 401 met WWW-Authenticate, een te beperkt scope geeft een 403 met insufficient_scope.
  • De client wordt verplicht PKCE (S256) te gebruiken; korte access tokens plus roterende refresh tokens.
  • Geen token-passthrough naar upstream-API's; upstream-tokens staan los en per gebruiker in een veilige store.
  • Tokens staan in een externe store (Redis, Firestore), niet in geheugen van een stateless instance.
Moet elke MCP-server authenticatie hebben?

Een lokale stdio-server die alleen op je eigen machine draait, heeft geen extra autorisatielaag nodig: de credentials komen uit de omgeving. Elke server die via het netwerk bereikbaar is, moet wel autorisatie afdwingen.

Wat betekent het dat mijn server een resource server is?

Je server geeft zelf geen tokens uit. Hij ontvangt een Bearer-token, controleert of het geldig en specifiek voor hem bedoeld is, en bedient daarna het verzoek. De tokens komen van een aparte authorization server, die ook met de gebruiker mag praten.

Waarom moet ik de audience van een token controleren?

Zonder die controle zou je server een token accepteren dat eigenlijk voor een andere dienst was uitgegeven. Een aanvaller kan dan een legitiem token van elders hergebruiken bij jouw server. De spec verplicht daarom dat je alleen tokens accepteert waarin jouw server als doelgroep staat.

Hoe beveilig ik tokens bij een stateless deployment?

Bij serverless of containers zonder vaste staat sla je tokens op in een externe store zoals Redis of Firestore, nooit in het geheugen van een losse instance. Versleutel ze in rust en geef ze een korte levensduur.

Wat als een access token verloopt tijdens een sessie?

Vang de 401 op, gebruik de refresh token om een nieuw access token te halen en herhaal de aanroep. Roteer refresh tokens voor publieke clients, zoals de spec vraagt, zodat een gelekt token beperkt bruikbaar is.

Mag ik het MCP-token doorsturen naar Google of een andere upstream-API?

Nee. Token-passthrough is verboden. Als je server upstream-diensten aanroept, doet hij dat als een aparte OAuth-client met een eigen token dat door die upstream-authorization server is uitgegeven. Het token van de MCP-client blijft bij jouw server.