feat(oidc): provide actor information in userinfo actions (#12566)

# Which Problems Are Solved

Expose impersonating actor to `preAccessToken` and `preUserinfo`
actions.

# How the Problems Are Solved

Provide the `actor` information already present in the OIDC session
model of JWT token to the relevant actions. Both goja-based actions "v1"
and webhook based execution targets carry the actor information now.
Actor remains null in case of non-impersonated tokens. Actor may contain
nested actors to display a delegation chain of impersonators.

# Additional Changes

- dba6261c8ed0dfd00ffde6193910b1405b17432f: refactor `userinfoFlows` to
reduce complexity, add test coverage and solve a couple of potential
bugs.
- `getClientId` in actions was previously undocumented. Added to
documentation.
- A skill that reproduces manual testing using webhook.site or a local
sink.

# Additional Context

- Closes https://github.com/zitadel/zitadel/issues/12097

---------

Co-authored-by: Claude Sonnet 5 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Wim Van Laer <[email protected]>
This commit is contained in:
Tim Möhlmann
2026-08-12 07:36:56 +00:00
committed by GitHub
co-authored by Claude Sonnet 5 Copilot Autofix powered by AI Wim Van Laer
parent 790244dd49
commit 6127b567e7
16 changed files with 1406 additions and 141 deletions
@@ -25,6 +25,11 @@ The trigger is represented by the following Ids in the API: `4`
- `grants` [*UserGrantList*](./objects#user-grant-list)
- `org`
- `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `application`
- `getClientId()` *string*
The client id of the application the token was requested for
- `actor` [*token actor*](./objects#token-actor)
The party which obtained the token on behalf of the user, `null` unless the token was obtained through token exchange or impersonation
- `api`
The second parameter contains the following fields:
- `v1`
@@ -59,6 +64,11 @@ The trigger is represented by the following Ids in the API: `5`
- `grants` [*UserGrantList*](./objects#user-grant-list)
- `org`
- `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `application`
- `getClientId()` *string*
The client id of the application the token was requested for
- `actor` [*token actor*](./objects#token-actor)
The party which obtained the token on behalf of the user, `null` unless the token was obtained through token exchange or impersonation
- `api`
The second parameter contains the following fields:
- `v1`
@@ -187,6 +187,19 @@ This object represents [the claims](../openidoauth/claims) which will be written
There could be additional fields depending on the settings of your [project](../../guides/manage/console/projects-overview#role-settings) and your [application](../../guides/manage/console/applications-overview#token-settings)
## token actor
This object represents the actor of a token, this is the party which obtained the token on behalf of the
subject through [token exchange](/guides/integrate/token-exchange#actor-token) or impersonation.
It corresponds to the [`act` claim](../openidoauth/claims) of the token and is `null` if the token has no actor.
- `userId` *string*
The id of the user acting on behalf of the subject
- `issuer` *string*
The issuer of the token the actor was authenticated with
- `actor` *token actor*
The previous actor in the delegation chain, `null` if there is none
## user grant list
This object represents a list of user grants (role assignments) stored in ZITADEL.
@@ -163,10 +163,16 @@ Your server should now print out something like the following. Check out the [Se
"id" : "312909075211944344",
"name" : "ZITADEL",
"primary_domain" : "example.com"
},
"application" : {
"client_id" : "312909075212534168"
}
}
```
This login was a regular OIDC flow, so no `actor` is included. The `actor` object is only sent if the
token was obtained through [token exchange](/guides/integrate/token-exchange#actor-token) or impersonation.
For any further information related to [the OIDC Flow, refer to our documentation.](/guides/integrate/login/oidc/login-users)
## Conclusion
@@ -138,10 +138,24 @@ The information sent to the Endpoint is structured as JSON:
"projectId": "",
"projectName": ""
}
]
],
"application": {
"client_id": "The client_id of the application the token was requested for"
},
"actor": {
"user_id": "The ID of the user acting on behalf of the token subject",
"issuer": "The issuer of the token the actor was authenticated with",
"actor": {
"user_id": "The previous actor in the delegation chain, if there is one",
"issuer": ""
}
}
}
```
The `actor` object corresponds to the `act` claim of the token. It is only sent if the token was
obtained through [token exchange](/guides/integrate/token-exchange#actor-token) or impersonation.
The expected structure of the JSON as response:
```json
@@ -232,10 +246,24 @@ The information sent to the Endpoint is structured as JSON:
"projectId": "",
"projectName": ""
}
]
],
"application": {
"client_id": "The client_id of the application the token was requested for"
},
"actor": {
"user_id": "The ID of the user acting on behalf of the token subject",
"issuer": "The issuer of the token the actor was authenticated with",
"actor": {
"user_id": "The previous actor in the delegation chain, if there is one",
"issuer": ""
}
}
}
```
The `actor` object corresponds to the `act` claim of the token. It is only sent if the token was
obtained through [token exchange](/guides/integrate/token-exchange#actor-token) or impersonation.
The expected structure of the JSON as response:
```json