Remote MCP servers increasingly require OAuth 2.0 instead of a static bearer token. Genesis runs the authorization-code flow from the Control UI, stores the resulting tokens encrypted on the gateway host, and refreshes them automatically.
Flow
sequenceDiagram
participant UI as Control UI
participant GW as Gateway
participant P as OAuth provider
UI->>GW: mcp.oauth.start
GW-->>UI: authorize URL (PKCE S256)
UI->>P: open popup to authorize URL
P-->>UI: redirect to /mcp-oauth-callback.html?code&state
UI->>UI: callback page postMessage(code, state)
UI->>GW: mcp.oauth.callback (code, state)
GW->>P: exchange code + verifier for tokens
GW-->>UI: connected
- The operator clicks Connect on an OAuth-required server in the Control UI.
mcp.oauth.startbuilds the provider authorize URL. Genesis generates a PKCE verifier and sends the S256 challenge; the verifier is bound to the requeststateand never leaves the gateway.- A popup opens the provider consent screen. After consent the provider
redirects to the gateway-served callback page at
/mcp-oauth-callback.html. - The callback page posts the
codeandstateback to the Control UI. The UI only accepts that message when it comes from the gateway origin (see Callback origin checks). mcp.oauth.callbackexchanges the code (plus the PKCE verifier and any client secret) at the token endpoint and stores the resulting tokens.
Token storage
Tokens live on the gateway host, never in the browser:
- File:
~/.genesis/mcp-oauth.json, written atomically with0600permissions. - Encryption: the file is encrypted at rest with AES-256-GCM under a
machine-local key at
~/.genesis/mcp-oauth.key(also0600). If the key is missing or the file cannot be decrypted, Genesis treats the server as not connected and never deletes the file, so an operator can investigate. - Redaction: server configs written to logs strip
auth.clientSecretand anyAuthorizationheader.
Legacy plaintext stores from earlier builds are read transparently and re-encrypted on the next write.
Refresh and revoke
- Tokens are refreshed automatically when a request finds them within 60 seconds of expiry, using the stored refresh token (RFC 6749 section 6). A new refresh token is adopted when the provider issues one; otherwise the existing one is kept.
mcp.oauth.refresh(admin scope) forces a refresh on demand.- Disconnecting a server makes a best-effort RFC 7009 revocation call when a
revokeUrlis known, then removes the local token. Revocation failures never block disconnect.
Discovery
When you add a server by link, Genesis probes it for OAuth requirements:
- The standard
/.well-known/oauth-authorization-serverand/.well-known/openid-configurationmetadata under the server origin. - RFC 9728
/.well-known/oauth-protected-resource, following the first advertised authorization server to its metadata. - An unauthenticated
initializeprobe. A401with aWWW-Authenticate: Bearerchallenge (or a JSON-RPC authorization error) marks the server as OAuth-required and, when present, theresource_metadatahint is followed.
Manual configuration
If a provider is not auto-detected, set the auth block directly under
mcp.servers.<name>.auth (JSON tab in the Control UI, or the config file):
{
"mcp": {
"servers": {
"notion": {
"url": "https://mcp.notion.com/mcp",
"auth": {
"authorizeUrl": "https://api.notion.com/v1/oauth/authorize",
"tokenUrl": "https://api.notion.com/v1/oauth/token",
"clientId": "your-registered-client-id",
"clientSecret": "your-client-secret",
"scopes": ["read", "write"],
"revokeUrl": "https://api.notion.com/v1/oauth/revoke",
"usePkce": true
}
}
}
}
}
clientIdmust be the id you registered with the provider. When omitted, Genesis falls back to a generic identifier that most providers will reject.usePkcedefaults totrue. Set it tofalseonly for providers that do not support PKCE.clientSecretis optional for public clients that rely on PKCE alone.
Self-hosted servers
Outbound metadata and OAuth requests are SSRF-guarded: by default Genesis blocks private, loopback, link-local, and other internal addresses, and pins DNS to prevent rebinding. To reach an MCP server on a private network, allow its hostname explicitly:
{
"mcp": {
"metadataFetch": {
"allowedHosts": ["mcp.internal.example"]
}
}
}
Listing a host exempts it from the private-address block. Leave this empty unless you operate the target server.
Callback origin checks
The callback page is served same-origin by the gateway. The Control UI accepts a callback message only when it carries the Genesis source tag and originates from the gateway web origin. When a browser reports an opaque origin for the popup, the UI falls back to verifying the message came from the popup window it opened. This prevents a malicious page from injecting a forged authorization code.