doc/development/ai_features/composite_identity.md
For security reasons, you should use composite identity for any AI-generated activity on the GitLab platform that performs write actions.
Features that use Composite Identity:
To generate a composite identity token, you must have:
A service account user who is the primary token owner.
Availability and licensing:
The service account must have composite_identity_enforced set to true.
The OAuth application used for composite identity must enable a dynamic scope of user:*.
Only OAuth tokens are supported.
user:$ID (for example, user:123). Include other scopes as needed (for example, api).https://gitlab.example.com/oauth/token.user:$ID and any required base scopes.https://gitlab.example.com/oauth/token> endpoint using grant_type=refresh_token.user:$ID in its scopes. It refreshes like a standard OAuth access token.ee/app/services/ai/duo_workflows/onboarding_service.rb):# Rails console
app = Authn::OauthApplication.new(
name: "Composite Identity App",
redirect_uri: Gitlab::Routing.url_helpers.root_url, # unused but cannot be nil
scopes: ::Gitlab::Auth::AI_WORKFLOW_SCOPES + [::Gitlab::Auth::DYNAMIC_USER],
trusted: false,
confidential: false # public client (no secret required)
)
app.save!
# Assuming you want to create a composite OAuth token for the GitLab Duo Workflow OAuth application and service account + root user in your GDK.
org = Organizations::Organization.default_organization
user = User.first
oauth_token_service = Ai::DuoWorkflows::CreateCompositeOauthAccessTokenService.new(
current_user: user,
organization: org,
).execute
oauth_token_service.payload[:oauth_access_token].plaintext_token
A request made with a composite identity token is authorized only if both are true:
user:$ID in the token scopes has access to the resource.When a request includes a composite identity OAuth token, the Rails request context overrides current_user to the human user extracted from the user:$ID scope. While the token itself still belongs to the service account, the user who originated the request is considered the current user. This means:
current_user runs as the human user.resolve_composite_identity_actor to determine the correct actor for attribution, the result depends on how the identity was linked for the current request.Always use resolve_composite_identity_actor to resolve the actor for any write operation. You do not need to determine the context yourself — it is set automatically at the request boundary:
actor = Gitlab::Auth::Identity.resolve_composite_identity_actor(current_user)
Use the returned actor wherever you set authorship (for example: notes, issues/MRs, commits, pipeline user context).
The method returns the correct actor for the situation:
user:$ID scope, or CI job): internally tagged as :authentication context. Returns the service account. The SA is the actor and should be attributed.:permission_check context. Returns the human. The human is the actor and should be attributed.current_user unchanged.You never need to call this method differently depending on the scenario — the identity system records the context when the request is authenticated, and resolve_composite_identity_actor uses it automatically.
Reference: MR !204010, MR !223788
# Expect 200 only if BOTH the human and service account can read the project
curl --silent --show-error --fail --header "Authorization: Bearer <COMPOSITE_TOKEN>" \
"https://gitlab.example.com/api/v4/projects/<NAMESPACE>%2F<PROJECT>"
Common outcomes:
# Rails console
service_account = User.find_by_username("service_account")
service_account.update!(composite_identity_enforced: true)
dynamic_scopes: "user:*".user:$ID: re-issue the grant/token with the concrete user:$ID in scopes.:authentication (SA attributed); web/assignment flows use :permission_check (human attributed). See Attributing actions to the correct actor.api) are present.