OAuth for Deployed MCP Servers
When you build a custom MCP server that needs to call third-party APIs (e.g., Slack, Zendesk, Google) on behalf of users, someone has to run an OAuth flow with that service and hold the resulting tokens. On Runlayer Deploy that is either Runlayer, using an OAuth client you enter on the connector, or your server, hosting its own OAuth authorization server. This guide explains the dual-auth architecture, the two modes, 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 (Runlayer or MCP Server → Third-Party)
When a user first connects, someone runs an OAuth flow with the vendor (Slack, Zendesk, Google, etc.), receives an access token and refresh token, and stores them keyed by the user’s identity. On later tool calls the stored token is used to call the vendor API on the user’s behalf. On Runlayer Deploy that someone is one of:- Runlayer, using a Pre-registered Client you enter on the connector’s Settings tab. Runlayer stores the tokens and sends the user’s vendor access token to your server as the Bearer token on
/mcp. - Your MCP server, acting as a container-hosted authorization server with its own
/.well-known/oauth-authorization-server,/authorize, and/tokenendpoints. Your server stores the 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
DB_TABLE_NAME,DB_TABLE_REGION, andTOKEN_ENCRYPTION_KMS_KEY_ID
TOKEN_ENCRYPTION_KMS_KEY_ID contains the ARN of a customer-managed KMS key with automatic rotation. The deployment task role can use it for kms:GenerateDataKey and kms:Decrypt.
When TOKEN_ENCRYPTION_KMS_KEY_ID is set, DynamoDBTokenStore uses the key automatically to encrypt sensitive token fields with AES-256-GCM. TOKEN_ENCRYPTION_MODE selects how strictly: bridge (the default) also keeps a plaintext copy so older connector builds can still read the record, while encrypted requires ciphertext for all reads and writes and fails closed if a record cannot be decrypted. Runlayer sets encrypted on newly created deployments; deployments created earlier stay on bridge until their tables hold no plaintext-only records. Passing mode in code takes effect only when TOKEN_ENCRYPTION_MODE is unset; if both are set to different values, the store fails at startup.
DynamoDB server-side encryption applies to the whole table. Application-layer encryption applies only when your code uses the KMS key; custom token stores must encrypt sensitive fields before writing them.
pk string + sk string) with TTL support. Example usage:
Connecting OAuth to Runlayer
Both modes start with the vendor: register an OAuth app with it (Slack, Zendesk, Google, etc.) and note the client ID, client secret, and the scopes your server needs. In both modes, registerhttps://<your-runlayer-host>/oauth/callback as the redirect URI with the vendor. Your container gets the same value as the RUNLAYER_OAUTH_CALLBACK_URL environment variable.
Then pick a mode.
Pre-registered Client: Runlayer runs the vendor flow
Use this when the vendor issues you a fixed client ID and secret and you want no OAuth code in your container. Zendesk, for example, publishes no OAuth discovery metadata and offers no dynamic client registration, so this is the mode that fits it. Your server hosts no OAuth endpoints, needs noenable_db, and needs no service.expose. It has two jobs: reject requests to POST /mcp that carry no valid vendor token (respond 401 with a WWW-Authenticate: Bearer header), and use the token it does receive to call the vendor API.
The client lives on the connector, not in runlayer.yaml:
- Run
runlayer deploy initif you have not already. This creates the connector inDraft. - In Runlayer, open the connector and go to its Settings tab.
- Set Authentication to OAuth 2.1 and Registration to Pre-registered Client.
- Enter the Client ID and Client Secret, the vendor’s Authorization URL and Token URL, and the OAuth Scopes your server needs.
- Save, then run
runlayer deploy.
Draft: Runlayer’s connection test reaches your server, gets a 401, and has no OAuth client to sign a user in with, so it does not activate the connector. Once the client is saved, the next deploy that succeeds activates it. You can enter the client before the first deploy or after one that left the connector in Draft.
Container-hosted authorization server: your server runs the vendor flow
Use this when your server needs to control the vendor exchange itself, or when the vendor client secret must stay inside the container. Your server acts as an OAuth authorization server. It hosts:GET /.well-known/oauth-authorization-server— discovery metadata, with every URL built fromDEPLOYMENT_URLGET /authorize— redirects the user to the vendor’s consent pagePOST /token— exchanges authorization codes and refreshes tokensPOST /register— optional. Returns the client Runlayer should use when talking to your server. Without it, an admin enters that client on the connector’s Settings tab instead.- Token storage in DynamoDB (
enable_db: true)
/authorize, and calls your /token. Your server owns the credential exchange with the vendor and the per-user tokens. The vendor client goes in runlayer.yaml as env vars. This is how Runlayer’s built-in Slack, Google Drive, and Gmail connectors work:
Comparison
OAuth Expose Paths and Auth Middleware
When your container hosts the authorization server, its OAuth endpoints must be accessible without a Bearer token:
Only list OAuth endpoints your server actually implements. Typical OAuth expose paths are
/.well-known/*, /register, /authorize, /token, and optionally /revoke.
/mcp stays authenticated and should not be listed in service.expose. Servers that use a Pre-registered Client, and servers without OAuth, host no OAuth endpoints and should not set service.expose.
If your auth middleware is applied globally (blocking all unauthenticated requests), connector registration will fail because Runlayer can’t reach the OAuth discovery routes.
Fix: scope your auth middleware to protect POST /mcp, and allow only the OAuth routes with service.expose in your runlayer.yaml:
FAQ
Does the agent ever see the vendor token (e.g., Slack token)?
Does the agent ever see the vendor token (e.g., Slack token)?
No. The agent only holds a Runlayer Bearer token. The vendor token is stored and used server-side — either by Runlayer (Pre-registered Client) or by your server (container-hosted authorization server).
Why DynamoDB for token storage?
Why DynamoDB for token storage?
Only needed when your server hosts the authorization server. 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.My deploy succeeded but the connector is still in Draft. What's wrong?
My deploy succeeded but the connector is still in Draft. What's wrong?
Runlayer’s connection test did not pass, so it did not activate the connector. If your vendor issued you a fixed client ID and secret, open the connector’s Settings tab, choose Pre-registered Client, enter the client and the vendor’s authorization and token URLs, and deploy again. If your server hosts the authorization server, your auth middleware is probably blocking its endpoints: make sure
/.well-known/oauth-authorization-server, /authorize, and /token are listed in service.expose and served without a Bearer token.Can I look at an example?
Can I look at an example?
The Runlayer-built connectors (Slack, Google Drive, Gmail) host their own authorization server: the server runs the vendor flow and stores tokens in DynamoDB. Open-source examples are coming soon.