Hoe een MCP-server jouw API aanroept namens gebruikers: een productie-tokenstrategie
Verklaart de uitgaande tokenstrategie voor MCP-servers: waarom token-doorgeven en M2M niet werken, en hoe tokenuitwisseling de rechten in lijn houdt met de gebruiker.
YijunDeveloper
Stop met weken verspillen aan gebruikersauthenticatie
Lanceer veilige apps sneller met Logto. Integreer gebruikersauthenticatie in minuten en focus op je kernproduct.
Hoe een MCP-server jouw API aanroept namens gebruikers: een productie-tokenstrategie#
In ons vorige artikel deelden we onze algemene ervaring met het bouwen van de Logto remote MCP-server. Dit artikel behandelt het architectuurontwerp en de OAuth-flow in detail.
Met de MCP-server als grens heeft authenticatie voor een remote MCP-server twee benen: inbound en outbound.
Inbound: de MCP-client (VS Code, Cursor, enz.) logt in via OAuth, ontvangt een access token, en gebruikt deze om toegang te krijgen tot jouw MCP-server
Outbound: bij het verwerken van tool-aanroepen vraagt de MCP-server namens de gebruiker jouw eigen business API aan
Het inbound-traject is duidelijk gedefinieerd in de MCP-specificatie, met veel discussies en implementaties in de community. We schreven eerder ook een implementatiegids. Het outbound-traject krijgt echter veel minder aandacht: de MCP-server beschikt alleen over een token om de MCP-server zelf te benaderen. Hoe kan deze namens de gebruiker jouw business API aanroepen?
Dit was precies de vraag waar wij steeds tegenaan liepen tijdens het bouwen van de Logto MCP-server. Logto Cloud is een typisch B2B multi-tenant product: een gebruiker kan lid zijn van meerdere tenants, en rechten zijn gekoppeld aan de rol van de gebruiker binnen elke tenant. De AI moet niet alleen als de gebruiker handelen, maar ook in de juiste tenant landen.
Dit artikel volgt ons daadwerkelijke beslissingsproces:
Waarom de MCP-server onafhankelijk gedeployed moet worden, als een aparte beveiligde resource
Twee uitgaande benaderingen die we hebben verworpen (token-doorgeven en M2M-token) en hun problemen
Het uiteindelijke ontwerp: tokenuitwisseling plus subject token, inclusief hoe organisatietokens multi-tenancy afhandelen
Een paar principes om in acht te nemen als jij iets vergelijkbaars bouwt
Het kernprobleem: de dubbele rol van een MCP-server#
Vanuit het OAuth-perspectief speelt een remote MCP-server tegelijkertijd twee rollen:
Voor de MCP-client is het een resource server: de client moet een token aanleveren om toegang te krijgen
Voor de business API is het een client: de server levert een token aan om iemand anders te benaderen
Met andere woorden: de MCP-server speelt aan beide zijden een andere rol, en elke kant gebruikt een ander token. Het token dat de MCP-client krijgt via OAuth heeft de MCP-server als audience, waardoor de business API het zal weigeren vanwege de audience check.
Dus het outbound-traject is wezenlijk een delegatieprobleem: hoe roept de MCP-server downstream API's aan als de gebruiker, met diens rechten, zonder de credentials van de gebruiker te hebben?
Architectuurkeuze: de MCP-server als zelfstandige service#
De MCP-endpoint integreren in de business API-service, met gedeeld proces en auth-stack, lijkt het meest direct. Toch kozen wij voor een zelfstandige deployment:
Risico-isolatie: toen we deze beslissing namen, was de officiële MCP SDK niet klaar voor productie, en het protocol evolueerde snel (zoals de overstap van SSE naar Streamable HTTP). Logto is een IAM-service en de beschikbaarheid van het inlogpad is cruciaal. Met een zelfstandige deployment, als de MCP-server down gaat, gaat alleen het AI-ingangspunt offline. De hoofdservice blijft onaangetast.
Onafhankelijke iteratie: de MCP-ecosysteem verandert wekelijks, en client-compatibiliteitsproblemen kunnen elk moment hotfixes vereisen, terwijl de core-service een strikt releaseproces met regressietests heeft. Scheiden houdt de vaart in beide teams.
Vrijheid qua runtime: een standalone service kan de runtime kiezen die het beste past. De Logto MCP-server draait op Cloudflare Workers: stateless, schaalt per request, en quasi geen beheer. De embedded-optie biedt niet deze flexibiliteit.
De MCP-server is uitgezet op een eigen domein, mcp.logto.io, zonder privé koppeling met de hoofdservice. Mochten we die ooit willen open sourcen, dan is dat eenvoudig.
De embedded-optie vermijdt het tokenprobleem overigens niet: de MCP-endpoint en de business API zouden dezelfde resource identifier delen, dus het token dat de MCP-client krijgt brengt volledige API-rechten met zich mee. De vraag "met wiens rechten wordt deze call gedaan" verschuift alleen van cross-service tokenuitwisseling naar permission passing binnen het proces, maar het probleem blijft bestaan. Zelfstandig uitrollen dwingt je om de rechtenafbakening expliciet te ontwerpen, wat het onderwerp van de rest van dit artikel is.
De MCP-server moet een eigen resource identifier hebben#
De outbound-discussie begint bij een fundamentelere vraag: wat moet de audience zijn van het token dat de MCP-client krijgt?
De resource identifier van de business API hergebruiken is het meest direct: Logto's Management API is al een standaard OAuth-beschermde bron, dus de MCP-client kan direct zijn token aanvragen en de MCP-server kan het doorspelen. Vele vroege MCP-serverimplementaties deden dit.
Het nadeel: als de MCP-token's audience de business API is, bestaat de rechtenafbakening niet meer.
Je zorgvuldig ontworpen MCP-tools worden decoratie: het token kan zelf de volledige API aanroepen en omzeilt je tools
Het risico bij token-lek groeit van "enkele gecontroleerde bewerkingen via de MCP-server" naar "de volledige Management API"
Onafhankelijke auditing, rate limiting en permission scoping voor MCP-scenario's zijn onmogelijk
Dus onze eerste beslissing: de MCP-server is een aparte beveiligde resource, met een eigen resource identifier (https://mcp.logto.io) en eigen scope.
Dit houdt de rechtenafbakening strak en maakt de outbound-vraag concreet: het token kan alleen toegang tot de MCP-server, dus wat gebruikt de MCP-server om de API aan te roepen?
Het eerste idee ontstond uit een natuurlijke analogie.
Logto Console is een SPA. De manier waarop deze de Management API aanroept is simpel: de gebruiker logt via OAuth in de browser in, krijgt een token met de Management API als audience, en de frontend roept de API ermee aan.
Kan de MCP-server werken als een andere soort Console? Door de MCP-client direct het business API-token te laten aanvragen bij het inloggen, en de MCP-server laat het ongewijzigd door:
Het aantrekkelijke aan deze aanpak is de extreme eenvoud: de MCP-server hoeft alleen tokens door te geven. Maar er kleven duidelijke problemen aan:
Ten eerste: het ondermijnt direct de eerder genoemde rechtenafbakening. Token-doorgeven vereist dat de MCP-client het business API-token beheert, wat we net afgewezen hebben.
Ten tweede: de MCP-server is geen echte beveiligde resource meer. De audience van de tokens is niet zichzelf, waardoor audience-validatie betekenisloos wordt en degradeert tot "verifieer handtekening en issuer". Dit past niet bij de MCP-standaard (de server hoort als resource server te werken en zich kenbaar te maken via RFC 9728 Protected Resource Metadata). Deze aanpak doet alsof een backend-service een SPA in een browser is.
Ten derde: MCP-clients werken niet mee. Een standaard MCP-client vraagt tokens aan volgens de Protected Resource Metadata, en de audience zal de MCP-server zijn. Er is geen gangbare manier om het business API-token op te vragen, dus deze route loopt spaak.
Als het user-token niet voldoet, wat dan de identiteit van de MCP-server zelf?
Geef de MCP-server een M2M (machine-to-machine) applicatie, verkrijg een token via client credentials, en bel hiermee de business API aan. Dit is ook de standaardpraktijk tussen interne services.
Inbound authenticatie werkt dan prima: het MCP-client-token heeft de MCP-server als audience, de server valideert het, doet zijn werk met het eigen M2M-token.
Het fatale probleem is dat de M2M-tokenrechten niets te maken hebben met de gebruiker:
Te veel rechten: de rechten van het M2M-token zijn "wat de MCP-server mag doen", niet "wat de gebruiker mag". Een gebruiker met alleen-lezenrechten kan via MCP toch applicaties verwijderen omdat het M2M-token het kan
Identiteitsverlies: de downstream API ziet altijd de M2M-applicatie als afzender, waardoor geen audit trail naar individuele gebruikers mogelijk is
Confused deputy: de MCP-server wordt een doorgeefluik met hoge rechten; wie alleen maar een call weet uit te lokken, krijgt daarmee die rechten
M2M past bij scenario's zonder gebruikerscontext, zoals geplande taken of systeemkoppelingen. Een MCP-server is anders: elke call wordt door een specifieke gebruiker geïnitieerd, dus moet ook met hun identiteit en rechten draaien.
Het uiteindelijke ontwerp: token-uitwisseling + subject token#
Tel de lessen op van de vorige twee aanpakken, en je krijgt de eisen voor het juiste ontwerp:
Het token in handen van de MCP-client mag alleen toegang geven tot de MCP-server (les van aanpak 1)
Voor downstream API-calls moet de MCP-server als de gebruiker werken, met diens rechten (les van aanpak 2)
Dit wijst op een standaard mechanisme: tokenuitwisseling (RFC 8693). De MCP-server pakt een credential die staat voor de gebruiker, en wisselt die bij de auth-server om voor een downstream API-token. De identiteit en rechten van de gebruiker blijven behouden.
In Logto komt deze "credential die de gebruiker vertegenwoordigt" uit de user impersonation functie: het subject token. Dit is een kortlevende credential die de server aanvraagt voor een specifieke gebruiker, hetgeen betekent: "de volgende tokenuitwisseling draait als deze gebruiker". De gebruiker hoeft zelf niets aan te maken of in te stellen; de hele flow is geautomatiseerd. Het subject token is kortlevend, eenmalig, en verloopt zodra het gebruikt wordt.
Er zijn vier rollen in de architectuur. De MCP-server draait onafhankelijk op mcp.logto.io:
De volledige token-flow achter één tool-aanroep:
Stap voor stap:
① Inbound validatie. De MCP-client roept een tool aan met het user token. De audience van het token is de resource identifier van de MCP-server en de scope is mcp:all. De MCP-server verifieert handtekening, issuer, audience, scope, en verkrijgt de user identity. Hier eindigt inbound, dit token gaat nooit downstream.
② Service-identiteit. De MCP-server gebruikt zijn eigen M2M-credentials om een acceso-token met een specifieke scope, access:mcp:api, te verkrijgen. Deze scope dient slechts één doel: toegang tot de endpoint in de volgende stap.
③ Aanvragen van het subject token, de crux van de hele keten. De MCP-server roept POST /api/mcp/subject-tokens aan, een endpoint dat Cloud specifiek voor MCP opent, en presenteert twee credentials tegelijk:
Authorization-header: het M2M-token, bewijs "ik ben de officiële MCP-server"
x-mcp-user-token-header: het user token, bewijs "deze gebruiker heeft mij gemachtigd, en de autorisatie is nog geldig"
Cloud verifieert het user token volledig: handtekening, issuer, vervaldatum, audience moet gelijk zijn aan de resource identifier van de MCP-server, en scope bevat mcp:all. Na verificatie: userId komt direct uit de sub-claim van het token. De endpoint heeft geen parameter om zelf een userId aan te geven.
Dit voorkomt misbruik van de M2M-credential. Zou de endpoint een willekeurige userId accepteren, dan kan iedereen met de M2M-credential als elke gebruiker optreden. Hier kan de MCP-server alleen voor een gebruiker credentials uitwisselen als diens geldige authorisatie aanwezig is.
Het subject token is kortlevend en eenmalig. Er wordt nooit gecachet; altijd een nieuwe bij elk gebruik.
④ Uitwisseling naar werk-tokens. Met het subject token vindt een standaard tokenuitwisseling plaats voor twee soorten tokens:
Cloud API-token: voor operaties op gebruikersniveau als de gebruiker, zoals tenants opvragen of aanmaken
Org-token: gespecificeerd op een doeltenant (in Logto Cloud staat elke tenant voor een organisatie). Het token wordt als de gebruiker uitgegeven, met exact de rechten van de gebruiker binnen die tenant
⑤ Uitgaande call. Roep de business API aan met het uitgewisselde token en return het resultaat naar de MCP-client.
De multi-tenantcontext wordt opgelost bij de uitwisseling: de tenant leeft in de uitwisselingslaag, niet de connectielaag. list_tenants geeft keuzemogelijkheden met het Cloud API-token, de gebruiker kiest een, en de MCP-server wisselt een org-token voor die tenant uit. Eén endpoint bedient alle tenants, geen deployment per tenant nodig, tenants die midden in het gesprek ontstaan, zijn direct beschikbaar.
We leggen het naast de gebreken van de eerdere benaderingen, en elk probleem is ondervangen:
De schade bij een user-token-lek is beperkt (les uit aanpak 1): de audience is alleen de MCP-server. Zelfs als het token uitlekt, kunnen alleen die gecontroleerde tools worden aangeroepen
M2M-token kan niet worden misbruikt (les uit aanpak 2): wordt niet meer direct voor de business API gebruikt, alleen voor service-identiteit. Het subject token kan alleen verkregen worden als een geldig user-token tegelijkertijd wordt aangeleverd. Het confused deputy-probleem is verholpen
Geen user credentials opslaan: subject tokens worden direct op aanvraag aangevraagd en weggegooid. De MCP-server bewaart alleen zijn eigen M2M-credentials
Revocatie is direct verbonden: als de gebruiker de MCP-autorisatie intrekt, is het user-token ongeldig, de x-mcp-user-token-check faalt, en de outbound-keten stopt direct
Rechten kloppen met de gebruiker: uitgewisselde tokens worden uitgegeven als de gebruiker. Een alleen-lezen gebruiker blijft alleen-lezen via MCP, privilege-escalatie faalt bij de auth-server. Audit logs laten ook de echte gebruiker zien
Terugkijkend: het M2M-token heeft in het uiteindelijke ontwerp de juiste functie: het bewijst "wie ik ben", terwijl het "handelen als de gebruiker" altijd ter plekke met diens geldige autorisatie moet worden uitgewisseld.
Samenvattend komt de tokenstrategie voor een remote MCP-server neer op een paar punten:
Inbound en outbound zijn twee volledig gescheiden authenticatiefases, waarbij de audience van het MCP-token altijd de MCP-server zelf moet zijn
Outbound wordt opgelost door tokenuitwisseling: als jouw auth-server toelaat het inbound token direct te ruilen, is standaard tokenuitwisseling voldoende; als dat niet zo is (zoals bij Logto, waar de tokenuitwisseling een subject token vereist), gebruik dan een impersonation-feature om van serverkant te starten
Multi-tenancy verandert het mechanisme niet. De organisatiecontext is gewoon een parameter tijdens de uitwisseling; voor single-tenantproducten kan die stap gewoon worden overgeslagen
De AI handelt namens de gebruiker, dus permissies van downstream tokens moeten vanzelfsprekend terugvallen op de gebruiker
Een simpele test: stel het token van de MCP-client lekt. Het enige wat een aanvaller kan, is die gecontroleerde tools op de MCP-server gebruiken, altijd nog binnen de rechten van de gebruiker. Als directe toegang tot de volledige API mogelijk is, is de rechtenafbakening stuk.
Het MCP-ecosysteem ontwikkelt zich snel. Inbound authenticatie is netjes gespecificeerd, maar "hoe de MCP-server downstream aanroept" moeten teams nu nog zelf uitwerken. Hopelijk biedt onze aanpak een waardevol referentiepunt.