Module 6: Scoped credentials (Delegation)#
You are in the CPEX tutorial. This module needs the IdP.
Goal: mint a narrow, downstream-scoped credential for a call with a real OAuth 2.0 token exchange (RFC 8693), instead of forwarding the caller’s full token.
The problem#
When your agent calls a downstream API, handing it the caller’s original token is over-broad: that token works everywhere, for everything the caller can do. You want a token minted for this one downstream call, scoped to a single audience, so a leak is contained. Token exchange does that, and CPEX makes it a policy step rather than integration code.
Build it#
Add a delegator plugin and a delegate(...) step. From policies/m06.yaml:
plugins:
- name: keycloak
kind: identity/jwt
hooks: [identity.resolve]
config: { ... as in module 2 ... }
- name: workday-oauth
kind: delegator/oauth
hooks: [token.delegate]
capabilities: [read_inbound_credentials, write_delegated_tokens]
config:
token_endpoint: http://localhost:8081/realms/cpex-tutorial/protocol/openid-connect/token
client_id: cpex-gateway
client_secret_source: { kind: literal, secret: gateway-dev-secret }
insecure_http: true
routes:
- tool: get_compensation
authentication: [keycloak]
authorization:
pre_invocation:
- "require(role.hr)"
- "delegate(workday-oauth, target: workday-api, audience: workday-api)"
- "require(delegation.granted)"The delegate(...) step exchanges the caller’s token for one scoped to the workday-api audience, against the real Keycloak token endpoint. require(delegation.granted) then proceeds only if the exchange succeeded.
Two things are load-bearing:
- The plugin declares
capabilities: [read_inbound_credentials, write_delegated_tokens]. Without them, the caller’s inbound token is filtered out before the exchange runs, and delegation fails with an empty token. Capabilities scope what each step may touch. - The gateway client in Keycloak must be allowed to exchange for the target audience. In the tutorial realm,
cpex-gatewaysetsstandard.token.exchange.audiences: workday-api,github-apiand carries audience mappers for those clients (seeidp/realm-export.json).
Run it#
cargo run -p cpex-tutorial --example m06_delegation▸ alice (hr) → get_compensation (delegate mints a workday-api token, then allow)
✓ ALLOWED { ... }
▸ evan (engineer) → get_compensation (denied at require(role.hr), no delegation)
✗ DENIED [...] access deniedalice’s call runs a real token exchange and gets a workday-api-scoped token before the backend call. evan never reaches delegation, because require(role.hr) stops him first. Delegation is cheap to skip when it is not needed.
Try it#
- Drop the capabilities. Remove the
capabilities:line from the plugin and re-run. Expect: alice is denied withdelegation.bad_request(empty token), because the inbound credential was filtered out. - Wrong audience. Change
audience:to a client the gateway may not target and re-run. Expect: the exchange is rejected by Keycloak and the step denies. - Narrow the grant (advanced). In
policies/m06.yaml, add apermissions:argument to thedelegate(...)step so it readsdelegate(workday-oauth, target: workday-api, audience: workday-api, permissions: [read_compensation]). The exchange then requests only that scope. For Keycloak to actually issue it, theread_compensationclient scope must exist on the realm and be assigned to theworkday-apiclient (seeidp/README.md); without that realm setup the exchange returns the default scope. This is how you narrow the downstream grant to exactly what the operation needs.
Checkpoint#
Why does the plugin need capabilities to succeed?
Where does the scoping happen, CPEX or Keycloak?
Go deeper#
- Delegation for token exchange, capability reduction, and downstream verification.
Next#
Module 7: Information flow: carry security state across a session to block write-down.