Docs/OAuth 2.1 setup
OAuth 2.1

OAuth 2.1 proxy setup

ModGate implements the full OAuth 2.1 authorization code flow with PKCE and Dynamic Client Registration — exactly as required by the MCP specification. This page explains how it works and how to integrate it with your agent or client.

1. Discovery endpoint

Every ModGate deployment exposes a compliant OAuth 2.1 discovery endpoint. MCP clients that support the spec auto-discover everything they need from here — no manual configuration required.

GET https://api.modgate.io/.well-known/oauth-authorization-server

{
  "issuer": "https://api.modgate.io",
  "authorization_endpoint": "https://api.modgate.io/oauth/authorize",
  "token_endpoint": "https://api.modgate.io/oauth/token",
  "registration_endpoint": "https://api.modgate.io/oauth/register",
  "scopes_supported": ["mcp:read","mcp:write","mcp:execute","mcp:admin"],
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": ["authorization_code","refresh_token"]
}

2. Dynamic Client Registration

ModGate supports RFC 7591 Dynamic Client Registration. AI agents and MCP clients register themselves programmatically — no manual client setup or pre-shared credentials required.

POST https://api.modgate.io/oauth/register
Content-Type: application/json

{
  "client_name": "My AI Agent",
  "redirect_uris": ["https://myapp.com/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "scope": "mcp:read mcp:execute",
  "token_endpoint_auth_method": "none"
}

# Response:
{
  "client_id": "client_abc123...",
  "client_secret": null,
  "client_name": "My AI Agent",
  "redirect_uris": ["https://myapp.com/callback"]
}
For public clients (CLI tools, browser-based agents), set token_endpoint_auth_method to "none". PKCE is mandatory for all public clients.

3. Authorization code flow with PKCE

ModGate uses PKCE (Proof Key for Code Exchange) with the S256 method. The full flow:

Step 1 — Generate PKCE values

# Generate a cryptographically random code verifier (43-128 chars)
code_verifier = base64url(random_bytes(32))

# Derive the code challenge
code_challenge = base64url(sha256(code_verifier))

Step 2 — Redirect to authorization endpoint

GET https://api.modgate.io/oauth/authorize
  ?response_type=code
  &client_id=client_abc123
  &redirect_uri=https://myapp.com/callback
  &scope=mcp:read mcp:execute
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256
  &state=RANDOM_STATE_VALUE

Step 3 — Exchange code for tokens

POST https://api.modgate.io/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://myapp.com/callback
&client_id=client_abc123
&code_verifier=ORIGINAL_CODE_VERIFIER

# Response:
{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "rt_...",
  "scope": "mcp:read mcp:execute"
}
Access tokens expire after 15 minutes. Use the refresh token to get new access tokens without requiring user interaction — critical for headless AI agents, and refreshing doesn't count against your authentication quota. This TTL is deliberate: paired with continuous authorization, which re-checks live agent state on every proxied request regardless of the token's own expiry, a longer-lived token doesn't weaken revocation speed — a revoked or force-reauthed agent is still cut off on its very next request.

Step 4 — Refresh access tokens

POST https://api.modgate.io/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=rt_...
&client_id=client_abc123

Refresh is denied outright if the agent has been revoked or has expired. If the agent was force-reauthed by an admin after this refresh token was issued, refresh is also denied — you'll need a full new authorization_code exchange, not just a refresh, to clear a force-reauth. Refreshing is not itself a billable authentication — only the original authorization_code exchange and delegation issuance count toward your quota.

4. Scopes

ScopeDescription
mcp:readRead-only access — list tools, read resources
mcp:writeWrite access — create or update resources
mcp:executeExecute tools — call any tool on the server
mcp:adminFull admin access — use with caution

Scopes are further constrained by agent-level policies. Even if a token has mcp:execute, an agent configured to only call read_contacts will be denied any other tool call. See Agent identity for per-tool policies.

5. Token introspection and revocation

Introspect a token

POST https://api.modgate.io/oauth/introspect
Content-Type: application/x-www-form-urlencoded
X-ModGate-API-Key: YOUR_API_KEY

token=eyJ...

# Response:
{
  "active": true,
  "sub": "agent_xyz",
  "scope": "mcp:read mcp:execute",
  "exp": 1735000000,
  "aud": "mcp:your-tenant-id"
}

Revoke a token

POST https://api.modgate.io/oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=eyJ...
&client_id=client_abc123

Revocation takes effect immediately for any request through the ModGate MCP proxy — every proxied call re-checks the agent's live status against its current stored state, not just the token's signature. Third-party resource servers calling /oauth/introspect get this same live check, so active correctly flips to false the moment an agent is revoked. Only a server that validates the JWT signature entirely on its own, without ever calling ModGate again, is limited to revocation-at-expiry — now a maximum of 15 minutes. Introspection itself is never a billable event.

6. Cross-agent delegation (xAA)

Orchestrator agents with delegation enabled can mint scoped tokens for sub-agents they spawn, without those sub-agents ever going through the authorization code flow themselves. The delegated token embeds a cryptographically verifiable chain back to the root authority.

POST https://api.modgate.io/oauth/delegate
Authorization: Bearer ORCHESTRATOR_ACCESS_TOKEN
Content-Type: application/json

{
  "sub_agent": "agent_worker_42",
  "scope": "mcp:read mcp:execute",
  "ttl": 900
}

# Response:
{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "mcp:read mcp:execute",
  "delegation": {
    "chain": ["agent_orchestrator_1", "agent_worker_42"],
    "depth": 1,
    "delegated_by": "agent_orchestrator_1"
  }
}
A sub-agent can only receive scopes the orchestrator already holds, and delegation depth is capped by the orchestrator's configured maximum. Revoke the orchestrator — or let its own session go stale under continuous authorization — and delegation is denied immediately, even mid-chain. Unlike a refresh, each delegated token is a billable authentication.

See Agent identity for the full delegation model, and Continuous authorization for how live session checks apply to orchestrators, not just their sub-agents.