Identity and IdP Integration
On this page
Policy reads attributes: role.hr, perm.view_ssn, subject.id. Those
attributes must come from a trusted source. Identity resolution runs before
policy and turns a verified credential into the attribute bag that predicates
read.
Identity requirement
The scenario authorizes with require(role.hr) and redacts with
redact(!perm.view_ssn). For those to mean anything, PPE must know, for each
request, who the caller is and what roles and permissions they hold, established
from a token the caller cannot forge, not from anything the LLM said.
Resolving identity
An identity plugin validates an inbound token and populates the subject. The
bundled identity/jwt plugin verifies a JWT against a trusted issuer and maps
its claims into the bag:
plugins:
- name: jwt-user
kind: identity/jwt
hooks: [identity.resolve]
config:
role: user
header: X-User-Token
trusted_issuers:
- issuer: "https://idp.example.com/realms/agents"
audiences: ["ppe-gateway"]
decoding_key:
kind: jwks_url
url: "https://idp.example.com/realms/agents/protocol/openid-connect/certs"
The token is verified against the issuer’s JWKS. Only after verification do its
claims become attributes. An unverified or expired token resolves to no subject,
and require(authenticated) denies.
identity/api-key looks up an opaque key in a file or HTTP directory and maps
its record into the same attributes. Policies such as require(role.hr) work
with either resolver. See Recipe 7
for configuration and revocation timing.
What lands in the bag
A resolved identity populates a flat attribute namespace that predicates read directly:
| Source | Attributes |
|---|---|
| Subject | subject.id, authenticated, subject.roles, subject.permissions, subject.teams |
| Roles | role.<r> for undotted names (for example role.hr, role.security) |
| Permissions | perm.<p> for undotted names (for example perm.view_ssn) |
| Claims | claim.<k> |
| OAuth client | client.client_id, client.authorized_scopes, client.roles, client.permissions, and aliases for undotted members under client.role.<r> / client.perm.<p> |
| Workload (SPIFFE / mTLS) | caller_workload.spiffe_id, caller_workload.trust_domain |
require(role.hr) is true when the verified token carried the hr role, and
redact(!perm.view_ssn) redacts unless it carried the view_ssn permission.
Membership names containing . do not receive aliases because policy engines
can interpret dotted keys as nested namespaces. Names containing : cannot be
written as attribute paths. Use the canonical sets for either case:
subject.roles contains "admin.readonly" or
subject.roles contains "system:authenticated".
Multiple sources
A request often carries more than one identity: the end user, the calling
application, and the calling workload. Register an identity plugin per source.
The bundled JWT plugin takes a role (user, client, or caller_workload)
and a header, so each inbound credential resolves into its own namespace:
role | Namespace | Typical credential |
|---|---|---|
user | subject.* | End-user OIDC token on X-User-Token |
client | client.* | OAuth client token on Authorization |
caller_workload | caller_workload.* | SPIFFE JWT-SVID on X-Workload-Token |
Policy can then require several at once: require(authenticated) & client.authorized_scopes contains "tools:invoke".
Workload identity
A role: caller_workload resolver is the ingress for the calling agent’s
SPIFFE JWT-SVID:
plugins:
- name: jwt-workload
kind: identity/jwt
hooks: [identity.resolve]
config:
role: caller_workload
header: X-Workload-Token
trusted_issuers:
- issuer: "https://spire.example.com"
audiences: ["ppe-gateway"]
decoding_key:
kind: jwks_url
url: "https://spire.example.com/keys"
The SPIFFE ID is read from the SVID’s sub claim, and the trust domain is
derived from it, populating caller_workload.spiffe_id and
caller_workload.trust_domain. A token whose sub is not SPIFFE-shaped is
rejected rather than filed into the workload slot, so anything policy finds
there really is an attested workload.
The bag distinguishes two machine identities:
caller_workload— the attested workload on the inbound network peer. The agent calling us. Many different agents call through one gateway.this_workload— this PPE instance’s own attested identity, used for outbound calls. A single principal.
They are not interchangeable, and confusing them is how a token minted for one agent ends up presented by another. Delegation depends on the distinction — see Delegation.
Pipeline integration
Identity resolution is a hook (identity.resolve) that runs ahead of the
route’s policy phase. The resolved subject is filtered by each downstream
plugin’s declared capabilities (see Extensions &
Capability-Gating): a plugin only sees the identity fields it
is entitled to. APL predicates read the same bag, gated the same way.
Once identity is resolved, policy can authorize (APL), delegate downstream (Delegation), or hand a relationship decision to a PDP (PDP Integration).
Next
- Identity Claim Mapping: add and test a provider-specific JWT claim mapper preset.
- Static Attributes: combine verified identity with operator-maintained facts.
- Delegation: exchange verified credentials for scoped downstream tokens.