Skip to main content

Plugin - Token exchange (OBO delegation)

This plugin lets Gate stand in front of a protected service and exchange the caller's token for a delegated token that acts on behalf of the caller, with Gate (or a configured workload identity) recorded as the actor in the RFC 8693 act claim chain. The original token is stripped from the proxied request and replaced with the delegated one.

Concretely: an MCP server, an internal API or any other consumer can rely on Gate as the request-time delegate. Upstreams receive a delegated token that proves "X acted on behalf of Y" — not the raw user token.

Modes

The plugin supports three independent actor sources, selected with actor_mode. The subject is always read from the inbound request (header or cookie); only the actor changes between modes.

actor_modeWhat Gate sends as actor_token
oauth_clientA client_credentials access token Gate mints for itself against the SlashID /oauth2/tokens endpoint, using a SlashID OAuth2 client registered with the urn:ietf:params:oauth:grant-type:token-exchange grant. Cached locally and refreshed near expiry.
gate_svidA SPIFFE JWT-SVID Gate sources from one of file, env or workload_api (the SPIRE Workload API socket). The SlashID org must list the trust domain in svid_jwks_urls and the SPIFFE ID in allowed_spiffe_ids.
request_actorAn actor token carried by the inbound request itself (configurable header or cookie). Accepts any token shape the SlashID /oauth2/tokens endpoint accepts: a SlashID-issued person JWT, an opaque access token, or a SPIFFE JWT-SVID.

A single Gate config can declare any number of token-exchange plugins side by side — e.g. one route per actor mode — by giving each a distinct id.

Caller-client authentication

When the inbound subject is an opaque SlashID access token, RFC 6749 binds it to the OAuth2 client it was issued to. The SlashID /oauth2/tokens endpoint requires the call to be authenticated as the same client; configure oauth_client_id / oauth_client_secret on the plugin and Gate will attach HTTP Basic auth to the exchange call.

When the subject is a SlashID JWT (e.g. minted with POST /persons/{id}/mint-token), the binding doesn't apply and the call can be clientless — omit oauth_client_id / oauth_client_secret.

actor_mode: oauth_client reuses the same oauth_client_id / oauth_client_secret for both the actor identity (used to mint the client_credentials access token) and the caller-client Basic auth.

Configuring Gate

A minimal config for each actor mode is below. Pick the format you use and the mode that fits your setup; see the Field reference at the bottom for everything you can tune.

# actor_mode = oauth_client
GATE_PLUGINS_<N>_TYPE=token-exchange
GATE_PLUGINS_<N>_PARAMETERS_SLASHID_BASE_URL=https://api.slashid.com
GATE_PLUGINS_<N>_PARAMETERS_SUBJECT_HEADER_WITH_TOKEN=Authorization
GATE_PLUGINS_<N>_PARAMETERS_ACTOR_MODE=oauth_client
GATE_PLUGINS_<N>_PARAMETERS_OAUTH_CLIENT_ID=<oauth2 client id>
GATE_PLUGINS_<N>_PARAMETERS_OAUTH_CLIENT_SECRET=<oauth2 client secret>
GATE_PLUGINS_<N>_PARAMETERS_REQUESTED_TOKEN_TYPE=urn:ietf:params:oauth:token-type:jwt

# actor_mode = gate_svid via the SPIRE Workload API
GATE_PLUGINS_<N>_PARAMETERS_ACTOR_MODE=gate_svid
GATE_PLUGINS_<N>_PARAMETERS_ACTOR_SVID_SOURCE=workload_api
GATE_PLUGINS_<N>_PARAMETERS_ACTOR_WORKLOAD_API_SOCKET=unix:///run/spire/sockets/agent.sock
GATE_PLUGINS_<N>_PARAMETERS_ACTOR_WORKLOAD_API_AUDIENCE=https://api.slashid.com/oauth2/tokens

# actor_mode = request_actor
GATE_PLUGINS_<N>_PARAMETERS_ACTOR_MODE=request_actor
GATE_PLUGINS_<N>_PARAMETERS_ACTOR_HEADER_WITH_TOKEN=X-Actor-Token

In Environment variable configuration, <N> defines plugin execution order.

Field reference

SlashID endpoint

  • slashid_base_url (required) — Base URL of the SlashID API, e.g. https://api.slashid.com. The exchange call goes to <slashid_base_url>/oauth2/tokens.
  • slashid_org_id (optional) — Org id, when needed for the deployment.

Subject (the user the delegated token will act on behalf of)

  • subject_header_with_token — Header to read the subject token from (e.g. Authorization). Mutually exclusive with subject_cookie_with_token.
  • subject_cookie_with_token — Cookie name to read the subject token from. Mutually exclusive with subject_header_with_token.
  • subject_token_type (optional) — Override for the token-type URI sent to the AS. Defaults to inferred from the token shape: a value starting with eyJ is treated as urn:ietf:params:oauth:token-type:jwt, anything else as urn:ietf:params:oauth:token-type:access_token.
  • subject_strip_after_exchange (default true) — Remove the subject token from the request before forwarding it to the upstream.

Actor

  • actor_mode (required) — One of oauth_client, gate_svid, request_actor. See Modes above.
  • actor_svid_source — When actor_mode=gate_svid, where the SVID comes from. One of file, env, workload_api.
  • actor_svid_file_path — When actor_svid_source=file, path to a file containing the raw JWT-SVID. Typically populated by a sidecar like spiffe-helper.
  • actor_svid_file_reload (default 30s) — How often Gate stats the SVID file and reloads when the mtime changes.
  • actor_svid_env_var — When actor_svid_source=env, environment variable containing the JWT-SVID.
  • actor_workload_api_socket — When actor_svid_source=workload_api, the SPIRE agent UDS, e.g. unix:///run/spire/sockets/agent.sock.
  • actor_workload_api_audience — Audience requested when calling FetchJWTSVID. The SlashID AS expects <slashid_base_url>/oauth2/tokens when present.
  • actor_header_with_token — When actor_mode=request_actor, header to read the actor token from. Mutually exclusive with actor_cookie_with_token.
  • actor_cookie_with_token — When actor_mode=request_actor, cookie to read the actor token from. Mutually exclusive with actor_header_with_token.
  • actor_strip_after_exchange (default true) — In request_actor mode, remove the actor header/cookie from the request before forwarding it.

OAuth2 caller client

Required when the subject is an opaque SlashID access token (RFC 6749 binding) and when actor_mode=oauth_client. Optional otherwise.

  • oauth_client_id — The SlashID OAuth2 client id.
  • oauth_client_secret — Corresponding client secret. Sent as HTTP Basic auth on the exchange call.

Exchange request shape

  • requested_token_type (required) — Output token format, one of urn:ietf:params:oauth:token-type:jwt or urn:ietf:params:oauth:token-type:access_token.
  • scope (optional) — Space-delimited scopes to narrow the delegated token. Applies to opaque output only.
  • audience (optional) — Array of audiences to bind into the delegated token (opaque output only).
  • resource (optional) — Array of resource URIs per RFC 8707 (opaque output only).
  • requested_expires_in (optional) — Requested lifetime in seconds; the AS may clamp this against its own ceiling.

Output and transport

  • output_header (default Authorization) — Header Gate writes the delegated token into on the proxied request.
  • output_prefix (default "Bearer ") — Prefix prepended to the delegated token in the output header. Include the trailing space if you want it.
  • upstream_request_timeout (default 5s) — Timeout for the call to SlashID's /oauth2/tokens endpoint.

To learn more about configuring Gate, please visit the configuration page and the plugins section.

info

Order of plugins in configuration determines their execution order.

Disabling plugin for specific URLs

You can enable or disable this plugin for specific URLs by using the enabled option in the URLs configuration.

GATE_URLS_0_PATTERN=svc-example.com/*
GATE_URLS_0_TARGET=http://example:8080

GATE_URLS_1_PATTERN=svc-another-example.com/
GATE_URLS_1_TARGET=https://another-example:8080

Output

On success, Gate replaces the configured output header (default Authorization) on the proxied request with <output_prefix><access_token> — so by default the upstream sees Authorization: Bearer <delegated token>.

The delegated token's payload contains the RFC 8693 act claim with the actor's identifier (the SlashID person id, OAuth2 client id, or SPIFFE ID, depending on mode), preserving the on-behalf-of relationship for audit and downstream authorization.

Error mapping

ConditionGate response
Missing subject token (configured header/cookie empty)401 Unauthorized
actor_mode: request_actor and the inbound actor header/cookie is empty401 Unauthorized
actor_mode: gate_svid or oauth_client and Gate can't materialise the actor (broken SVID source, client_credentials mint rejected, SPIRE agent unreachable, …)502 Bad Gateway
SlashID /oauth2/tokens returns 4xx/5xx, or its response is malformed502 Bad Gateway