Identity and IdP Integration

Policy reads attributes: role.hr, perm.view_ssn, subject.id.
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:

SourceAttributes
Subjectsubject.id, authenticated, subject.roles, subject.permissions, subject.teams
Rolesrole.<r> for undotted names (for example role.hr, role.security)
Permissionsperm.<p> for undotted names (for example perm.view_ssn)
Claimsclaim.<k>
OAuth clientclient.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:

roleNamespaceTypical credential
usersubject.*End-user OIDC token on X-User-Token
clientclient.*OAuth client token on Authorization
caller_workloadcaller_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