Module 17: Multi-issuer identity (trust more than one IdP)#
You are in the CPEX tutorial. This module needs the IdP.
Goal: accept callers from more than one identity provider with a single resolver, each token validated against its own issuer’s keys.
The problem#
A single enforcement point often fronts several identity providers: your own workforce IdP and a partner org’s, a legacy realm and its replacement during a migration, one IdP per business unit. You want to accept tokens from all of them, only those, and validate each on the correct keys.
A JWT names its issuer in the iss claim. CPEX matches that to a trusted issuer and validates the token against that issuer’s JWKS. List several, and one resolver federates them; a token whose iss is in none is rejected with auth.untrusted_issuer.
Build it#
One resolver, two trusted_issuers. From policies/m17.yaml:
plugins:
- name: keycloak
kind: identity/jwt
config:
role: user
claim_mapper: standard
trusted_issuers:
- issuer: http://localhost:8081/realms/cpex-tutorial # home realm
audiences: [cpex-tutorial]
algorithms: [RS256]
decoding_key: { kind: jwks_url, url: "…/cpex-tutorial/…/certs" }
- issuer: http://localhost:8081/realms/cpex-partner # partner realm
audiences: [cpex-tutorial]
algorithms: [RS256]
decoding_key: { kind: jwks_url, url: "…/cpex-partner/…/certs" } # ITS OWN keysEach entry is a full trust anchor: its own issuer string, its own JWKS, its own accepted audiences. Here both issuers are realms in the same Keycloak, but they could be entirely separate products. The resolver only matches iss.
Run it#
The tutorial IdP imports a second realm, cpex-partner, with one user (pat).
cargo run -p cpex-tutorial --example m17_federation▸ alice (home realm cpex-tutorial) → get_compensation (issuer #1, role.hr)
✓ ALLOWED { ... }
▸ pat (partner realm cpex-partner) → get_compensation (issuer #2, validated on ITS keys)
✓ ALLOWED { ... }
▸ outsider (master realm, an untrusted issuer) → get_compensation (rejected)
✗ DENIED [auth.untrusted_issuer] issuer 'http://localhost:8081/realms/master' is not in the trusted-issuer listBoth alice and pat reach the same route under the same rule (require(role.hr)), and the policy never mentions issuers. The third token is a genuine, validly-signed JWT from Keycloak’s master realm, which the resolver doesn’t trust, so it is rejected before any authorization runs.
Claims still have to line up#
Federation validates signatures; it does not normalize claims. Two IdPs may express roles differently, and the resolver reads a flat roles array (module 2). The partner realm here is configured to emit the same roles and permissions shape as the home realm, which is why pat satisfies require(role.hr). Against a real partner IdP, mapping its claims into the shape your policy expects is the harder half; the trust list is the easy one.
Try it#
- Drop the partner issuer. Delete the second
trusted_issuersentry and re-run. Expect:patnow fails withauth.untrusted_issuer, the same token, no longer trusted. - Break the partner keys. Point the partner entry’s
decoding_key.urlat the home realm’s certs. Expect:patfails signature validation, because each issuer must be verified on its own keys. - Diverge the claims. Give
pata role the home realm doesn’t use and gate the route on it. That is the per-issuer claim-mapping problem, in one realm.
Checkpoint#
How does CPEX pick which keys to validate a token with?
Is a trusted issuer the same as trusting everything it says?
Go deeper#
- Identity → Multiple sources for the full multi-issuer / multi-resolver model.
- Configuration for the
trusted_issuersschema.
Next#
Module 18: Static attributes: feed policy operator-maintained facts from a data file, read as data.*.