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

MCPGate OAuth clients are public clients as defined by RFC 6749. There is no 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.

Discovery
bash
# OAuth server metadata
GET https://mcpgate.sh/.well-known/oauth-authorization-server

# Public keys for JWT verification
GET https://mcpgate.sh/oauth/jwks

Step 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.

register.sh
bash
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"]
  }'
Response
json
{
  "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

MCPGate requires PKCE on every authorization request. Requests without code_challenge will be rejected with {"error":"invalid_request","error_description":"missing required parameters"}. Standard OAuth without PKCE is not supported.

Required parameters#

ParameterRequiredDescription
client_idYesFrom POST /oauth/register response
redirect_uriYesMust match one of the registered redirect URIs
response_typeYesMust be code
code_challengeYesbase64url(SHA256(code_verifier)) — RFC 7636
code_challenge_methodRecommendedMust be S256 if provided (only supported method)
stateYesRandom CSRF token — validated on callback
authorize.ts
typescript
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:

ParameterRequiredDescription
grant_typeYesMust be authorization_code
client_idYesSame client_id used in the authorize request
codeYesAuthorization code from the callback
code_verifierYesThe original random string used to generate code_challenge (43-128 chars)
redirect_uriYesMust match the redirect_uri from the authorize request
callback.ts
typescript
// 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.

mcp-client.ts
typescript
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

Access tokens are short-lived JWTs. When a token expires, the user must re-authorize. For long-running agents, implement a refresh mechanism or prompt re-authorization when you receive a 401 Unauthorized response.

Security best practices#

  • Always use a fresh, random code_verifier for every authorization request — never reuse.
  • Validate the state parameter 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.