Call services with the user's credentials

Give a route or an MCP tool the signed-in user’s own token for an external service, through the token broker.

Useful if you want to…

  • let a route or an MCP tool call an external API as the user, not as a shared service account;

  • keep the provider’s client secret out of Gate One;

  • let every user reach the upstream with their own rights;

  • give users a page where they connect, pause and erase those services.

The two credential shapes

When a route calls an external API, the credential can be:

  • static — one shared value for every caller, set with upstream.headers (for example a service token);

  • delegated — the signed-in user’s own token, obtained after they authorize the service once.

Gate One implements the delegated shape with the token broker. upstream.tokenBroker on a route tells Gate One to fetch the caller’s token from the broker and inject it into the upstream request. It applies to plain proxied API routes and to the REST/MCP tools a route exposes — see MCP tools.

Important
If the upstream needs the same credential for every caller, do not delegate. Set it once with upstream.headers, for example:
"upstream": {
  "headers": { "Authorization": "Basic ${SERVICE_BASIC_AUTH}" }
}

Delegation is only for credentials that belong to the individual user. A shared service account, a static bearer token or a Cloudflare Access service token is simpler and has no connection page to manage.

What delegation is not

Delegation is not an identity provider and not SSO:

  • the IAM/IdP still decides who the user is (auth, auth.identity);

  • the token broker only stores a token the user granted for one upstream service.

The names are easy to confuse, so see IAM, IdPs and provider types for the cladone-auth IAM (an OIDC issuer/IAM), the http-bridge provider type and the token broker side by side.

The token broker and the connections page

The token broker is a small, internal-only service: a registry of OAuth providers plus an encrypted per-user token store. It is never exposed directly; only its browser routes are published through the login host, served by Gate One (tokenBroker.browserHost, provider-agnostic). auth-bridge no longer holds the broker secret and cannot proxy them: only Gate One (and the broker) has TOKEN_BROKER_SHARED_SECRET.

Users authorize a service on the connections page ("Service connections"). Gate One links to it from the user badge as Services, and shows that link only when an exposed route declares delegation.

For the user

The link is always present when the token broker is configured, even if no route currently delegates: it is also where the user erases stored credentials.

  1. Open Services from the user badge (or follow the connect_url a delegated call returned).

  2. Next to the service (for example GitHub), choose Connect and authorize it at the provider.

  3. The broker stores the token; the next call to a delegated route uses it.

  4. Disconnect pauses the delegation. It does not delete the credential and does not contact the provider; Connect resumes the same credential.

  5. Erase deletes the stored credential locally (and its metadata). The provider is not contacted: the same credential can still be used elsewhere. Erase all my data does this for every service.

If the token can no longer be refreshed, the connection is marked reauthorization required and Gate One sends the user back to this page (409 reauthorization_required for an API call, a redirect for a browser route). A paused delegation answers 403 delegation_disabled with the Services URL.

Stored rows whose provider was disabled or removed from the registry are still listed, with Erase only: the user can always delete their own data.

Add a delegation service

A provider is a registry entry, not code: the token broker’s providers.json lists each OAuth 2.0 service (endpoints, client id, scopes, …). Adding GitHub, Google, Slack or an internal OAuth service is a registry entry; its client secret comes from the broker’s environment, never from the file.

Gate One only references the provider name (upstream.tokenBroker.provider). See the token broker for the fields and an example.

Declare delegation on a route

{
  "tokenBroker": {
    "url": "http://token-broker:4000",
    "secretEnv": "TOKEN_BROKER_SHARED_SECRET",
    "browserHost": "login.example.com",
    "links": {
      "services": {
        "label": "Services",
        "url": "https://login.example.com/connections"
      }
    }
  },
  "routes": [
    {
      "host": "api.example.com",
      "path": "/github/*",
      "stripPrefix": "/github",
      "backend": "https://api.github.com",
      "auth": {},
      "upstream": {
        "host": true,
        "tokenBroker": { "provider": "github" }
      }
    }
  ]
}
  • auth is required: the broker needs an authenticated user id.

  • provider is a broker provider name (github), validated against the broker at startup.

  • the token is sent as Authorization: Bearer … by default; set header and prefix to change the header or the prefix.

  • browserHost is where Gate One serves the connections page and the OAuth callback (/connections, /connect/, /oauth//callback), authenticating the user with the configured provider and injecting the identity. Empty = derived from links.services.url. The browser routes must not be proxied directly to the broker, and auth-bridge does not receive the broker secret.

  • the shared secret is read from the environment named by secretEnv and is never written in the file.

  • the operator can list connections (metadata only), pause/resume a delegation and delete credentials through a separate admin API (TOKEN_BROKER_ADMIN_SECRET, never held by Gate One). The operator cannot add or replace a credential.

  • retention is unlimited by default (TOKEN_BROKER_RETENTION_DAYS=0); when enabled, it only purges paused connections.

Note
The tokenBroker block and the route are shown together because a route that declares upstream.tokenBroker requires the top-level tokenBroker block; without it the configuration is refused at load. This is a partial extract (the providers and other routes are omitted) — the complete file is examples/mcp-tools/routes.json.

When the user has not connected the service

Gate One never sends a request without credentials:

  • an API/JSON call gets 401/409 with a connect_url the client can open;

  • a browser (frontend) route redirects to the connections page.

Important
  • Gate One never stores the provider’s client secret; the broker does, and only the broker talks to the provider’s token endpoint.

  • Disconnect pauses; Erase deletes locally; neither calls the provider. Revoke the application authorization at the provider if that is the intent.

  • Destructive actions are CSRF-protected (signed form token + origin check).

  • The broker is internal-only and protected by a shared secret read from the environment.

  • Reserved header names (Host, Cookie, X-User-, X-Forwarded-, X-Gate-One-*) are refused for header.

  • A delegated route requires auth: there is no anonymous delegation.

Complete example

MCP tools shows the token broker with a delegated API route and delegated MCP tools.

Configuration reference