Skip to content
Open
Show file tree
Hide file tree
Changes from all 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,12 @@ 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 ACTOR_SUBJECT = "ACTOR_SUBJECT";
public static final String EXISTING_ACT_CLAIM = "EXISTING_ACT_CLAIM";

/**
* Enum for OIDC supported subject types.
*/
Expand Down Expand Up @@ -195,10 +201,12 @@ public static SubjectType fromValue(String text) {
public static final String ID_TOKEN_SUBJECT_TOKEN = "id_token subject_token";
public static final String IMPERSONATED_SUBJECT = "IMPERSONATED_SUBJECT";
public static final String IMPERSONATING_ACTOR = "IMPERSONATING_ACTOR";
public static final String DELEGATING_ACTOR = "DELEGATING_ACTOR";
public static final String IDTOKEN_TOKEN = "id_token token";
public static final String ACTOR_TOKEN = "actor_token";
public static final String SCOPE = "scope";
public static final String MAY_ACT = "may_act";
public static final String ACT = "act";
public static final String SUB = "sub";

//Constants used for OAuth/OpenID Connect Configuration UI
Expand Down Expand Up @@ -598,6 +606,7 @@ public static class OIDCClaims {
public static final String APP_ROLES = "application_roles";
public static final String CUSTOM = "custom";
public static final String AZP = "azp";
public static final String CLIENT_ID = "client_id";
public static final String AUTH_TIME = "auth_time";
public static final String AT_HASH = "at_hash";
public static final String NONCE = "nonce";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.wso2.carbon.identity.openidconnect.action.preissueidtoken.dto.IDTokenDTO;

import java.io.Serializable;
import java.util.List;
import java.util.Properties;

/**
Expand Down Expand Up @@ -69,6 +70,8 @@ public class OAuthAuthzReqMessageContext implements Serializable {
private IDTokenDTO preIssueIDTokenActionDTO;
private String tokenId;

private List<String> audiences;

public OAuthAuthzReqMessageContext(OAuth2AuthorizeReqDTO authorizationReqDTO) {

this.authorizationReqDTO = authorizationReqDTO;
Expand Down Expand Up @@ -322,6 +325,16 @@ public void setPreIssueIDTokenActionDTO(IDTokenDTO preIssueIDTokenActionDTO) {
this.preIssueIDTokenActionDTO = preIssueIDTokenActionDTO;
}

public List<String> getAudiences() {

return audiences;
}

public void setAudiences(List<String> audiences) {

this.audiences = audiences;
}

public String getTokenId() {

return tokenId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@

import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.ACTOR_TOKEN;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.DELEGATING_ACTOR;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.GrantTypes.REFRESH_TOKEN;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.GrantTypes.TOKEN_EXCHANGE;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.IMPERSONATING_ACTOR;
Expand Down Expand Up @@ -627,6 +628,12 @@ private OAuth2AccessTokenRespDTO validateGrantAndIssueToken(OAuth2AccessTokenReq
diagnosticLogBuilder.inputParam(IMPERSONATOR, impersonatorId);
}
diagnosticLogBuilder.resultMessage("Impersonated Access token issued for the application.");
} else if (tokReqMsgCtx.isDelegationRequest()) {
if (tokReqMsgCtx.getProperty(DELEGATING_ACTOR) != null) {
diagnosticLogBuilder.inputParam("delegating_actor",
tokReqMsgCtx.getProperty(DELEGATING_ACTOR).toString());
}
diagnosticLogBuilder.resultMessage("Delegated Access token issued for the application.");
} else {
diagnosticLogBuilder.resultMessage("Access token issued for the application.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
import java.util.Map;
import java.util.UUID;

import static org.wso2.carbon.identity.oauth.common.OAuthConstants.ACTOR_AZP;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.ACTOR_SUBJECT;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.EXISTING_ACT_CLAIM;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.IS_DELEGATION_REQUEST;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.OIDCConfigProperties.SUBJECT_TOKEN_EXPIRY_TIME_VALUE;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.RENEW_TOKEN_WITHOUT_REVOKING_EXISTING_ENABLE_CONFIG;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.REQUEST_BINDING_TYPE;
Expand Down Expand Up @@ -428,6 +432,11 @@ protected String buildJWTToken(OAuthTokenReqMessageContext request) throws Ident
.getClientId());
JWTClaimsSet.Builder jwtClaimsSetBuilder = new JWTClaimsSet.Builder(jwtClaimsSet);

// Set explicitly requested audiences first (if present)
if (request.getAudiences() != null && !request.getAudiences().isEmpty()) {
jwtClaimsSetBuilder.audience(request.getAudiences());
}

if (request.getScope() != null && Arrays.asList((request.getScope())).contains(AUDIENCE)) {
jwtClaimsSetBuilder.audience(Arrays.asList(request.getScope()));
}
Expand Down Expand Up @@ -464,6 +473,11 @@ protected String buildJWTToken(OAuthAuthzReqMessageContext request) throws Ident
.getConsumerKey());
JWTClaimsSet.Builder jwtClaimsSetBuilder = new JWTClaimsSet.Builder(jwtClaimsSet);

// Set explicitly requested audiences first (if present)
if (request.getAudiences() != null && !request.getAudiences().isEmpty()) {
jwtClaimsSetBuilder.audience(request.getAudiences());
}

if (request.getApprovedScope() != null && Arrays.asList((request.getApprovedScope())).contains(AUDIENCE)) {
jwtClaimsSetBuilder.audience(Arrays.asList(request.getApprovedScope()));
}
Expand Down Expand Up @@ -895,6 +909,54 @@ protected JWTClaimsSet createJWTClaimSet(OAuthAuthzReqMessageContext authAuthzRe
jwtClaimsSetBuilder.audience(tokenReqMessageContext != null && tokenReqMessageContext.getAudiences() != null ?
tokenReqMessageContext.getAudiences() : OAuth2Util.getOIDCAudience(consumerKey, oAuthAppDO));

// Handle act claim for delegation (covers both regular delegation and self-delegation).
// Both flows set IS_DELEGATION_REQUEST=true, ACTOR_SUBJECT, ACTOR_AZP, and optionally EXISTING_ACT_CLAIM.
if (tokenReqMessageContext != null) {
Object isDelegationRequest = tokenReqMessageContext.getProperty(IS_DELEGATION_REQUEST);

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));
}
}
}
// 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 @@ -61,6 +61,8 @@ public class OAuthTokenReqMessageContext {

private boolean isImpersonationRequest;

private boolean isDelegationRequest;

private boolean preIssueAccessTokenActionsExecuted;

private boolean preIssueIDTokenActionsExecuted;
Expand Down Expand Up @@ -209,11 +211,21 @@ public boolean isImpersonationRequest() {
return isImpersonationRequest;
}

public boolean isDelegationRequest() {

return isDelegationRequest;
}

public void setImpersonationRequest(boolean impersonationRequest) {

isImpersonationRequest = impersonationRequest;
}

public void setDelegationRequest(boolean delegationRequest) {

isDelegationRequest = delegationRequest;
}

public boolean isPreIssueAccessTokenActionsExecuted() {

return preIssueAccessTokenActionsExecuted;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@

import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants.COMMONAUTH_COOKIE;
import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants.RequestParams.TYPE;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.DELEGATING_ACTOR;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.IMPERSONATING_ACTOR;

/**
Expand Down Expand Up @@ -332,16 +333,23 @@ private void revokeTokensOfBindingRef(AuthenticatedUser user, String tokenBindin

boolean isImpersonatingActorInitiatedRevocation = validateImpersonatingActorInitiatedRevocation(
accessTokenDO, user.getAuthenticatedSubjectIdentifier());

boolean isDelegatingActorInitiatedRevocation = validateDelegatingActorInitiatedRevocation(
accessTokenDO, user.getAuthenticatedSubjectIdentifier());

boolean isActorInitiatedRevocation = isImpersonatingActorInitiatedRevocation
|| isDelegatingActorInitiatedRevocation;

if (isFederatedRoleBasedAuthzEnabled
&& StringUtils.equalsIgnoreCase(
user.getFederatedIdPName(), authenticatedUser.getFederatedIdPName())
&& (isImpersonatingActorInitiatedRevocation
&& (isActorInitiatedRevocation
|| StringUtils.equalsIgnoreCase(user.getUserName(), authenticatedUser.getUserName()))) {
revokeFederatedTokens(consumerKey, user, accessTokenDO, tokenBindingReference);
} else if (
StringUtils.equalsIgnoreCase(tokenBindingType,
OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER)
|| isImpersonatingActorInitiatedRevocation
|| isActorInitiatedRevocation
|| StringUtils.equalsIgnoreCase(userId, authenticatedUser.getUserId())
) {
revokeTokens(consumerKey, accessTokenDO, tokenBindingReference);
Expand Down Expand Up @@ -369,6 +377,22 @@ private boolean validateImpersonatingActorInitiatedRevocation(
return false;
}

private boolean validateDelegatingActorInitiatedRevocation(
AccessTokenDO accessTokenDO, String authenticatedSubjectIdentifier) {

boolean isDelegationRequest = accessTokenDO.getAccessTokenExtendedAttributes() != null &&
accessTokenDO.getAccessTokenExtendedAttributes().getParameters() != null &&
accessTokenDO.getAccessTokenExtendedAttributes().getParameters()
.containsKey(DELEGATING_ACTOR);
if (isDelegationRequest) {
return Objects.equals(
accessTokenDO.getAccessTokenExtendedAttributes()
.getParameters().get(DELEGATING_ACTOR),
authenticatedSubjectIdentifier);
}
return false;
}

/**
* Get the access tokens mapped for the session identifier and revoke those tokens.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext;
import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext;

import java.util.Collections;
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 +21,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";
private static final String CIBA_GRANT_TYPE = "urn:openid:params:grant-type:ciba";

@Override
Expand All @@ -40,8 +42,23 @@ public Map<String, Object> getAdditionalClaims(OAuthTokenReqMessageContext conte
} else if ((GrantType.AUTHORIZATION_CODE.toString().equals(context.getOauth2AccessTokenReqDTO().getGrantType())
|| CIBA_GRANT_TYPE.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 @@ -85,6 +85,7 @@
import java.util.Set;
import java.util.UUID;

import static org.wso2.carbon.identity.oauth.common.OAuthConstants.DELEGATING_ACTOR;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.IMPERSONATING_ACTOR;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.OAUTH_APP;
import static org.wso2.carbon.identity.oauth.common.OAuthConstants.RENEW_TOKEN_WITHOUT_REVOKING_EXISTING_ENABLE_CONFIG;
Expand Down Expand Up @@ -656,6 +657,11 @@ private AccessTokenExtendedAttributes getAccessTokenExtendedAttributes(
addExtendedAttribute(IMPERSONATING_ACTOR, tokReqMsgCtx.getProperty(IMPERSONATING_ACTOR).toString(),
accessTokenExtendedAttributes);
}
if (tokReqMsgCtx.isDelegationRequest()) {
accessTokenExtendedAttributes =
addExtendedAttribute(DELEGATING_ACTOR, tokReqMsgCtx.getProperty(DELEGATING_ACTOR).toString(),
accessTokenExtendedAttributes);
}
// Add any new extended attributes here using @addExtendedAttribute.
return accessTokenExtendedAttributes;
}
Expand Down Expand Up @@ -692,7 +698,11 @@ private void updateMessageContextToCreateNewToken(OAuthTokenReqMessageContext to
long validityPeriodInMillis = getConfiguredExpiryTimeForApplication(tokReqMsgCtx, consumerKey, oAuthAppBean);
tokReqMsgCtx.setValidityPeriod(validityPeriodInMillis);
tokReqMsgCtx.setAccessTokenIssuedTime(timestamp.getTime());
tokReqMsgCtx.setAudiences(OAuth2Util.getOIDCAudience(consumerKey, oAuthAppBean));

// Only set default audiences if not already specified by grant handler
if (tokReqMsgCtx.getAudiences() == null || tokReqMsgCtx.getAudiences().isEmpty()) {
tokReqMsgCtx.setAudiences(OAuth2Util.getOIDCAudience(consumerKey, oAuthAppBean));
}

updateRefreshTokenValidityPeriodInMessageContext(oAuthAppBean, existingTokenBean, tokReqMsgCtx);
}
Expand Down
Loading
Loading