Pular para o conteúdo

Koder ID Token Exchange — RFC 8693 delegation grant

auth specs/auth/token-exchange.kmd

Normative contract for the OAuth 2.0 Token Exchange grant (RFC 8693) on Koder ID. Lets a Koder service (the actor) exchange an end-user access token for a NEW token scoped to a downstream audience, preserving the user's tenant and recording the actor in the `act` claim. This is the canonical way a multi-tenant gateway/proxy calls a second Koder service ON BEHALF OF the authenticated user without holding broad impersonation rights or forwarding the raw user token. Motivating consumer: Koder Kli (products/dev/kli) driving per-tenant sandbox sessions in services/ai/sandbox (agent-console-RFC-001).

Quando esta spec se aplica

Triggers primários

Todos os triggers

Corpo da especificação

Spec — Koder ID Token Exchange (RFC 8693)

Version: 0.1.0 Status: Implemented (2026-06-22, id/engine#246 — POST /oauth/v2/token grant urn:ietf:params:oauth:grant-type:token-exchange)

Motivation

A Koder service that fronts the user (a gateway/proxy/agent driver) often must call a second Koder service as that user — e.g. Koder Kli allocating a services/ai/sandbox session that must land in the end-user's tenant (multi-tenant-by-default), not the calling service's tenant.

Two non-solutions, and why this grant exists:

  • Token forwarding — the actor replays the user's own access token downstream. Works only while the downstream skips audience checks; it is a confused-deputy hazard (a token minted for audience A is presented to audience B) and breaks the moment the downstream enforces aud.
  • Admin impersonation (POST /v1/admin/users/{id}/impersonate) — issues a sub=user / act=admin token, but is gated on the powerful user.impersonate permission and is semantically admin-only. Wrong primitive for a service minting per-request delegated tokens.

Token Exchange is the correct primitive: a scoped, audience-bound, actor-stamped delegation token, with downscoping only (never escalation).

Grant

Koder ID's token endpoint (POST /oauth/v2/token) gains a fifth grant alongside authorization_code / client_credentials / refresh_token / device_code.

  • R1grant_type = urn:ietf:params:oauth:grant-type:token-exchange.
  • R2 — Request params (RFC 8693 §2.1):
    • subject_token (REQUIRED) — the user access token being exchanged.
    • subject_token_type (REQUIRED) — urn:ietf:params:oauth:token-type:access_token.
    • audience (REQUIRED in Koder profile) — the target service identifier (e.g. sandbox). The minted token's aud is set to exactly this.
    • scope (OPTIONAL) — requested downscope; MUST be a subset of the subject token's scope (R7).
    • actor_token / actor_token_type (OPTIONAL) — the actor's own client credential; when absent the authenticated client (mTLS/SVID or client_credentials bearer on the request) is the actor.
  • R3 — Response (RFC 8693 §2.2): access_token, issued_token_type = ...:access_token, token_type = Bearer, expires_in. No refresh token.

Claims of the issued token

  • R4tenant_id is copied verbatim from the subject token. The exchange NEVER changes the tenant (multi-tenancy is preserved, not crossed).
  • R5sub = the subject token's sub (the end-user remains the principal).
  • R6act (RFC 8693 §4.1) = { "sub": "<actor client id>" }, chained if the subject token already carried an act. Audit trails MUST log the actor chain (reuse the impersonation issuer's act machinery).
  • R7 — Scope of the issued token = min(requested_scope, subject_scope). Escalation is impossible: a scope not present in the subject token is dropped, never granted.
  • R8aud = the requested audience (single value in v0). TTL ≤ the subject token's remaining lifetime and ≤ a configured ceiling (default 15 min, matching impersonation).

Authorization policy

  • R9 — Only registered service clients (client-credentials clients, id#115) flagged token_exchange_allowed MAY use this grant. A normal user client cannot exchange.
  • R10 — A client MAY be restricted to a set of allowed downstream audience values; requesting an audience outside the set → invalid_target.
  • R11invalid_request / invalid_grant / invalid_target / insufficient_scope errors per RFC 8693 §2.2.2, mapped to Koder error IDs.

Security constraints

  • R12 — No privilege escalation: the issued token is a strict downscope of the subject token (R7) with an unchanged tenant (R4) and a recorded actor (R6). It cannot be exchanged again for a broader token.
  • R13 — Exchanges are audited unconditionally (actor, subject sub, tenant, audience, granted scope).

Tests (outline)

  • T1 — exchange a user token for audience=sandbox → issued token has same tenant_id+sub, aud=sandbox, act.sub=<client>, scope ⊆ subject.
  • T2 — requesting a scope absent from the subject token → it is dropped (not granted).
  • T3 — a non-service / non-flagged client → unauthorized_client.
  • T4 — requesting an audience outside the client's allowlist → invalid_target.
  • T5 — issued TTL ≤ subject remaining TTL and ≤ ceiling.

Consumers

  • Koder Kli (products/dev/kli, KLI-001/1e-ii): klid exchanges the inbound user token for an audience=sandbox token and presents it to services/ai/sandbox so each session runs in the user's tenant.
  • Any future Koder gateway/proxy that fans a user request into downstream Koder services.

Prior art in-tree

services/foundation/id/engine/services/admin/internal/handler/impersonation_http.go already issues short-lived act-stamped tokens — the token-exchange grant generalizes that issuer to a self-service, scope-downscoping, audience-bound, non-admin path.

Referências