OAuth for Deployed MCP Servers
When you build a custom MCP server that needs to call third-party APIs (e.g., Slack, Google, Atlassian) on behalf of users, your server needs to broker its own OAuth flow with that service. This guide explains the dual-auth architecture and how to persist tokens across container restarts.Dual-Auth Architecture
There are two completely independent OAuth flows involved when an agent uses your deployed MCP server:Auth 1 — Runlayer Platform Auth (Agent → MCP Server)
The agent authenticates to your MCP server using a Runlayer Bearer token. This token comes from Runlayer platform auth (e.g., Rippling SSO → Runlayer). The agent stores this token locally and sends it in theAuthorization header on every request to /mcp.
This token identifies who is calling. Your server uses it to look up the right vendor tokens for that user.
Auth 2 — Vendor OAuth (MCP Server → Third-Party)
Your MCP server acts as an OAuth authorization server itself. It hosts standard OAuth endpoints:/.well-known/oauth-authorization-server— discovery metadata/authorize— redirects the user to the vendor’s consent screen/token— exchanges authorization codes and refreshes tokens
DynamoDB Storage
Runlayer can provision a DynamoDB table for your deployed container. This is useful for any persistent storage your server needs — OAuth tokens, user preferences, cached data, etc. In yourrunlayer.yaml, enable it with:
- Creates a DynamoDB table with server-side encryption
- Sets up IAM permissions for your container
- Injects two environment variables:
DB_TABLE_NAMEandDB_TABLE_REGION
pk string + sk string) with TTL support. Example usage:
Connecting OAuth to Runlayer
Both approaches below start the same way:- Register an OAuth app with the vendor (Slack, Google, etc.) — note the client ID, client secret, and scopes
- Pass the credentials as env vars in your
runlayer.yaml - Expose
/.well-known/oauth-authorization-serverpublicly (no auth) so Runlayer can discover your server’s OAuth configuration
Option A: Let Runlayer handle OAuth (simpler)
Your server implements a/register endpoint (Dynamic Client Registration) that returns the OAuth credentials it already has from env vars. When you register the connector, Runlayer calls /register, gets the credentials, and takes over from there — it drives the OAuth flow with the vendor and stores the resulting tokens.
/authorize or /token, doesn’t need to store tokens, and doesn’t need enable_db. It just receives a Bearer token on POST /mcp and uses it for vendor API calls.
This is the same pattern used by Runlayer’s built-in connectors.
Option B: Server handles OAuth itself (self-contained)
Your server implements the full OAuth flow internally:POST /authorize— redirect the user to the vendor’s consent pagePOST /token— exchange auth codes for tokens, handle refresh- Token storage in DynamoDB (via
enable_db: true)
Comparison
| Option A (Runlayer handles OAuth) | Option B (Server handles OAuth) | |
|---|---|---|
| Server implements | /.well-known/* + /register | /.well-known/* + /authorize + /token + token storage |
| Token storage | Runlayer | Your server (DynamoDB) |
enable_db needed | No | Yes |
| Server complexity | Lower | Higher |
Manual fallback
If you don’t implement DCR (/register) and your server doesn’t handle OAuth itself, Runlayer will detect the OAuth endpoints via discovery and prompt you to enter the client ID and secret manually in the UI. This works but requires a manual step each time you set up the connector.
Public Endpoints and Auth Middleware
Regardless of which option you choose, certain endpoints must be accessible without a Bearer token:| Endpoint | Purpose | Required for |
|---|---|---|
GET /.well-known/oauth-authorization-server | OAuth discovery | Both options |
POST /register | Dynamic Client Registration | Option A |
POST /authorize | OAuth authorization redirect | Option B |
POST /token | OAuth token exchange | Option B |
POST /mcp should require authentication.
If your auth middleware is applied globally (blocking all unauthenticated requests), connector registration will fail because Runlayer can’t reach the discovery endpoints.
Fix: scope your auth middleware to only protect POST /mcp, or use service.expose in your runlayer.yaml:
FAQ
Q: Does the agent ever see the vendor token (e.g., Slack token)? A: No. The agent only holds a Runlayer Bearer token. The vendor token is stored and used server-side — either by Runlayer (Option A) or by your server (Option B). Q: Are the two OAuth flows related? A: No. Runlayer platform auth (SSO → Bearer token) and vendor OAuth (your server → Slack) are completely independent. The Runlayer token identifies the user; the vendor token authorizes API calls. Q: Why DynamoDB for token storage? A: Only needed for Option B. Deployed containers can restart or scale at any time, so in-memory or file-based storage won’t survive.enable_db: true gives you persistent storage with no setup.
Q: My connector registration fails. What’s wrong?
A: Most likely your auth middleware is blocking the discovery endpoints. Make sure /.well-known/oauth-authorization-server and any other required endpoints (see table above) are publicly accessible. Only POST /mcp should require a Bearer token.
Q: Can I look at an example?
A: The Runlayer-built connectors (Slack, Google Drive, Gmail) follow Option B — the server hosts its own OAuth endpoints, brokers the vendor flow, and stores tokens in DynamoDB. Open-source examples are coming soon.