Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dbf8887
Support audince parameter in token request to support multiple audien…
Bin4yi Feb 3, 2026
8cda162
Supports nested act sub claim with azp claim inside it
Bin4yi Feb 3, 2026
e0f979c
defined constants for the string literals
Bin4yi Feb 10, 2026
7a55608
defined constants for the string literals and remove explicit null ch…
Bin4yi Feb 10, 2026
de9aeff
introduce "DELEGATING_ACTOR" constant to the delegation flow
Bin4yi Mar 4, 2026
1c4b0e8
refactor the code by combining selfDelegation and Delegation flows
Bin4yi Mar 12, 2026
51eef55
Merge branch 'audience-validation' into act-sub-azp
Bin4yi Mar 19, 2026
7700b80
merge audience-validation branch into act-sub-azp branch
Bin4yi Mar 19, 2026
45d0f85
Apply suggestion from @pavinduLakshan
Bin4yi Mar 19, 2026
c9b9537
added tests
Bin4yi Mar 19, 2026
697e0a3
added tests
Bin4yi Mar 19, 2026
2a1ab5a
added tests
Bin4yi Mar 20, 2026
00f0aa1
refactor ActorTokenValidator function
Bin4yi Mar 23, 2026
6abcc16
refactor ActorTokenValidator function
Bin4yi Mar 23, 2026
5678abd
Merge branch 'master' into act-sub-azp
Bin4yi Mar 23, 2026
8d217e5
merge the file ActorTokenValidator.java from git
Bin4yi Mar 23, 2026
6701453
refactor ActorTokenValidator.java file
Bin4yi Mar 23, 2026
29afe65
fix checkstyle errors
Bin4yi Mar 23, 2026
22de21d
fix checkstyle errors
Bin4yi Mar 23, 2026
12d79b5
fix test error in AgentAccessTokenClaimProviderTest file
Bin4yi Mar 23, 2026
1ec499b
used constants in OAuthConstants.java file
Bin4yi Mar 23, 2026
5015add
refactor the code
Bin4yi Mar 30, 2026
edd02f8
refactor the code
Bin4yi Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ public final class OAuthConstants {
".EnableHybridFlowAppLevelValidation";
public static final String RESTRICT_FRAGMENT_COMPONENTS = "OAuth.Callback.RestrictFragmentComponents";

// Token Exchange delegation property key
public static final String ACTOR_AZP = "ACTOR_AZP";
public static final String IS_DELEGATION_REQUEST = "IS_DELEGATION_REQUEST";
public static final String IS_SELF_DELEGATION_WITH_ACT = "IS_SELF_DELEGATION_WITH_ACT";
public static final String ACTOR_SUBJECT = "ACTOR_SUBJECT";
public static final String EXISTING_ACT_CLAIM = "EXISTING_ACT_CLAIM";

/**
* Enum for OIDC supported subject types.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.REQUEST_BINDING_TYPE;
import static org.wso2.carbon.identity.oauth2.util.OAuth2Util.JWT_X5T_ENABLED;
import static org.wso2.carbon.identity.oauth2.util.OAuth2Util.getPrivateKey;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.IS_DELEGATION_REQUEST;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.IS_SELF_DELEGATION_WITH_ACT;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.ACTOR_SUBJECT;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.ACTOR_AZP;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.EXISTING_ACT_CLAIM;

/**
* Self contained access token builder.
Expand Down Expand Up @@ -724,6 +729,77 @@ protected JWTClaimsSet createJWTClaimSet(OAuthAuthzReqMessageContext authAuthzRe
jwtClaimsSetBuilder.audience(tokenReqMessageContext != null && tokenReqMessageContext.getAudiences() != null ?
tokenReqMessageContext.getAudiences() : OAuth2Util.getOIDCAudience(consumerKey, oAuthAppDO));

// Handle act claim for both delegation and self-delegation with existing act
if (tokenReqMessageContext != null) {
Object isDelegationRequest = tokenReqMessageContext.getProperty(IS_DELEGATION_REQUEST);
Object isSelfDelegationWithAct = tokenReqMessageContext.getProperty(IS_SELF_DELEGATION_WITH_ACT);

// Case 1: Regular delegation - create new act claim with nesting
if (Boolean.TRUE.equals(isDelegationRequest)) {
Object actorSubject = tokenReqMessageContext.getProperty(ACTOR_SUBJECT);
Object actorAzp = tokenReqMessageContext.getProperty(ACTOR_AZP);

if (actorSubject != null) {
Object existingActClaim = tokenReqMessageContext.getProperty(EXISTING_ACT_CLAIM);

// Build the act claim structure
Map<String, Object> actClaim = new HashMap<>();
actClaim.put("sub", actorSubject.toString());

//Include azp in act claim
if (actorAzp != null) {
actClaim.put("azp", actorAzp.toString());
}

// Support nested act claims for chained delegation
if (existingActClaim != null) {
if (existingActClaim instanceof Map) {
actClaim.put("act", existingActClaim);
if (log.isDebugEnabled()) {
log.debug("Delegation: Nesting existing act claim. New actor: " + actorSubject +
", AZP: " + actorAzp);
}
} else {
if (log.isDebugEnabled()) {
log.debug("Delegation: Existing act claim is not in expected Map format. " +
"Type: " + existingActClaim.getClass().getName());
}
}
}

jwtClaimsSetBuilder.claim("act", actClaim);

if (log.isDebugEnabled()) {
log.debug("Added act claim for delegation. Actor: " + actorSubject +
", AZP: " + actorAzp + ", Has nested act: " + (existingActClaim != null));
}
}
}
// Case 2: Self-delegation with existing act claim - preserve it
else if (Boolean.TRUE.equals(isSelfDelegationWithAct)) {
Object existingActClaim = tokenReqMessageContext.getProperty(EXISTING_ACT_CLAIM);

if (existingActClaim != null) {
if (existingActClaim instanceof Map) {
// Preserve the entire act claim chain as-is
jwtClaimsSetBuilder.claim("act", existingActClaim);

if (log.isDebugEnabled()) {
log.debug("Self-delegation: Preserved existing act claim chain with azp");
}
} else {
if (log.isDebugEnabled()) {
log.debug("Self-delegation: Existing act claim is not in expected format. " +
"Type: " + existingActClaim.getClass().getName());
}
}
}
}

// Note: Regular self-delegation (without existing act claim) does NOT add any act claim
// Note: Impersonation uses "may_act" claim in subject token, not "act" in issued token
}

JWTClaimsSet jwtClaimsSet;

// Handle custom claims
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import java.util.HashMap;
import java.util.Map;

import static org.wso2.carbon.identity.oauth.common.OAuthConstants.ACTOR_AZP;

Comment on lines +13 to +14
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

This branch overwrites the nested delegation chain.

JWTTokenIssuer.createJWTClaimSet(..) already builds the delegation act claim from ACTOR_* plus EXISTING_ACT_CLAIM. Because provider claims are applied afterward in buildJWTToken(..), this branch replaces it with a flat {sub, azp} and drops any incoming nested act. Please add a regression test with an actor token that already contains act.

🛠️ Minimal fix
 import static org.wso2.carbon.identity.oauth.common.OAuthConstants.ACTOR_AZP;
+import static org.wso2.carbon.identity.oauth.common.OAuthConstants.IS_DELEGATION_REQUEST;
@@
-        } else if (GrantType.AUTHORIZATION_CODE.toString().equals(context.getOauth2AccessTokenReqDTO().getGrantType())
-            && context.getRequestedActor() != null) {
+        } else if (GrantType.AUTHORIZATION_CODE.toString().equals(context.getOauth2AccessTokenReqDTO().getGrantType())
+                && context.getRequestedActor() != null
+                && !Boolean.TRUE.equals(context.getProperty(IS_DELEGATION_REQUEST))) {

Also applies to: 42-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/claims/AgentAccessTokenClaimProvider.java`
around lines 14 - 15, The provider overwrites any existing nested delegation
chain by replacing the act claim with a flat {sub, azp} when
AgentAccessTokenClaimProvider applies provider claims; update
AgentAccessTokenClaimProvider (the branch handling ACTOR_AZP/ACTOR_* in
buildJWTToken flow) to merge with an existing act claim instead of replacing it:
detect EXISTING_ACT_CLAIM built by JWTTokenIssuer.createJWTClaimSet(..),
preserve nested act entries and append/merge the new actor (azp/sub) into the
existing structure rather than clobbering it, and add a regression test that
issues an actor token already containing an act claim and asserts the nested
delegation chain is retained; apply the same fix to the other similar branch
(lines referenced as 42-60) to ensure consistency.

/**
* A class that provides additional claims for JWT access tokens when the AI agent is used.
*/
Expand All @@ -20,6 +22,7 @@ public class AgentAccessTokenClaimProvider implements JWTAccessTokenClaimProvide
private static final String SUB = "sub";
private static final String AGENT = "AGENT";
private static final String AUT = "aut";
private static final String AZP = "azp";

@Override
public Map<String, Object> getAdditionalClaims(OAuthAuthzReqMessageContext context) throws IdentityOAuth2Exception {
Expand All @@ -38,8 +41,23 @@ public Map<String, Object> getAdditionalClaims(OAuthTokenReqMessageContext conte
return agentMap;
} else if (GrantType.AUTHORIZATION_CODE.toString().equals(context.getOauth2AccessTokenReqDTO().getGrantType())
&& context.getRequestedActor() != null) {

Map<String, Object> actClaimMap = new HashMap<>();
actClaimMap.put(SUB, context.getRequestedActor());
// Include azp in act claim from context property
Object actorAzp = context.getProperty(ACTOR_AZP);
if (actorAzp != null) {
actClaimMap.put(AZP, actorAzp.toString());
} else {
// Fallback: use the client_id of the requesting application
String clientId = context.getOauth2AccessTokenReqDTO().getClientId();
if (StringUtils.isNotEmpty(clientId)) {
actClaimMap.put(AZP, clientId);
}
}

Map<String, Object> agentMap = new HashMap<>();
agentMap.put(ACT, Collections.singletonMap(SUB, context.getRequestedActor()));
agentMap.put(ACT, actClaimMap);
return agentMap;
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,32 @@ private void validateActorToken(OAuthTokenReqMessageContext tokReqMsgCtx, String
if (!StringUtils.equals(actorTokenSubject, requestedActor)) {
throw new IdentityOAuth2Exception("Actor token subject does not match the requested actor");
}

Object actorAzpClaim = claimsSet.getClaim("azp");
if (actorAzpClaim == null) {
// Fallback to client_id if azp not present
actorAzpClaim = claimsSet.getClaim("client_id");
}
// Check for existing act claim in actor token for delegation chain
Object existingActClaim = claimsSet.getClaim("act");
// Set delegation properties in context
tokReqMsgCtx.setImpersonationRequest(false);
tokReqMsgCtx.addProperty("IS_DELEGATION_REQUEST", true);
tokReqMsgCtx.addProperty("ACTOR_SUBJECT", actorTokenSubject);
if (actorAzpClaim != null) {
tokReqMsgCtx.addProperty("ACTOR_AZP", actorAzpClaim.toString());
if (log.isDebugEnabled()) {
log.debug("Actor AZP extracted from actor token: " + actorAzpClaim.toString());
}
}
// Preserve existing act claim for delegation chain nesting
if (existingActClaim != null) {
tokReqMsgCtx.addProperty("EXISTING_ACT_CLAIM", existingActClaim);
if (log.isDebugEnabled()) {
log.debug("Found existing act claim in actor token - will nest in delegation chain");
}
}

// Validate mandatory claims
JWTUtils.validateMandatoryClaims(claimsSet);
String jwtIssuer = claimsSet.getIssuer();
Expand Down
Loading