How remote MCP servers do delegated authorisation; what the Docker MCP Gateway adds; and how to pick a provider — commercial or open-source — that won't paint you into a corner.
Discover · Register · Authorise · Bind · Audit
01
Topics
MCP foundations
The Model Context Protocol — one-slide refresher
Local (stdio) vs Remote (HTTP / SSE / Streamable) servers
Why remote MCP has to use OAuth
The MCP authorisation profile (2025-06 spec)
The MCP OAuth flow
Discovery — RFC 9728 + RFC 8414
Dynamic Client Registration (RFC 7591)
End-to-end sequence with Auth Code + PKCE + RIs
Confused-deputy & token-passthrough — what NOT to do
MCP is an open protocol from Anthropic (Nov 2024) that standardises how LLM applications talk to tools, resources and prompts. Think of it as "USB-C for AI integrations" — one protocol between any host and any data source or tool.
Tools
Functions the LLM can call — write a file, send a Slack message, run a SQL query.
Resources
Data the LLM can read — files, DB rows, doc fragments. Identified by URI.
Prompts
Reusable templates the host can offer to the user — slash commands, "code review", etc.
04
Local vs Remote MCP — Why Auth Matters
Local server (stdio)
Spawned as a child process by the host.
Trust boundary = the OS user. Anything that user can do, the server can do.
No network — no OAuth, no tokens, no network attacker.
Reached over the public internet (or VPC). Multi-tenant. Survives between hosts.
Trust boundary = whoever holds a valid token.
Needs to know which user is calling and what they're allowed to do.
Good for: SaaS connectors, enterprise data, anything where the API isn't on this laptop.
Without OAuth, the only options are bearer-API-keys-in-config-files, which puts you back in 2006.
Why a global API key is wrong for remote MCP
No user identity — the MCP server can't enforce per-user permissions.
No revocation — leaked once, replayable forever.
No audit — you can't tell which human triggered which tool.
No consent — users have no UI to approve or revoke specific scopes.
No MFA — credential strength is whatever the operator chose, not the org's IdP policy.
Every one of these is a thing OAuth gives you for free. That is why the MCP spec mandates it for HTTP transports.
05
The MCP Authorization Profile (2025-06)
The MCP spec includes a normative Authorization section for HTTP-based servers. It's a profile of OAuth 2.1 — pinning the choices a generic spec leaves open.
What the MCP profile says — MUST
The MCP server is an OAuth 2.1 Resource Server.
The MCP client uses Authorization Code + PKCE (S256).
The client discovers the AS via the server's RFC 9728 Protected Resource Metadata document.
The client uses RFC 8707 Resource Indicators to bind the access token's audience to the MCP server URL.
The MCP server validates aud on every token and rejects any token not minted for it.
The server returns 401 + WWW-Authenticate with a resource_metadata URL when no/expired token is presented.
What it says — SHOULD / MAY
Dynamic Client Registration (RFC 7591) so users don't have to pre-register every host.
DPoP (RFC 9449) for sender-constrained tokens — strongly recommended over plain Bearer.
Refresh tokens with rotation.
Scopes named after MCP capabilities (e.g. tools:write, resources:read).
OIDC if the server needs to know who the user is, not just that they're authorised.
Anti-patterns the spec calls out by name
Token passthrough — the MCP server forwarding the user's token to a downstream API the client never consented to.
Confused-deputy tokens — accepting any valid token from "your" AS regardless of audience.
Static API keys in HTTP headers in lieu of OAuth.
06
Discovery — How the Client Finds the AS
The MCP client doesn't get told upfront which auth server to use. It asks the MCP server.
Pre-MCP, every OAuth client was registered by hand in the AS UI. That doesn't scale to "any user, any host". RFC 7591 Dynamic Client Registration lets the host register itself the first time it sees a server.
Hosts (Claude Desktop, Cursor, claude.ai, Goose, custom agents) cannot pre-arrange with every MCP server's AS.
DCR turns "first connection" into a one-RPC handshake.
Registration produces a per-host, per-AS client_id the host stores locally.
The client uses that client_id in every subsequent Authorization Code + PKCE flow.
DCR with Initial Access Tokens
For tighter control, an AS can require an Initial Access Token on /register — issued out-of-band by an admin. Stops random clients self-registering against an enterprise AS.
Watch out
Open DCR is a free abuse vector. Enterprise deployments usually disable it and use software statements (RFC 7591 §2.3) or pre-issued client IDs instead.
08
End-to-End Sequence — Host → MCP → AS → Resource
Step 14 is the critical separation: the MCP server uses its own credentials upstream — it doesn't replay the user's token. That's how the spec avoids the confused-deputy class of bugs.
09
Resource Indicators & Audience Binding in MCP
Why RIs are non-negotiable for MCP
One AS often serves many MCP servers — and other unrelated APIs.
Without resource=, an access token issued for an innocent MCP could be replayed against any other RS that trusts the same AS.
RFC 8707 lets the client name the intended RS so the AS can pin aud.
The MCP server's job: validate aud = its own URL.
If you skip RIs
You've built a ready-made token launderer. A compromised MCP server gets bearer tokens it can spend at every RS in the org.
MCP tokens travel from a host → over the public internet → to an MCP server.
Hosts often run on dev laptops with browser extensions, proxies, MITM dev tools — token leak surface is large.
A leaked plain Bearer = the attacker can act as the user against the MCP server until expiry.
DPoP binds the token to a private key the host generated locally and never sends.
The exchange
# 1. host generates a key pair, includes JWK on /token
POST /token ...
DPoP: eyJ... # proof for the token endpoint
# 2. AS issues an access token bound to that key
{ "access_token": "eyJ...",
"token_type": "DPoP",
"expires_in": 600 }
# AT payload includes
"cnf": { "jkt": "sha256(jwk)" }
# 3. host calls MCP server
POST /mcp/tools/call
Authorization: DPoP eyJ...
DPoP: eyJ... # fresh proof per request, signs htm+htu+iat+jti
What the MCP server must check
Validate the access token (signature, iss, aud, exp, scope).
Parse the DPoP header JWS.
Verify the proof is signed by the JWK whose hash matches cnf.jkt.
Check htm = request method, htu = request URL.
Check iat within a small window (default 60 s).
Check jti hasn't been seen before (replay cache).
In code (tinypseudo)
def authorize(req):
at = parse_bearer_or_dpop(req)
claims = jwt.verify(at, jwks)
require(claims.aud == MY_RS_URL)
require("tools:write" in claims.scope)
if claims.token_type == "DPoP":
proof = req.headers["DPoP"]
verify_dpop(proof, claims["cnf"]["jkt"], req)
return claims["sub"]
11
Anti-Patterns & Confused-Deputy Pitfalls
1. Token passthrough
The MCP server takes the user's access token and calls a downstream API with it.
The downstream API was never named in the user's consent.
The token's aud doesn't match — but downstream APIs that don't validate aud won't notice.
This is the OAuth "confused deputy" attack with a thin wrapper.
Fix: the MCP server obtains its own credentials for the downstream API (Client Credentials, or Token Exchange RFC 8693 if user identity must be preserved). Never re-send the user's bearer.
2. Skipping aud checks
"Any token from our AS that signs is fine." → token launderer. Always pin aud == my_rs_url.
3. Accepting tokens from any issuer
If the server lists multiple AS in its protected-resource metadata, every one is implicitly trusted. Pin to a specific issuer or maintain an explicit allow-list.
4. Per-server static API keys ("OAuth on the wire only")
Some early MCP servers fronted a static upstream API key with OAuth login. The AS issues real access tokens, but the server doesn't actually use the user identity downstream — every user shares the same upstream credentials.
Result: zero per-user audit, no scope enforcement, single-secret breach radius. Fix: per-user upstream creds via Token Exchange or per-user OAuth-on-behalf-of.
5. Running without DPoP/mTLS over the public internet
Bearer tokens through dev-laptop network paths get leaked. Ship DPoP from day one for any non-trivial scope.
6. Open DCR with no rate-limiting
Anyone on the internet can spin up infinite client_ids on your AS, blowing through quotas and audit storage. Rate-limit, require Initial Access Tokens, or use software statements.
7. Logging the Authorization header
Tokens end up in CloudWatch / Loki / Datadog. Scrub aggressively.
12
The Docker MCP Gateway — Intro
The Docker MCP Gateway (Docker Inc., 2025) is an open-source binary that fronts many MCP servers (each running in its own container) behind a single endpoint. It pairs with the Docker MCP Catalog and the Docker MCP Toolkit in Docker Desktop.
What problem it solves
Each MCP server has a different install story (npm? Python? Docker?). The Gateway runs them all uniformly as containers from a curated catalog.
Each server has its own auth model (env-var API key here, OAuth there). The Gateway centralises secret handling and OAuth.
Hosts (Claude Desktop, Cursor, claude.ai, VS Code) want one HTTP endpoint, not n.
Operators want one place to log, audit, sign and policy-gate every tool call.
Components
Gateway — the binary. Speaks MCP to hosts upstream and MCP to servers downstream.
Catalog — JSON/YAML index of vetted servers, container images, default scopes, OAuth metadata.
Secrets — local secret store; never written into container env vars.
The Gateway's OAuth interceptor moves the entire OAuth dance out of every host and into one trusted process. Hosts only see "tools that work"; the Gateway holds the tokens.
What it does
For each enabled server, reads OAuth metadata from the catalog (issuer, scopes, audience, DCR endpoint).
On first use, opens the user's system browser to complete Auth Code + PKCE.
Stores access + refresh tokens in the local secrets store, scoped per-server, per-user.
Refreshes silently when an access token expires; rotates refresh tokens.
Injects the right token on each downstream call to the right MCP server.
Logs every tool invocation with user, scope, server, and request hash for audit.
Why this is better than each host doing its own OAuth
One consent, all hosts on the machine benefit.
Tokens never leave a single trust boundary — Cursor doesn't see your Slack token, claude.ai doesn't see your GitHub one unless explicitly allowed.
Centralised revocation — kill the Gateway secret, every server is logged out.
Per-server scope enforcement at one chokepoint.
Audit — one log file vs n hosts × n servers.
CLI flow
# list catalog
docker mcp catalog ls
# enable a server
docker mcp server enable github
# → Gateway opens browser for OAuth
# → tokens stored in local secret store
# → 'github' now appears as a tool source to every host
# revoke
docker mcp server logout github
Trust note
You are now trusting the Gateway with every connected service's tokens. Run it locally; pin its image; restrict who can talk to its socket. For team deployments, treat it like a privileged credential broker.
15
Provider Landscape — At a Glance
Provider
Model
OAuth 2.1 ready
DCR
DPoP
Free tier
Sweet spot
Auth0 (Okta)
SaaS
yes
yes
preview
7,500 MAU
Fast time-to-first-token; deep MCP samples.
Okta Workforce / Customer
SaaS
yes
limited
n/a
—
Enterprise IdP with SCIM, governance, advanced policies.
Microsoft Entra ID
SaaS
yes
limited
via FAPI profile
50k MAU (External ID)
M365 / Azure shops; CIAM via External ID.
Google Identity Platform
SaaS
yes
via Firebase
n/a
generous
Consumer apps already on GCP / Firebase.
AWS Cognito
SaaS
2.0 + most BCP 240
limited
n/a
50k MAU
AWS-native CIAM; cheap at scale.
Stytch
SaaS
yes
yes
yes
10k MAU
Modern API-first; strong MCP & agent story.
WorkOS
SaaS
yes
yes
via AuthKit
1M users (AuthKit)
"SSO / SCIM / MCP for B2B SaaS" turn-key.
Clerk
SaaS
yes
yes
preview
10k MAU
React/Next.js DX-first; simple MCP integration.
Descope
SaaS
yes
yes
preview
7k MAU
Drag-and-drop flows; agentic identity features.
FusionAuth
SaaS or self-host
yes
partial
n/a
Community edition free
Same product on-prem & in cloud; flexible licensing.
Keycloak
OSS, self-host
yes
yes
yes
free
Most flexible OSS IdP; FAPI 2.0 conformant.
ZITADEL
OSS or SaaS
yes
yes
yes
25k MAU (cloud)
Modern multi-tenant CIAM; first-class MCP guides.
Authentik
OSS, self-host
yes
partial
n/a
free
Easy OIDC + SAML for homelabs & small teams.
Ory Hydra (+ Kratos)
OSS or SaaS (Ory Network)
yes
yes
yes
free
OAuth/OIDC server you compose with your own login UI.
Authelia
OSS, self-host
OIDC 1.0
limited
n/a
free
Forward-auth for reverse-proxies; great for homelabs.
Logto
OSS or SaaS
yes
partial
n/a
5k MAU (cloud)
Lightweight CIAM for small SaaS / indie hackers.
SuperTokens
OSS or SaaS
core OAuth
partial
n/a
5k MAU
Self-host with managed dashboard option.
16
Commercial Providers — Detail
Auth0 (Okta)
Most-cited SaaS IdP; published official "OAuth for MCP" guides and samples in 2025.
Full OIDC, OAuth 2.1, DCR, fine-grained scopes, Actions for custom logic.
Free dev tenant: 7,500 MAU (essentials), social connections, basic MFA. Paid plans add SAML, attack protection, large MAU.
Microsoft Entra ID (formerly Azure AD)
Default for any org on Microsoft 365.
Entra External ID = the CIAM brand (replaces Azure AD B2C). Free tier ~50k MAU.
Apache 2.0 self-host; Ory Network is the SaaS form.
Best for teams that want OAuth without dragging in a full IdP UI.
Authelia
Forward-auth companion for nginx / Traefik / Caddy.
OIDC 1.0 server, MFA, single sign-on for self-hosted services.
Light footprint; ideal for homelab stacks pairing with Caddy.
Logto · SuperTokens · Janssen
Logto — modern OSS CIAM with managed cloud option (free 5k MAU).
SuperTokens — OSS auth with optional managed dashboard.
Janssen Project — Linux Foundation OAuth/OIDC stack (Gluu lineage); enterprise-grade and FIDO2.
18
SaaS vs Self-Hosted — Trade-offs
Concern
SaaS (Auth0, Stytch, Entra...)
Self-host (Keycloak, ZITADEL, Hydra...)
Time to first OAuth flow
minutes
hours – a day
Operational burden
vendor's problem
your problem (Postgres, JVM, TLS, certs)
Compliance evidence
SOC2 / ISO reports come from vendor
you produce them
Data residency / sovereignty
vendor regions; sometimes a constraint
full control
Vendor lock-in
moderate to high; export tooling varies
low
Cost at 100 users
often free
~ small VPS
Cost at 1M users
£tens of thousands / month
~ a few VMs + ops time
Custom flows / branding
limited to vendor primitives
anything (you wrote it)
FedRAMP / sovereign cloud
special SKUs; not all features
you choose the cloud
"Auth team" required
no
yes, eventually
A reasonable rule
SaaS until either (a) you're paying more than the cost of one auth engineer, or (b) you have a data-residency / sovereignty / compliance constraint that the SaaS doesn't meet. At that point, Keycloak or ZITADEL self-host.
19
Choosing a Provider for an MCP Server
If you...
Strongest pick
Why
Want to ship a public remote MCP today, minimal ops
Auth0 or Stytch
Both publish current MCP samples, full OAuth 2.1, DCR, DPoP-ready, generous free tier.
Build a B2B SaaS that resells MCP to enterprise customers
User pastes https://mcp.acme.com into "Add MCP server".
claude.ai gets 401 + WWW-Authenticate with resource_metadata.
Fetches the protected-resource doc; fetches Auth0's /.well-known/oauth-authorization-server.
Calls POST /oidc/register on Auth0 (DCR) to mint a per-claude.ai client_id.
Pops a system browser to /authorize with response_type=code, PKCE S256, resource=https://mcp.acme.com/, scope=tools:read tools:write.
User logs into Auth0, sees consent for the listed scopes.
Auth0 redirects back to claude.ai's loopback / https://claude.ai/oauth/callback.
claude.ai exchanges the code at /oauth/token with PKCE verifier and resource=.
Stores access + refresh tokens in claude.ai's per-server token cache.
Calls tools/list on the MCP server; off you go.
21
Worked Example 2 — Claude Desktop → Docker Gateway → Keycloak
You're an enterprise. You self-host an internal MCP server (jira-mcp) talking to internal Jira. Claude Desktop must auth users via your existing Keycloak realm.
Topology
User on a laptop with Docker Desktop + MCP Toolkit.
OAuth for MCP is not a new protocol — it's a small set of pinned choices on top of OAuth 2.1. Pick a conformant provider, validate aud, and let the Gateway hold your tokens.