Auth0 in front of an MCP server, with a thin proxy
The architecture that got a self-hosted MCP server through claude.ai connector validation: Auth0 for login and tokens, a small proxy for the metadata and DCR endpoints Auth0 does not serve the way MCP clients expect.
auth0 · updated 2026-08-22
Auth0 gives you login, token issuance, and refresh out of the box — but an MCP client discovers your OAuth setup through metadata endpoints on your domain, and registers itself via dynamic client registration. Pointing claude.ai straight at an Auth0 tenant fails on both counts. The fix is a thin proxy on your MCP domain that owns discovery and DCR, and delegates the actual authorization to Auth0.
The shape
claude.ai
│
├─ /.well-known/oauth-protected-resource ← proxy (static JSON)
├─ /.well-known/oauth-authorization-server ← proxy (static JSON, endpoints on your domain)
├─ /register ← proxy (DCR: issue/echo a client_id)
├─ /authorize ──────────────► Auth0 /authorize (redirect, params mapped)
├─ /token ──────────────────► Auth0 /token (server-to-server exchange)
│
└─ /mcp ── Bearer token ──► your MCP server (validates Auth0 JWT)
Everything the client sees lives on one domain. Auth0 only appears in the middle of the browser redirect, where users expect a login page.
Auth0 tenant setup
- Create a Regular Web Application. Note the client ID and secret — the
proxy uses these when it forwards
/token. - Create an API in Auth0 (this becomes the JWT
audience), identifier set to your MCP URL, e.g.https://yourdomain.com/mcp. - Allowed callback URLs: your proxy’s
/callbackif you terminate the redirect yourself, or the claude.ai callbacks directly if you pass them through:https://claude.ai/api/mcp/auth_callback,https://claude.com/api/mcp/auth_callback.
The proxy endpoints
Metadata — two static JSON routes. The authorization-server document
advertises your /authorize, /token, /register; S256 in
code_challenge_methods_supported.
/register (DCR) — Auth0’s open DCR is off by default and issues clients
without your API’s grants. Simplest reliable approach: don’t call Auth0 at
all. Accept the registration, store the redirect_uris, and return your
existing Auth0 application’s client_id. Every MCP client shares one Auth0
app; per-client isolation comes from the tokens, not the client record.
/authorize — 302 to
https://TENANT.auth0.com/authorize?audience=https://yourdomain.com/mcp&…
forwarding client_id (mapped to the real Auth0 one), redirect_uri,
state, code_challenge, code_challenge_method, scope.
/token — forward the code exchange to Auth0’s /token, adding the
client secret. Return Auth0’s JSON unchanged. Refresh grants pass through the
same way.
Token validation at the MCP server
Validate the incoming Authorization: Bearer JWT against your tenant’s JWKS
(https://TENANT.auth0.com/.well-known/jwks.json): signature, iss, exp,
and — the one people skip — aud equal to your API identifier. Reject with
401 + WWW-Authenticate so clients know to re-authenticate.
Serving it
Any TLS-terminating reverse proxy works. A minimal Caddyfile:
yourdomain.com {
handle /mcp* {
reverse_proxy localhost:8000
}
handle /.well-known/* {
reverse_proxy localhost:8001
}
handle /authorize* { reverse_proxy localhost:8001 }
handle /token* { reverse_proxy localhost:8001 }
handle /register* { reverse_proxy localhost:8001 }
}
Caddy provisions and renews the Let’s Encrypt certificate on its own. The only DNS requirement: the hostname resolves publicly — see the pre-flight section of the checklist.
Verify
Run the four-curl self-test from the connector checklist, then add the connector in claude.ai → Settings → Connectors. You should land on the Auth0 login page, and after consent the tools list should populate.
Metadata
- Commit
- 650e1b8
- Browser
- —
- Current Time
- —
- Dimensions
- —
- Source
- guides
- Last Updated
- 2026-08-22