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 |
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);
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;
}
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
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.