Token broker

The optional service behind delegated credentials — endpoints, provider registry, environment and installation.

The token broker is the optional companion service behind Delegated credentials. It holds a registry of OAuth 2.0 providers and an encrypted per-user token store, and hands Gate One the access token of one authenticated user for one provider.

It is deliberately not an identity provider and not a reverse proxy: Gate One keeps its own identity; the broker only obtains and stores the user’s token for an upstream service.

Pause, resume and erase

Disconnect is a pause, not a deletion. The credential stays stored and Connect resumes the same delegation; the provider is never contacted by pause or erase.

ActionEffectProvider called?

Connect

starts the OAuth flow when nothing is stored, or resumes a paused delegation with the stored token (refresh if needed; OAuth if the token can no longer work)

on a new authorization only

Disconnect

pauses the delegation: Gate One receives 403 delegation_disabled; the credential is kept

no

Erase

deletes the stored credential and metadata locally

no

Erase all my data

deletes every stored credential of the user

no

Erase is local only on purpose: the same credential may be used by the user elsewhere. To remove the application authorization itself, the user revokes it at the provider, in the provider’s own settings. The broker never calls the provider on erase (there is no revocation endpoint in the registry).

Rows are listed even when their provider is disabled or removed from providers.json: the user can still Erase them.

Where the code lives

The reference implementation is services/token-broker/ in the Gate One source repository, and it ships as services_<version>_src.tar.gz in each release. It is a small Node.js service with its own Dockerfile; it is not part of the gate-one binary.

Install it

The broker runs as a separate container, reachable only from the internal network:

docker build -t gate-one-token-broker services/token-broker

docker run -d --name token-broker \
  --network internal \
  --read-only --tmpfs /tmp:size=16m,mode=1777 \
  --cap-drop ALL --security-opt no-new-privileges \
  -e TOKEN_BROKER_SHARED_SECRET=... \
  -e TOKEN_BROKER_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
  -e TOKEN_BROKER_PUBLIC_BASE_URL=https://login.example.com \
  -e TOKEN_BROKER_RETURN_TO_HOSTS=.example.com \
  -e GITHUB_CLIENT_SECRET=... \
  -v token_broker_data:/data \
  gate-one-token-broker
  • No published port and no ingress route: the broker is on the internal network only. Gate One reaches it at tokenBroker.url (default http://token-broker:4000).

  • Browser routes (/connections, /connect/, /oauth/) are published through the login host and served by Gate One: set tokenBroker.browserHost; Gate One authenticates the user with the configured provider and injects the identity. Do not expose the broker itself.

  • Only Gate One holds TOKEN_BROKER_SHARED_SECRET (with the broker itself). auth-bridge no longer receives it: its legacy broker routes are inert (503). An ingress may route the login host to Gate One, but it must never proxy the browser routes directly to the broker: the broker would answer 401 without the secret.

  • State: the encrypted SQLite store lives on the /data volume; the key is in the environment, so a backup needs both the volume and the key.

  • Hardening: read-only root, dropped capabilities, non-root user.

Note

Environment variables (all optional unless marked):

VariableRequiredPurpose

TOKEN_BROKER_SHARED_SECRET

yes

internal X-Broker-Secret shared with Gate One

TOKEN_BROKER_ENCRYPTION_KEY

yes

AES-256-GCM key, 32 bytes base64

TOKEN_BROKER_PUBLIC_BASE_URL

no

public login origin (callback/connect URLs)

TOKEN_BROKER_RETURN_TO_HOSTS

no

comma list of allowed returnTo hosts

TOKEN_BROKER_DB_PATH

no

SQLite path (default /data/token-broker.db)

TOKEN_BROKER_PROVIDERS_FILE

no

registry path (default /app/providers.json)

TOKEN_BROKER_STATE_TTL_SECONDS

no

authorization state lifetime (default 600)

TOKEN_BROKER_REFRESH_SKEW_SECONDS

no

refresh this long before expiry (default 60)

TOKEN_BROKER_HTTP_TIMEOUT_SECONDS

no

provider HTTP timeout (default 15)

TOKEN_BROKER_ADMIN_SECRET

no

operator API secret (X-Broker-Admin-Secret). Absent = admin API disabled (404). Gate One does not hold it

TOKEN_BROKER_RETENTION_DAYS

no

erase paused connections after N days. 0 (default) = keep forever; active delegations are never purged

Add a delegation service

A provider is a registry entry, not code. Edit providers.json:

{
  "providers": {
    "github": {
      "enabled": true,
      "display_name": "GitHub",
      "type": "oauth2",
      "authorization_endpoint": "https://github.com/login/oauth/authorize",
      "token_endpoint": "https://github.com/login/oauth/access_token",
      "client_id": "Iv23li...",
      "client_secret_env": "GITHUB_CLIENT_SECRET",
      "scopes": [],
      "pkce": false,
      "refresh": true,
      "token_auth_method": "client_secret_post"
    }
  }
}
FieldMeaning

enabled

disabled providers are neither advertised nor require a secret

type

oauth2

authorization_endpoint, token_endpoint

HTTPS (HTTP only on loopback)

client_id

provider client id

client_secret_env

environment variable holding the secret (never inline)

scopes

requested scopes; [] for GitHub App permissions

pkce

send S256 PKCE (default true)

refresh

allow the refresh-token grant (default true)

token_auth_method

client_secret_post (default), client_secret_basic or none

authorization_params

extra string parameters; protocol params cannot be overridden

Adding Google, Slack or an internal OAuth service is a registry entry, not a code change. Only genuinely non-standard providers need an adapter.

Gate One references the provider name (routes[].upstream.tokenBroker.provider) and never sees its secret.

Endpoints

MethodPathPurpose

GET

/health

liveness/readiness (no auth)

GET

/providers

enabled provider names and connection status

GET

/connections

browser page (HTML/JSON): states and actions

POST

/connections/{provider}/disconnect

pause the delegation (no delete, no provider call)

POST

/connections/{provider}/connect

resume a paused delegation (OAuth fallback)

POST

/connections/{provider}/erase

delete the local credential (no provider call)

POST

/connections/erase-all

delete every credential of the user

GET

/connect/{provider}

start the authorization-code flow

GET

/oauth/{provider}/callback

exchange the code, store the tokens

GET

/token/{provider}

fresh access token, or an explicit error

DELETE

/token/{provider}

internal local deletion (works for a disabled provider)

GET

/security

redacted posture (shared secret)

GET/POST/DELETE

/admin/…​

operator API (separate admin secret)

/token/{provider} answers 200 with the access token, 404 not_connected, 403 delegation_disabled (the user paused the delegation; the credential is kept), 409 reauthorization_required (refresh rejected; the row is deleted) or 502 upstream_unavailable — each non-200 with a connect_url.

Gate One declares the broker once and a provider per 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/*", "backend": "https://api.github.com",
  "auth": {}, "upstream": { "tokenBroker": { "provider": "github" } }
}]

browserHost is the public host on which Gate One serves the browser routes. It may be omitted when links.services.url is an absolute URL: Gate One derives the host from it (here login.example.com). Set it explicitly when the link is absent, relative, or served by another host. If both are absent, Gate One does not serve the page; an ingress must never proxy those paths directly to the broker.

See Delegated credentials for the route details and the tokenBroker reference.

Operator API

Set TOKEN_BROKER_ADMIN_SECRET and call the admin endpoints with X-Broker-Admin-Secret from the internal network:

curl -H "X-Broker-Admin-Secret: $ADMIN" http://token-broker:4000/admin/users
curl -H "X-Broker-Admin-Secret: $ADMIN" \
  http://token-broker:4000/admin/users/<sub>
curl -X POST -H "X-Broker-Admin-Secret: $ADMIN" \
  http://token-broker:4000/admin/users/<sub>/github/pause
curl -X DELETE -H "X-Broker-Admin-Secret: $ADMIN" \
  http://token-broker:4000/admin/users/<sub>

The operator can list users and connections, pause/resume a delegation, and delete credentials, but cannot add or replace a credential: no write endpoint exists. Responses never contain a token value. Every admin action is audited in the broker log.

The operator API is disabled when the secret is absent. The broker must stay on the internal network: the admin secret is the only credential.

The shared secret is a bearer secret: it proves that the caller knows it, not who the caller is. That is why only Gate One (and the broker) may hold it; the infrastructure check refuses a configuration where another service (auth-bridge included) receives TOKEN_BROKER_SHARED_SECRET. The browser routes are always served by Gate One, never by a second proxy pointing at the broker.

Security posture

GET /security (shared secret) returns a redacted view for gate-one doctor and the infrastructure checks: public base URL, returnTo hosts, encryption enabled, admin API enabled, retention, database permissions, enabled providers (HTTPS, PKCE, refresh, auth method), token/user/paused/orphan counts. No secret and no token value is returned.

The broker never validates the quality of the shared secret or of the provider secrets: that belongs to the deployment/provider. It does enforce the functional gates: secret present, AES key = 32 bytes, HTTPS endpoints, strict provider fields, local file permissions.

Licence

The broker is part of the Gate One distribution and follows the repository licence; it is not an authentication adapter. See Source and licence.