OAuth Setup
MCPGate uses OAuth 2.0 with PKCEto authenticate external applications. Your app registers once, directs users through a standard authorization flow, and receives a short-lived JWT access token scoped to that user's MCPGate account. No client secret is required — security is provided by the PKCE challenge.
Public client
client_secret. PKCE (code_challenge_method=S256) is mandatory and provides equivalent security for native and web apps.Discovery endpoints#
MCPGate publishes standard OAuth discovery metadata. Point any OAuth library at the well-known URL and it will configure itself automatically.
# OAuth server metadata
GET https://mcpgate.sh/.well-known/oauth-authorization-server
# Public keys for JWT verification
GET https://mcpgate.sh/oauth/jwksStep 1 — Register your client#
Call POST /oauth/register once per application. You will receive a client_id that is permanent — store it in your application configuration and reuse it for every authorization request.
curl -s -X POST https://mcpgate.sh/oauth/register \
-H 'Content-Type: application/json' \
-d '{
"client_name": "My Agent App",
"redirect_uris": ["https://myapp.example.com/oauth/callback"]
}'{
"client_id": "mc_a1b2c3d4e5f6...",
"client_name": "My Agent App",
"redirect_uris": ["https://myapp.example.com/oauth/callback"]
}Step 2 — Redirect users to authorize#
Generate a PKCE code verifier and challenge, then redirect the user to the authorization endpoint. MCPGate shows a login screen (via Clerk) and an app approval page.
PKCE is mandatory
code_challenge will be rejected with {"error":"invalid_request","error_description":"missing required parameters"}. Standard OAuth without PKCE is not supported.Required parameters#
| Parameter | Required | Description |
|---|---|---|
| client_id | Yes | From POST /oauth/register response |
| redirect_uri | Yes | Must match one of the registered redirect URIs |
| response_type | Yes | Must be code |
| code_challenge | Yes | base64url(SHA256(code_verifier)) — RFC 7636 |
| code_challenge_method | Recommended | Must be S256 if provided (only supported method) |
| state | Yes | Random CSRF token — validated on callback |
import crypto from 'crypto'
// Generate PKCE values
const codeVerifier = crypto.randomBytes(64).toString('base64url')
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url')
// Generate CSRF state token
const state = crypto.randomBytes(16).toString('hex')
// Store codeVerifier and state in the user's session before redirecting
session.set('pkce_verifier', codeVerifier)
session.set('oauth_state', state)
// Build the authorization URL
const params = new URLSearchParams({
client_id: process.env.MCPGATE_CLIENT_ID!,
redirect_uri: 'https://myapp.example.com/oauth/callback',
response_type: 'code',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state,
})
res.redirect(`https://mcpgate.sh/oauth/authorize?${params}`)Step 3 — Handle the callback#
After the user approves, MCPGate redirects to your redirect_uri with an authorization code and the state parameter. Verify the state matches, then exchange the code for an access token via POST /oauth/token.
Token exchange parameters#
Send as application/x-www-form-urlencoded:
| Parameter | Required | Description |
|---|---|---|
| grant_type | Yes | Must be authorization_code |
| client_id | Yes | Same client_id used in the authorize request |
| code | Yes | Authorization code from the callback |
| code_verifier | Yes | The original random string used to generate code_challenge (43-128 chars) |
| redirect_uri | Yes | Must match the redirect_uri from the authorize request |
// GET /oauth/callback?code=AUTH_CODE&state=STATE
app.get('/oauth/callback', async (req, res) => {
const { code, state } = req.query
// Verify CSRF state
if (state !== session.get('oauth_state')) {
return res.status(400).send('Invalid state')
}
const codeVerifier = session.get('pkce_verifier')
const response = await fetch('https://mcpgate.sh/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.MCPGATE_CLIENT_ID!,
code: code as string,
code_verifier: codeVerifier,
redirect_uri: 'https://myapp.example.com/oauth/callback',
}),
})
const { access_token, expires_in } = await response.json()
// Store access_token securely — never expose it to the browser
session.set('mcpgate_token', access_token)
res.redirect('/dashboard')
})Step 4 — Use the access token#
Pass the JWT access token as a bearer token on every request to the MCPGate MCP endpoint. The token encodes the user identity and app scope — MCPGate validates it on each request.
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
const transport = new SSEClientTransport(
new URL('https://mcpgate.sh/mcp'),
{
requestInit: {
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
)
const client = new Client({ name: 'my-agent', version: '1.0.0' }, { capabilities: {} })
await client.connect(transport)Per-app isolation#
Each registered OAuth client corresponds to a distinct MCP App in MCPGate. Tokens issued to one client only grant access to the tools and guardrails configured for that app. If you need a second AI client with different permissions, register a second client and create a separate MCP App in the dashboard.
Token lifetime
401 Unauthorized response.Security best practices#
- Always use a fresh, random
code_verifierfor every authorization request — never reuse. - Validate the
stateparameter on every callback to prevent CSRF attacks. - Store access tokens server-side only. Never expose them to the browser or embed in client-side JavaScript.
- Create a separate OAuth client registration for each distinct application or deployment environment.
- Re-authorize promptly when tokens expire rather than extending token lifetime unnecessarily.