Skip to content

Credex Exchange Policies

Credex token exchanges are mediated by exchange policies; allowing fine-grained control over which exchanges are permitted and which claims the issued JWTs may contain.

This guide covers how to author and manage those policies. For the exchanges these policies govern, see Exchange Types.

An exchange policy specifies who can exchange what, on whose behalf, and which scopes can be granted in the resulting access token. If no policy matches an exchange request, the exchange is denied. If a policy source contains zero policies, every exchange is denied.

The Cofide Connect platform provides a centralized source for policy configuration and auditing. Policies are managed by interacting with the Connect API using either the cofidectl CLI, the Terraform provider, or by interacting directly with the Connect gRPC API.

The remainder of this guide will use cofidectl commands to manage exchange policies.

A Credex RFC 8693 token exchange policy is scoped to a trust zone and contains the following fields, which include both matchers for the exchange request and settings for the outbound token:

Policy FieldField TypeUsage
nameStringName for the policy
trust_zone_idStringTrust zone the policy applies to; immutable after creation
subject_identityList of string matchersMatches the sub claim of the subject token
subject_issuerList of string matchersMatches the iss claim of the subject token
subject_audienceList of string matchersMatches the aud claim of the subject token (required for ID tokens)
actor_identityList of string matchersMatches the sub / SPIFFE ID of the actor token (delegation flows only)
actor_issuerList of string matchersMatches the iss claim of the actor token
client_idList of string matchersMatches the identity used in the client_assertion (e.g. the calling workload’s SPIFFE ID)
target_audienceList of string matchersMatches the audience parameter (if provided by the client)
outbound_identityStringOverrides the sub claim of the issued token
outbound_scopesList of stringsCaps the scopes granted on the issued token
outbound_issuer.oauth_asOutbound OAuth configRoutes the exchange to a downstream OAuth authorization server instead of minting a token locally (allow policies only); see OAuth Bridge exchange
outbound_issuer.spiffeOIDC to SPIFFE configMarks the policy as an OIDC to SPIFFE exchange
external_hooksList of external hooksCalled in order to enrich the outbound token’s claims (allow policies only); see External hooks
actionStringallow or deny

A string matcher is either an exact string equality check or a glob pattern. A list of string matchers field matches if any string matcher in the list matches.

Every field of type list of string matchers except actor_identity, actor_issuer, and subject_audience must have at least one matcher. For required fields, an absent or empty field never matches, and is not a wildcard. To leave a field unconstrained, set it to a single glob:"*" matcher.

Each policy also carries read-only fields set by the server: id, org_id, created_at, and last_updated_at. See ExchangePolicy in the Connect API reference for the full message.

All policies are evaluated against every request, not just the first match. Resolution proceeds as follows:

  1. If any matching policy has action: deny, the exchange is denied immediately.
  2. If any matching allow policy allows the requested scopes, the exchange is permitted.
  3. Otherwise, the exchange is denied.

Deny policies are useful for revoking access for a specific identity without rewriting or reordering the broader allow set.

The two actor fields (actor_identity and actor_issuer) are the only fields where absence is meaningful:

  • If both are absent or empty, the policy only matches exchanges without an actor token (impersonation).
  • If either is present, the policy only matches exchanges with a valid actor token (delegation).

This makes it possible to write distinct policies for the impersonation and delegation cases on the same subject identity.

For OAuth token exchanges, the sub claim of the issued token matches the sub claim of the subject token. The optional outbound_identity field can be used to override the sub claim of the issued token. This can be useful when a downstream resource server or authorization server expects a different subject than the one in the subject token.

For OIDC to SPIFFE exchanges, the outbound_identity field is required and must be set to a valid SPIFFE ID in the same SPIFFE trust domain as the target Credex instance. This ID will be the sub claim in the issued JWT-SVID.

If outbound_identity is non-empty, the subject_issuer field must use exact string matchers only - no glob matchers are allowed. A glob matcher on subject_issuer would let a token from any matching issuer acquire the outbound identity, which is a greater source of potential privilege escalation than matching many workloads from a single already-trusted issuer.

The subject_identity field may still use a glob matcher when outbound_identity is set, so many distinct principals (for example, every SPIFFE ID under a common path prefix) can be mapped onto the same outbound identity. This is useful when a downstream resource server only needs to allowlist one identity for a whole class of callers. Because every principal matched by the glob acquires that same outbound identity, scope subject_identity as tightly as the use case allows to avoid granting the outbound identity to more callers than intended.

If several policies match a token exchange request, they must all resolve to the same outbound_identity value, or an error will be returned. This ensures a predictable mapping of identities.

Impersonation

Allow any workload in the payments namespace to exchange a JWT-SVID for an access token targeting the payments API, with payments:read scope:

Terminal window
cofidectl exchange-policy add \
--name policy-1 \
--trust-zone example \
--subject-identity glob:"spiffe://example.org/cluster/example-cluster/ns/payments/sa/*" \
--subject-issuer glob:"*" \
--client-id glob:"spiffe://example.org/cluster/example-cluster/ns/payments/sa/*" \
--target-audience "https://payments.example.com" \
--outbound-scope "payments:read" \
--action allow

Delegation

Allow an agent workload to act on behalf of any user authenticated against an external IdP (a delegation flow):

Terminal window
cofidectl exchange-policy add \
--name policy-2 \
--trust-zone example \
--subject-identity glob:"*" \
--subject-issuer "https://login.example.com" \
--actor-identity "spiffe://example.org/ns/agents/sa/booking-agent" \
--actor-issuer glob:"*" \
--client-id "spiffe://example.org/ns/agents/sa/booking-agent" \
--target-audience "https://travel-api.example.com" \
--outbound-scope "bookings:write" \
--action allow

The corresponding exchange request for a delegated access token which is allowed by this policy would be:

POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-spiffe
&client_assertion=<JWT-SVID for booking-agent, aud=https://credex.example.com/token>
&subject_token=<external JWT from login.example.com>
&subject_token_type=urn:ietf:params:oauth:token-type:jwt
&actor_token=<JWT-SVID for booking-agent, aud=https://credex.example.com/token>
&actor_token_type=urn:ietf:params:oauth:token-type:jwt_spiffe
&audience=https://travel-api.example.com
&scope=bookings:write

with a subject JWT containing claims such as:

{
"iss": "https://login.example.com",
"sub": "user-12345",
"aud": "https://credex.example.com/token",
"exp": 1796000600,
"iat": 1796000000,
"email": "[email protected]",
"name": "Alice Example"
}

and actor JWT-SVID claims such as:

{
"sub": "spiffe://example.org/ns/agents/sa/booking-agent",
"aud": ["https://credex.example.com/token"],
"exp": 1796000300,
"iat": 1796000000
}

The resulting token issued by Credex would include the following claims structure:

{
"iss": "https://credex.example.com",
"sub": "user-12345",
"act": {
"sub": "spiffe://example.org/ns/agents/sa/booking-agent"
},
"aud": "https://travel-api.example.com",
"exp": 1796000700,
"iat": 1796000100
}

When this Credex-issued token is later sent to the service at https://travel-api.example.com, both the request subject (i.e. user-12345) and the request actor (i.e. the booking-agent) can be used for authorization decisions and clear audit trails.

An allow policy for an OAuth token exchange may optionally specify one or more external hooks. When Credex matches a policy with hooks, it calls each hook endpoint in sequence after evaluating the policy and before minting the token. Each hook can add, remove, or replace custom claims on the outbound token using a JSON Patch (RFC 6902), making it possible to enrich tokens with metadata that Credex does not hold directly.

For each hook, Credex sends a POST request with Content-Type: application/json and a body containing the following fields:

FieldRequiredDescription
subject_claimsYesClaims from the subject token being exchanged
actor_claimsNoClaims from the actor token, present only in delegation exchanges
outbound_claimsYesThe full set of claims that will appear in the issued token, including sub, aud, and scope

The hook must return HTTP 200 with a body containing:

FieldRequiredDescription
actionNoMust be either allow or deny (default: allow). Denial rejects the token exchange and prevents any subsequent hooks from running for the request.
action_reasonNoOptional message to complement action. Action reason is returned as a user-facing error message when action is deny.
claims_patchNoA JSON Patch (RFC 6902) array to apply to the outbound token’s claims

The following claims are protected and any attempt by a hook to modify them will result in an error and the exchange being rejected: iss, sub, aud, exp, nbf, iat, jti, scope, client_id, act.

If a hook returns a non-200 status code, exceeds its configured timeout (default: 3 seconds), or returns a body that does not conform to the expected schema, Credex rejects the exchange. Credex retries the hook once on a transient network error before failing.

Hooks can be configured using the Terraform provider or the cofidectl CLI. Each hook accepts the following fields:

Required fields:

FieldDescription
nameIdentifier for the hook, used in error messages
urlHTTPS endpoint URL of the hook server
authAuthentication configuration; exactly one variant must be set (see below)

Optional fields:

FieldDescription
descriptionHuman-readable description of the hook’s purpose
timeoutPer-request timeout; seconds in Terraform, a Go duration string (e.g. 5s) in cofidectl (default: 3 seconds)

The only supported auth variant is spiffe_mtls, which requires the SPIFFE ID of the hook server. Credex presents its own X.509-SVID and verifies that the hook server presents a certificate matching that SPIFFE ID. The hook server must be a SPIFFE workload reachable by Credex.

The following Terraform resource defines the impersonation policy above extended with a hook that adds the workload’s team and cost centre to the issued token.

resource "cofide_connect_exchange_policy" "policy_with_hook" {
name = "policy-with-hook"
trust_zone_id = cofide_connect_trust_zone.example.id
action = "ALLOW"
subject_identity = [{ glob = "spiffe://example.org/cluster/example-cluster/ns/payments/sa/*" }]
subject_issuer = [{ glob = "*" }]
client_id = [{ glob = "spiffe://example.org/cluster/example-cluster/ns/payments/sa/*" }]
target_audience = [{ exact = "https://payments.example.com" }]
outbound_scopes = ["payments:read"]
external_hooks = [
{
name = "workload-directory"
url = "https://directory.internal/hooks/claims"
timeout = 5
auth = {
spiffe_mtls = {
spiffe_id = "spiffe://example.org/ns/directory/sa/claims-hook"
}
}
}
]
}

When this policy matches an exchange request, Credex calls the hook with:

{
"subject_claims": {
"sub": "spiffe://example.org/cluster/example-cluster/ns/payments/sa/checkout",
"aud": ["https://credex.example.com/token"],
"exp": 1796000300,
"iat": 1796000000
},
"outbound_claims": {
"sub": "spiffe://example.org/cluster/example-cluster/ns/payments/sa/checkout",
"aud": "https://payments.example.com",
"scope": "payments:read",
"client_id": "spiffe://example.org/cluster/example-cluster/ns/payments/sa/checkout"
}
}

The hook looks up the workload by subject_claims.sub and responds with:

{
"claims_patch": [
{ "op": "add", "path": "/department", "value": "engineering" },
{ "op": "add", "path": "/cost_centre", "value": "CC-42" }
]
}

The resulting token issued by Credex includes the enriched claims:

{
"iss": "https://credex.example.com",
"sub": "spiffe://example.org/cluster/example-cluster/ns/payments/sa/checkout",
"aud": "https://payments.example.com",
"scope": "payments:read",
"department": "engineering",
"cost_centre": "CC-42",
"exp": 1796000700,
"iat": 1796000100
}

The credex-policy-hook-reference repository contains an example hook demonstrating the request and response schema required by Credex as well as the SPIFFE mTLS configuration to enable secure communication between Credex and the external hook.

By default, a matching allow policy causes Credex to mint the issued token itself. Setting the policy’s oauth_as field changes this: instead of minting a token, Credex obtains one from a downstream OAuth authorization server and returns it to the caller verbatim.

For the use case this exchange addresses, see Federating to a legacy OAuth authorization server on the Use Cases page. For the caller’s side of this exchange and how Credex authenticates to the downstream server, see OAuth Bridge exchange on the Exchange Types page.

The client assertion’s sub identifies the client in the downstream authorization server, and is equal to the resolved outbound identity for the exchange. The caller does not need to hold a SPIFFE SVID: a policy can match an external JWT or OIDC ID token from a trusted issuer just as it can match a JWT-SVID, and the bridge behaves the same way in both cases.

The oauth_as message accepts the following fields (see OutboundOAuthAS in the Connect API reference for the full schema):

FieldRequiredDescription
grant_typeYesMust be client_credentials, the only grant type supported for outbound exchanges.
issuer_urlIf no token_urlIssuer URL of the downstream authorization server. The token endpoint is discovered via RFC 8414 / OIDC discovery and cached for 24 hours.
token_urlIf no issuer_urlToken endpoint URL of the downstream authorization server. Bypasses discovery.
audiencesNoAudience values included in the JWT client assertion. Defaults to the token endpoint URL.
timeoutNoTimeout for the outbound call to the downstream authorization server. Defaults to 10 seconds.

At least one of issuer_url or token_url must be set. If both are set, token_url is used and issuer_url is ignored.

  • oauth_as is only valid on allow policies.
  • oauth_as cannot be combined with external_hooks on the same policy.

If several allow policies match a request, they must all agree on oauth_as. If some matching policies set it and others do not, or matching policies set structurally different values, the exchange is rejected as ambiguous.

Allow a workload in the prod namespace to exchange its JWT-SVID for a token minted by a downstream authorization server, targeting downstream-service with read scope:

Terminal window
cofidectl exchange-policy add \
--name policy-bridge \
--trust-zone example \
--subject-identity "spiffe://example.org/cluster/example-cluster/ns/prod/sa/my-service" \
--subject-issuer glob:"*" \
--client-id "spiffe://example.org/cluster/example-cluster/ns/prod/sa/my-service" \
--target-audience "downstream-service" \
--outbound-scope "read" \
--outbound-oauth-as '{"grant_type":"client_credentials","issuer_url":"https://downstream-as.example.com","audiences":["https://downstream-as.example.com/token"]}' \
--action allow

The --outbound-oauth-as flag sets the policy’s oauth_as field. It accepts the configuration as inline JSON, or as file://<path> to a JSON file with the same fields.

When a request matches this policy, Credex performs a client_credentials exchange against https://downstream-as.example.com, authenticating with a signed JWT assertion whose sub is the workload’s SPIFFE ID, and returns the downstream token to the caller. For a caller that is not SPIFFE-attested, set --subject-identity and --subject-issuer to match the external token’s sub and iss instead, and the assertion’s sub will be that subject identity. The corresponding exchange request is shown under OAuth Bridge exchange on the Exchange Types page.

For OIDC to SPIFFE exchanges, the issued tokens are signed by the SPIRE server rather than Credex. These exchange policies are specified by setting the spiffe field on the policy.

Inbound token exchange requests with requested_token_type=urn:ietf:params:oauth:token-type:jwt_spiffe are considered OIDC to SPIFFE exchanges; therefore, policy evaluation is restricted to policies which contain the spiffe field.

All OIDC to SPIFFE exchange policies must:

  • use allow as the policy action
  • have no external_hooks defined
  • have outbound_identity set to a valid SPIFFE ID within Credex’s trust domain
  • use exact matchers for subject_issuer and subject_identity (see outbound identity)

and each exchange request must satisfy the additional constraints described in Exchange Types.

An OIDC to SPIFFE exchange policy can be created using:

Terminal window
cofidectl exchange-policy add \
--name aws-workload-to-spiffe \
--trust-zone example \
--outbound-spiffe \
--client-id arn:aws:sts::012345678910:assumed-role/MyRole/session \
--subject-identity arn:aws:sts::012345678910:assumed-role/MyRole/session \
--subject-issuer https://sts.amazonaws.com \
--subject-audience https://credex.example.com \
--outbound-identity spiffe://example.com/oidc/aws/my-workload \
--target-audience https://api.example.com

The --outbound-spiffe flag sets the policy’s spiffe field.

Assuming https://sts.amazonaws.com is configured as a trusted issuer, this policy would permit exchange of an AWS token with the appropriate subject identity and audience for a JWT-SVID with SPIFFE ID spiffe://example.com/oidc/aws/my-workload and audience https://api.example.com.

The corresponding exchange request would be

POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:jwt_spiffe
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<AWS token>
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&subject_token=<AWS token>
&audience=https://api.example.com