Delegation
Delegation requires spirl-server 0.38.0 or later.
Delegation lets a workload exchange a user's identity token for a JWT-SVID, a JWT form of a SPIFFE Verifiable Identity Document (SVID), that represents the workload acting on behalf of that user (a delegated JWT-SVID). The issued credential carries both identities, the acting workload and the user, in a single token, so a downstream service can see who is calling and on whose behalf.
When to use it
Use delegation when a workload makes a request to a downstream service with the permissions of a user. A common case is an agent or application that takes an action a user initiated and needs the downstream service to apply that user's permissions, while still recording which workload performed the call.
How it works
Delegation uses an RFC 8693 token exchange on the Trust Domain Server's /oauth/token endpoint, the same endpoint the Defakto Agent connects to and that serverless workloads use for issuance. No new endpoint or infrastructure is required. Delegation shares that endpoint with the other token-exchange capabilities described in this section.
The workload calls this endpoint directly rather than going through the Agent. If your network does not currently allow workloads to reach the Trust Domain Server's agent endpoint, enable that traffic before using delegation.
The calling workload makes the exchange request with two inputs. The workload authenticates as itself with its own SVID (see Authenticating the calling workload for the two ways it can do this), and it presents the user's identity token (an OIDC ID token from the user's identity provider) as the subject token. The Trust Domain Server validates the subject token against the trust domain's allowlist (described under Configuration below) and, when the request is allowed, issues a delegated JWT-SVID for the requested audience.
Authenticating the calling workload
Every exchange authenticates the calling workload as itself, so the Trust Domain Server knows which workload is acting and records it on the issued credential and in the audit log. The workload authenticates with an SVID of this trust domain, presented in one of two ways. A request uses a single method.
JWT-SVID client assertion
The workload presents its JWT-SVID as an OAuth client_assertion, with client_assertion_type set to urn:ietf:params:oauth:client-assertion-type:jwt-spiffe. The assertion's audience is the Trust Domain Server's own issuer URL, which binds the assertion to the server it is presented to. The client-assertion method works over any TLS connection to the endpoint and does not require mutual TLS. It is most suitable when the connection terminates somewhere other than the server itself, for example a load balancer in front of a cloud-hosted deployment.
X.509 SVID over mutual TLS
The workload presents its X.509 SVID as the TLS client certificate on the connection. The Trust Domain Server verifies the certificate chain against the trust domain's X.509 bundle, checks its validity, and takes the workload identity from the certificate's SPIFFE ID. The request must also carry the client_id form parameter set to the workload's own SPIFFE ID, which RFC 8705 requires for mutual-TLS client authentication. A request without client_id, or with a value that does not match the certificate's SPIFFE ID, is rejected with invalid_client. The X.509 method requires the TLS session to terminate at the Trust Domain Server itself, which is how self-hosted deployments serve the agent endpoint when TLS is enabled, and is not available where TLS terminates earlier, such as behind a load balancer that does not forward the client certificate.
Choosing a method
When a client assertion is present, it is the credential that counts, and any certificate on the connection is ignored. The X.509 method applies only when no client assertion is present, so a workload whose HTTP client always presents a certificate can still authenticate with an assertion. Either way, the presented SVID must belong to this trust domain, and a credential from another trust domain is rejected. The method used is recorded on the token_exchange audit event, so you can review how each caller authenticated.
The delegated credential
A delegated credential is an ordinary JWT-SVID with the addition of a nested obo claim (the protocol identifier for delegation) that names the user and the acting workload. The outer token is signed with the trust domain's existing JWT-SVID signing key, so it validates with the same SPIFFE tooling and JWKS as any other JWT-SVID. The delegated credential's lifetime is bounded by the user token it was minted from, so it never outlives the user identity evidence behind it.
The obo claim carries the user (obo.sub), a sub_profile marking the subject as a user, the user's identity provider (obo.iss), and the acting workload (obo.act, whose sub is the workload's SPIFFE ID). The act object names the workload currently acting for the user. When a credential is re-exchanged (described below), the new caller becomes the outermost act and the presented credential's actors nest beneath it under obo.act.act, following RFC 8693 section 4.1, so the credential itself carries every workload that has carried the user's authority, most recent first. The chain is capped at 10 actors, and a re-exchange that would exceed the cap is denied rather than trimming the chain.
Every delegated credential also carries a txn claim, a transaction identifier as defined by RFC 8417. The Trust Domain Server creates an opaque value at the first exchange and carries it unchanged through every re-exchange, and never takes it from an external subject token, so all exchanges of one delegation share a single identifier. Downstream services receive the txn in the token, and the audit trail records it on every exchange of the chain, denials included, so one value correlates the whole delegation across services and logs.
A JWT-SVID from the trust domain is one of two shapes. When it has an obo claim it is a delegated token, and the sub of the outer envelope is the acting workload. When it has no obo claim it is an autonomous JWT-SVID that represents the workload's own identity, exactly as before.
Because the acting workload is always named in the credential, this is delegation in the RFC 8693 sense, never impersonation, because a downstream service can always see both who initiated the request and which workload is carrying it.
Configuration
Delegation denies every request until you configure an allowlist of trusted user identity providers. The allowlist is a TokenExchangePolicy section on the trust domain configuration. It lists each accepted issuer and the audiences its tokens may carry.
section: TokenExchangePolicy
schema: v1
spec:
allowlist:
- issuer: https://idp.example.com
audiences:
- my-app
Apply the policy to the trust domain:
spirlctl config set trust-domain --id <trust-domain-id> token-exchange-policy.yaml
A subject token is accepted only when its issuer and one of its audiences match an allowlist entry, both by exact string comparison. A token from an issuer that is not listed is rejected before its keys are fetched or its signature validated.
Additional claims
The policy can add claims to the delegated tokens it allows. Each entry under additionalClaims maps a claim name to a single reference of the form {{jwt.claim.<name>}}, rendered from the verified subject token's claims:
section: TokenExchangePolicy
schema: v1
spec:
allowlist:
- issuer: https://idp.example.com
audiences:
- my-app
additionalClaims:
email: "{{jwt.claim.email}}"
department: "{{jwt.claim.department}}"
A delegated token issued under this policy carries email and department claims whose values come from the user's identity token, so attributes the identity provider asserted travel with the delegation and the receiving service does not need to call the identity provider to learn them.
The reference must name a subject-token claim (the jwt.claim. prefix, written exactly as shown with no whitespace inside the braces), and only string-valued claims can be referenced. Added claims cannot override the claims the Trust Domain Server itself sets. The registered JWT claims (iss, sub, aud, exp, nbf, iat, jti) plus obo, txn, auth_time, and nonce are rejected as added claim names when the policy is applied. The restriction is on the names being added. A template may still read a reserved claim from the subject token, including registered ones such as {{jwt.claim.iss}}.
The added claim name and the referenced subject-token claim follow different rules. An added name must match [a-zA-Z_][a-zA-Z0-9_]*, so a name containing dots, slashes, or hyphens is rejected when the policy is applied. A referenced claim may contain letters, digits, underscores, and hyphens, but not dots or slashes, so a namespaced claim such as dept.example.com/name cannot be referenced.
An exchange whose subject token does not carry a referenced claim is rejected with invalid_request naming the missing claims, rather than issuing a token without them. The denial is recorded in the activity feed as "Missing subject claim", so a policy that expects claims the identity provider does not send is visible immediately.
Re-exchanging a delegated credential
A workload that received a delegated credential can exchange it for a new delegated credential targeting a different audience, so a call chain can carry the user across services that each expect their own audience. The workload presents the credential it holds as the subject token, using urn:defakto:security:token-type:jwt-svid-obo as the subject_token_type (the same token type the exchange issues), and authenticates as itself the same way as for a first exchange.
The Trust Domain Server validates a re-exchanged subject token against the trust domain's own signing keys rather than the issuer allowlist. The allowlist governs which external identity providers may assert users, a question that does not arise for credentials the trust domain issued, so no allowlist entry is needed for re-exchange. Re-exchange is available whenever a TokenExchangePolicy is configured, the same switch that enables first exchanges.
The new credential carries the user through unchanged. That is, obo.sub, obo.iss, and the txn transaction identifier are copied from the input, and any additional claims configured in the policy are carried forward with the values rendered at the first exchange. The new credential names the new caller as the outermost actor, with the presented credential's actors nested beneath it. The new credential never outlives the input credential, so every credential in a chain of re-exchanges expires by the time the original user-identity evidence does.
A re-exchange that would grow the chain beyond 10 actors is denied with invalid_request, and the denial is recorded in the activity feed as "Delegation chain too deep" together with the chain the credential presented.
A delegated credential is bearer evidence within its lifetime. Any workload in the trust domain that holds it can re-exchange it, just as any workload can present a user's ID token for a first exchange.
Every re-exchange is recorded in the audit log with the caller, the user, and both audiences. The console's Token Exchanges view shows each exchange in detail, for denied exchanges as well as successful ones. The previous hops of the call chain are shown oldest first, whether the subject was a user's identity token or an upstream delegated credential, and the transaction ID when the exchange carried one. A denied re-exchange shows the transaction ID of the credential it presented; a denied first exchange has none, since the identifier had not been minted yet.
Because the carried-forward values come from the presented credential rather than being rendered again, a claim the policy names but the presented credential does not carry fails the re-exchange with the same invalid_request rejection as a missing claim on a first exchange. Adding an entry to additionalClaims therefore leaves credentials issued before the change unable to re-exchange until they expire, so make the change expecting in-flight chains to restart.
An autonomous JWT-SVID (one without an obo claim) cannot be re-exchanged; there is no user to carry through, and a workload that needs a credential for itself uses ordinary issuance.
Recipient processing
A service that receives a delegated token validates it the same way it validates any JWT-SVID. The service verifies the signature against the trust domain's JWKS, checks the standard sub, aud, and exp claims, and then reads the obo claim to learn who the call is on behalf of.
When the obo claim is absent the token is the workload's own identity, and the receiver treats it as a normal JWT-SVID. When the claim is present the receiver reads obo.sub and obo.iss as the user and the user's identity provider, and obo.act as the acting workload.
The outermost act entry is the current actor, and its sub always equals the outer envelope's sub. This is a structural rule of these credentials: it tells a receiver which workload the credential was issued to act for the user. Like any bearer token, it does not prove which workload transmitted the request. Any nested obo.act.act entries are the previous hops of the call chain, most recent first, so a receiver that cares about the full delegation path can walk the chain without consulting anything beyond the token. As defense in depth, a receiver that knows which user identity providers it expects can also check obo.iss against that set.