You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/content/docs/tutorial/06-delegation.md
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,6 +6,8 @@ weight: 7
6
6
# Module 6: Scoped credentials (Delegation)
7
7
8
8
> You are in the [CPEX tutorial]({{< relref "_index" >}}). This module needs the IdP.
9
+
>
10
+
> **Cookbook recipe:**[Recipe 1: User acting through an agent (on-behalf-of)]({{< relref "/docs/identity-delegation#recipe-1-user-acting-through-an-agent-on-behalf-of" >}}).
9
11
10
12
**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.
11
13
@@ -86,6 +88,7 @@ Keycloak mints the scoped token during the exchange, constrained by the requeste
86
88
## Go deeper
87
89
88
90
- [Delegation]({{< relref "/docs/apl/delegation" >}}) for token exchange, capability reduction, and downstream verification.
91
+
- [Module 12: Delegation subjects]({{< relref "12-subjects" >}}) for the other principals a minted token can speak for.
> You are in the [CPEX tutorial]({{< relref "_index" >}}). This module needs the IdP.
9
+
10
+
**Goal:** factor the setup every route shares, here the identity resolver, into one reusable group that routes join instead of repeating.
11
+
12
+
## The problem
13
+
14
+
By now you have written `authentication: [keycloak]` on route after route. The resolver is the same everywhere; only the per-route authorization differs. That repetition is a maintenance hazard: add a second issuer or a claim mapper later, and you have to change every route and hope you caught them all.
15
+
16
+
A group is a named, reusable bundle of policy (authentication steps, authorization steps, plugins) that routes opt into. Put the shared part in a group once; each route joins it and adds only what is specific to it.
17
+
18
+
## Build it
19
+
20
+
Define a top-level `groups:` section and join it from each route with `groups:`. From [`policies/m11.yaml`](https://github.com/contextforge-org/cpex/tree/main/examples/tutorial/policies/m11.yaml):
21
+
22
+
```yaml
23
+
plugins:
24
+
- name: keycloak
25
+
kind: identity/jwt
26
+
hooks: [identity.resolve]
27
+
config: { ... as in module 2 ... }
28
+
29
+
# One reusable bundle: the resolver every route needs, written once.
30
+
groups:
31
+
identified:
32
+
authentication:
33
+
- keycloak
34
+
35
+
routes:
36
+
- tool: get_compensation
37
+
groups: identified # inherits keycloak; no authentication: line
38
+
authorization:
39
+
pre_invocation:
40
+
- "require(role.hr)"
41
+
- tool: search_repos
42
+
groups: identified
43
+
authorization:
44
+
pre_invocation:
45
+
- "require(role.engineer)"
46
+
- tool: send_email
47
+
groups: identified # still resolves the token; no role required
48
+
authorization:
49
+
pre_invocation:
50
+
- "require(authenticated)"
51
+
```
52
+
53
+
The `identified` group carries the one thing all three routes share. Each route joins it and adds only its own authorization, so identity is resolved the same way everywhere while policy still decides each outcome per caller.
54
+
55
+
- **`groups:` is sugar over tags.** `groups: identified` is exactly `meta: { tags: [identified] }`. The field just makes that membership a first-class, discoverable spelling, and a runtime tag a host injects joins the group the same way.
56
+
- **An unknown group is a load error.** Join a group that isn't defined, a typo like `groups: identifed`, and the config is rejected at load, so a mistake can't silently leave a route unauthenticated.
57
+
58
+
## Run it
59
+
60
+
```bash
61
+
cargo run -p cpex-tutorial --example m11_groups
62
+
```
63
+
64
+
```
65
+
▸ alice (hr) → get_compensation (group resolves her token, require(role.hr) passes)
66
+
✓ ALLOWED { ... }
67
+
68
+
▸ evan (engineer) → get_compensation (resolved by the same group, denied at require(role.hr))
▸ alice (hr) → search_repos (denied at require(role.engineer))
75
+
✗ DENIED [...] access denied
76
+
77
+
▸ alice (hr) → send_email (group resolves her token; require(authenticated) passes)
78
+
✓ ALLOWED { ... }
79
+
```
80
+
81
+
Every route resolved the caller's token through the same group, yet each outcome is decided by that route's own authorization. The resolver appears once, not three times.
82
+
83
+
## Try it
84
+
85
+
1. **Break a join.** Change one route's `groups:` to a name that doesn't exist (`groups: identifed`) and re-run. Expect: the config is rejected at load with an unknown-group error, so the typo fails loudly instead of silently dropping authentication.
86
+
2. **Change the resolver once.** Add `leeway_seconds: 5` (or a second entry under `trusted_issuers:`) to the `keycloak` plugin. Every route that joins `identified` picks it up: you edited one place, not three.
87
+
3. **Tags are the same thing.** Replace `groups: identified` on a route with `meta: { tags: [identified] }` and re-run. Same result, because a group and a matching tag are the same membership.
88
+
89
+
## Checkpoint
90
+
91
+
{{< details "Does the route repeat the group's authentication?" >}}
92
+
No. The route has no `authentication:` block, so it inherits the group's. Identity resolution stacks broad → narrow (global → group → route); a route joining `identified` runs the group's `keycloak` resolver without naming it again.
93
+
{{< /details >}}
94
+
95
+
{{< details "Group or the reserved `all` group?" >}}
96
+
Groups are opt-in: a route joins one by name. The reserved `all` group is the other case, applied to every request unconditionally. Reach for a named group when a *subset* of routes shares setup, as here.
97
+
{{< /details >}}
98
+
99
+
## Go deeper
100
+
101
+
- [Configuration]({{< relref "/docs/configuration" >}}#groups) for the full `groups:` schema, defaults, and how membership resolves.
102
+
103
+
## Next
104
+
105
+
[Module 12: Delegation subjects]({{< relref "12-subjects" >}}): choose whether a minted token speaks for the caller or the gateway itself.
0 commit comments