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.
Updated
On this page
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 under Customize → Connectors. You should land on the Auth0 login page, and after consent the tools list should populate.
Next steps
Before building this proxy, check whether you still need it: 2026 Auth0 tenants support the resource parameter natively (resource_parameter_profile: "audience") and OIDC dynamic client registration, which removes the proxy entirely; the no-proxy Auth0 guide is the current recommendation and was verified live. If you’re staying on the proxy (older tenant, extra control at the edge):
- Run the four-curl self-test from the connector checklist.
- Connect in claude.ai, then extend to ChatGPT and Grok via one server, three connectors.
- On failures, step through debugging connector failures.
The proxy prompt (auth0-front-mcp) builds this architecture for you.
Put it into practice
Put Auth0 in front of an existing MCP endpoint
Build the thin proxy that owns discovery and DCR on your domain while Auth0 handles login and tokens, accepting Claude, ChatGPT, and Grok as clients.
Preview prompt
Build a thin OAuth proxy in front of my existing MCP server so claude.ai can connect through Auth0. Architecture (all endpoints on MY_DOMAIN): - /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server: static JSON served by the proxy. The authorization-server document advertises MY_DOMAIN's /authorize, /token, /register (not Auth0's) and lists S256 under code_challenge_methods_supported. - POST /register: implement dynamic client registration locally. Store the exact redirect_uris the client sends, validate their hosts against an allowlist (claude.ai, claude.com, chatgpt.com, openai.com, grok.com, x.ai; known callbacks include https://claude.ai/api/mcp/auth_callback and https://chatgpt.com/connector_platform_oauth_redirect), and return my existing Auth0 application's client_id. At /authorize, validate the requested redirect_uri exactly against what was registered. Do not call Auth0's DCR. - GET /authorize: 302 to https://MY_TENANT.auth0.com/authorize, forwarding redirect_uri, state, code_challenge, code_challenge_method, scope; substitute the real Auth0 client_id and append audience=https://MY_DOMAIN/mcp. - POST /token: forward to Auth0's /token server-to-server, injecting the client secret from an environment variable; return Auth0's JSON body unchanged. Support both authorization_code and refresh_token grants. - /mcp: reverse-proxy to my MCP server, passing the Authorization header through untouched. The MCP server itself must validate the JWT: JWKS from https://MY_TENANT.auth0.com/.well-known/jwks.json, check iss, exp, and aud == https://MY_DOMAIN/mcp, and answer 401 with WWW-Authenticate: Bearer resource_metadata="https://MY_DOMAIN/.well-known/oauth-protected-resource" when validation fails. My stack for the proxy is: [fill in, e.g. FastAPI behind Caddy, an Express app, a Cloudflare Worker]. Auth0 tenant: [fill in]. Secrets go in environment variables, never in code. Implement it, then give me the four curl checks (401 probe, both metadata documents, /register) so I can verify discovery end-to-end before trying the connector UI.
Use this site from your agent
Add this URL as a connector in Claude, ChatGPT, or Grok. No sign-in required.
https://quickoauth.com/mcp