Skip to main content

Propagate a user's identity across services

This guide shows how a workload that acts for a person passes that person's identity to the services it calls, so each service knows both which workload is calling and on whose behalf. Use it when a workload, such as a web application's backend or an AI agent, calls other services for a signed-in user, and those services need to make decisions based on the user.

How it works​

The workload starts with a token for the user, such as the ID token or access token your identity provider issued when the user signed in to your application. Using OAuth token exchange, the workload trades that token for a new one that it can pass to the services it calls. Defakto offers two kinds:

  • On-behalf-of (OBO) tokens. An OBO token is a JWT-SVID for the calling workload, with an added obo claim that names the user. Any service that accepts JWT-SVIDs also accepts OBO tokens, and can find the user in the obo claim. Each service that calls further exchanges the OBO token it received for a new one whose audience is the next service, adding itself to the record of workloads that acted for the user.
  • Transaction Tokens. A Transaction Token, defined in an IETF draft standard, records who started a transaction. A workload at the edge of your system gets one Transaction Token, and every service passes it along unchanged, in addition to the credential that service already uses to authenticate its calls.

Choosing between them​

Use on-behalf-of tokens when each service should see the whole story in one token: who the user is, and every workload that has acted for them. Because an OBO token is a JWT-SVID, services and tools that already accept JWT-SVIDs, such as API gateways and JWT authorizers, receive both identities without any changes. Each hop exchanges the token again, so your token policy can check every hop, and the audit trail shows the full chain of calls.

Alice signs in to your app, and the web backend exchanges her token with the Defakto OAuth server for an on-behalf-of token, which it sends to the orders service. The orders service exchanges that token again, and sends the payments service a new on-behalf-of token that names Alice and both workloads that acted for her. Defakto checks your token policy at every exchange.

For example, suppose a request from a user, Alice, passes through three services: a web backend, then an orders service, then a payments service. Here is the OBO token at the second hop. The obo claim names Alice, and the workloads that acted for her, most recent first (standard claims such as iss and exp are left out for brevity):

{
"sub": "spiffe://example.com/ns/orders/sa/api",
"aud": "spiffe://example.com/payments",
"txn": "b7814530c95811d288ac0390515c61af",
"obo": {
"iss": "https://idp.example.com",
"sub": "alice-8f3k2",
"sub_profile": "user",
"via": "idToken",
"act": {
"iss": "spiffe://example.com",
"sub": "spiffe://example.com/ns/orders/sa/api",
"act": {
"iss": "spiffe://example.com",
"sub": "spiffe://example.com/ns/web/sa/backend"
}
}
}
}

Use Transaction Tokens when the way your services authenticate each other can't carry the user. For example, mutual TLS with X.509-SVIDs identifies the calling workload, but has no place for information about the user. A Transaction Token travels in addition to the existing credential, so services keep authenticating each other the way they do today. Transaction Tokens also suit requests that fan out to many services: You get one token at the edge and pass it along unchanged, instead of exchanging a token at every hop. The trade-off is that a Transaction Token records who started the transaction, not which workloads it passed through.

Alice signs in to your app, and the web backend exchanges her token once with the Defakto OAuth server for a Transaction Token. Every call from the web backend to the orders service, and from the orders service to the payments service, uses each service's own credential, and carries the same Transaction Token in the Txn-Token header.

For the same request, the web backend gets one Transaction Token at the start, and the orders and payments services each receive it unchanged, in addition to the caller's own credential (standard claims are left out for brevity):

{
"sub": "alice-8f3k2",
"aud": "example.com",
"txn": "97053963771d49cfa1bc8a2b4b4fd1ed",
"scope": "orders.write"
}
On-behalf-of tokensTransaction Tokens
What the token recordsThe user, and every workload that acted for themThe user who started the transaction
The user's token it's exchanged fromAn ID token or an access tokenAn access token
How a service receives itAs the calling workload's credentialAs an additional token, sent with each call in addition to the workload's own credential
How often it's exchangedAt every hop, each checked by your token policyOnce, at the edge

To get either kind, a workload authenticates to the OAuth server with its JWT-SVID or its X.509-SVID.

Set up the trust domain​

Your Defakto administrators do this once for the trust domain. After that, the owners of each workload and service follow either Use on-behalf-of tokens or Use Transaction Tokens. If your trust domain is already set up, skip to one of those sections.

Step 1: Add your identity provider​

Workloads exchange the token your application received when the user signed in with your identity provider. Depending on your identity provider, and on what your application keeps from the sign-in, that's an OpenID Connect ID token, an access token, or both. Configure only the kind your workloads will exchange. Transaction Tokens can only be exchanged from access tokens.

Add your identity provider to the trust domain's OAuthServer configuration, creating the configuration if the trust domain doesn't have one yet:

oauth-server.yaml
section: OAuthServer
schema: v1
spec:
endpoints:
publicBaseURL: https://td-server.defakto.example.com

identityProviders:
- name: corp-idp
issuer: https://idp.example.com
# If workloads exchange ID tokens:
idTokens:
audiences: [web-app]
# If workloads exchange access tokens:
accessTokens:
audiences: [https://api.example.com]
  • publicBaseURL is the address where you host your Trust Domain Servers. If Defakto hosts your Trust Domain Servers, leave it out.
  • issuer is your identity provider's issuer URL. Defakto reads the identity provider's discovery document from <issuer>/.well-known/openid-configuration, and verifies each token with the signing keys it lists.
  • idTokens.audiences is the audience of the ID tokens your application receives, usually your application's client ID.
  • accessTokens.audiences is the value of the aud claim in the access tokens your application receives. It depends on your identity provider and on what your application requested when the user signed in, so check a decoded token. Access tokens must be JWTs with an aud claim.

Defakto accepts a token only when its audience is in the list, so a token your identity provider issued for some other application can't be exchanged.

Depending on your identity provider, you might need additional token validation rules. For example, with Google as your identity provider, Defakto requires a rule that accepts only accounts from your company's Google Workspace domain. Otherwise, Defakto would accept tokens for anyone with a Google account who signs in to your application. You can also add rules of your own, such as requiring a verified email address. For details, see Issuer-specific validation rules.

Apply the configuration to your trust domain. You can apply Managed Configuration through the Defakto API, Terraform, or the Defakto CLI. See applying configuration. For example, with the CLI:

defakto config set trust-domain --id <trust-domain-id> oauth-server.yaml

Step 2 (optional): Restrict access​

With the configuration from step 1, any workload in your trust domain can exchange a user's token for any service. Each receiving service still decides what to allow, based on the user and the workloads named in the token.

A token policy adds a central control in front of that. Workloads that the policy doesn't allow never receive a token for the user, so they can't act for the user at all. For example, this policy lets the web backend act for users only toward the orders service, lets the orders service act for them only toward the payments service, and refuses every other request:

spec:
# endpoints and identityProviders as in step 1
policy:
authorization:
unmatched: deny
allow:
- name: backend-acts-for-users
match:
caller:
spiffeID: spiffe://example.com/ns/web/sa/backend
user:
identityProvider: corp-idp
requestedTokenType: urn:defakto:security:token-type:jwt-svid-obo
audience: spiffe://example.com/orders

- name: orders-acts-for-users
match:
caller:
spiffeID: spiffe://example.com/ns/orders/sa/api
user:
identityProvider: corp-idp
requestedTokenType: urn:defakto:security:token-type:jwt-svid-obo
audience: spiffe://example.com/payments

See the token policy reference for every kind of rule.

Use on-behalf-of tokens​

The owners of each workload and service in the chain do this.

You need:

  • The trust domain set up
  • Workloads that can get JWT-SVIDs or X.509-SVIDs, and can reach your Trust Domain Servers

Step 1: Exchange the user's token for an OBO token​

The first workload in the chain exchanges the user's token for an OBO token whose audience is the service it calls. Send a token exchange request to <address of your Trust Domain Servers>/oauth/token:

The workload proves its identity with a JWT-SVID sent as a client assertion:

POST /oauth/token HTTP/1.1
Host: td-server.defakto.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<the user's ID token or access token>
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&requested_token_type=urn:defakto:security:token-type:jwt-svid-obo
&audience=spiffe://example.com/orders
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-spiffe
&client_assertion=<the workload's JWT-SVID>

The body is shown on separate lines for readability. Send it as a single URL-encoded form body.

ParameterValue
grant_typeExactly urn:ietf:params:oauth:grant-type:token-exchange
subject_tokenThe user's token: the ID token or access token your identity provider issued when the user signed in to your application
subject_token_typeurn:ietf:params:oauth:token-type:id_token for an ID token, or urn:ietf:params:oauth:token-type:access_token for an access token
requested_token_typeExactly urn:defakto:security:token-type:jwt-svid-obo
audienceThe service you're calling
client_assertion_type and client_assertionWith a JWT-SVID: urn:ietf:params:oauth:client-assertion-type:jwt-spiffe, and a JWT-SVID for the workload with its audience set to your trust domain's JWT issuer URL, not its OAuth issuer URL. Find the JWT Issuer in the Defakto console, under your trust domain's Settings > Trust domain URLs.
client_idWith an X.509-SVID: the workload's SPIFFE ID, exactly as it appears in the X.509-SVID

A successful response has HTTP status 200 and a JSON body:

{
"access_token": "<OBO token>",
"issued_token_type": "urn:defakto:security:token-type:jwt-svid-obo",
"token_type": "Bearer",
"expires_in": 300
}

Step 2: Call the next service with the OBO token​

Send the OBO token to the service you're calling, the same way you send JWT-SVIDs to it today, usually in the Authorization header:

Authorization: Bearer <OBO token>

The OBO token expires no later than the user's token it was issued from. Exchange the user's token again for each new request, or reuse the OBO token until shortly before it expires.

Step 3: Exchange the OBO token at each further hop​

A service that received an OBO token, and calls another service for the same user, exchanges the OBO token for a new one whose audience is the next service:

The workload proves its identity with a JWT-SVID sent as a client assertion:

POST /oauth/token HTTP/1.1
Host: td-server.defakto.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<the OBO token the service received>
&subject_token_type=urn:defakto:security:token-type:jwt-svid-obo
&requested_token_type=urn:defakto:security:token-type:jwt-svid-obo
&audience=spiffe://example.com/payments
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-spiffe
&client_assertion=<the workload's JWT-SVID>

The parameters are the same as in step 1, except that subject_token is the OBO token the service received, and subject_token_type is urn:defakto:security:token-type:jwt-svid-obo. The service making this exchange authenticates with its own SVID.

The new OBO token names the same user, and adds this service to the workloads that acted for the user.

Step 4: Read the user in each receiving service​

A service that receives an OBO token validates it as a JWT-SVID, with a SPIFFE library or as the JWT-SVID specification describes. Then it reads these claims, shown in the example token above:

ClaimMeaning
subThe workload calling you
obo.subThe user
obo.issThe identity provider that vouched for the user
obo.actThe workloads that acted for the user, most recent first. The outermost act.sub is always the same as sub.
txnAn identifier that stays the same across every exchange for this request, so you can correlate the hops in your logs

A JWT-SVID without an obo claim is an ordinary JWT-SVID, for a workload acting as itself.

Use Transaction Tokens​

The owners of each workload and service in the chain do this.

You need:

  • The trust domain set up
  • Workloads that can get JWT-SVIDs or X.509-SVIDs, and can reach your Trust Domain Servers

Step 1: Get a Transaction Token at the edge​

The first workload in the chain, such as your API gateway, exchanges the user's access token for a Transaction Token. Transaction Tokens can't be exchanged from ID tokens. Send a token exchange request to <address of your Trust Domain Servers>/oauth/token:

The workload proves its identity with a JWT-SVID sent as a client assertion:

POST /oauth/token HTTP/1.1
Host: td-server.defakto.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<the user's access token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&requested_token_type=urn:ietf:params:oauth:token-type:txn_token
&audience=example.com
&scope=<scopes, separated by spaces>
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-spiffe
&client_assertion=<the workload's JWT-SVID>
ParameterValue
grant_typeExactly urn:ietf:params:oauth:grant-type:token-exchange
subject_tokenThe access token your identity provider issued when the user signed in to your application
subject_token_typeExactly urn:ietf:params:oauth:token-type:access_token
requested_token_typeExactly urn:ietf:params:oauth:token-type:txn_token
audienceYour trust domain name, such as example.com
scopeThe scopes of the transaction, separated by spaces. Each scope must be one of the scopes in the user's access token.
client_assertion_type and client_assertion, or client_idAs for OBO tokens

A successful response has HTTP status 200 and a JSON body, as the Transaction Tokens draft defines:

{
"access_token": "<Transaction Token>",
"issued_token_type": "urn:ietf:params:oauth:token-type:txn_token",
"token_type": "N_A",
"expires_in": 300
}

The Transaction Token expires no later than the user's token it was exchanged from.

Step 2: Pass the Transaction Token along​

Send the Transaction Token in the Txn-Token HTTP header of every call that's part of the transaction, in addition to the credential each service already uses to authenticate the call:

Authorization: Bearer <the calling workload's JWT-SVID>
Txn-Token: <Transaction Token>

Don't send the Transaction Token in the Authorization header, which stays free for each call's own credential, such as the calling workload's JWT-SVID. Each service forwards the same Transaction Token, unchanged, in the Txn-Token header of the calls it makes. This follows the Transaction Tokens draft standard.

Step 3: Read the user in each receiving service​

A service that receives a Transaction Token validates it as the Transaction Tokens draft describes, using the keys at your trust domain's OAuth JWKS Endpoint, shown in the Defakto console under Settings > Trust domain URLs, with your trust domain name as the audience. Then it reads these claims:

ClaimMeaning
subThe user who started the transaction
scopeThe scopes of the transaction
txnAn identifier for the transaction, the same in every service

Troubleshooting​

tip

To see why a request failed, open your trust domain in the Defakto console, and go to Issuance > Token Exchanges.

The token endpoint returns invalid_request with subject token rejected. Defakto didn't accept the user's token. Check that the token's issuer matches the identity provider's issuer, that its audience is in idTokens.audiences or accessTokens.audiences, that subject_token_type matches the kind of token you sent, that the token passes any claimValidationRules, and that it hasn't expired.

The token endpoint returns invalid_client. Defakto couldn't authenticate the workload making the exchange. Check that the client assertion's audience is the JWT issuer URL, not the OAuth issuer URL. With mutual TLS, check that client_id is exactly the SPIFFE ID in the workload's X.509-SVID.

The token endpoint refuses the request, but the tokens are correct. Your trust domain's token policy may not allow this workload to act for the user toward this service. Ask your Defakto administrators to check the policy. The OAuth server's audit event for the request shows why it was refused.