diff --git a/components/org.wso2.carbon.identity.oauth.common/src/main/java/org/wso2/carbon/identity/oauth/common/OAuthConstants.java b/components/org.wso2.carbon.identity.oauth.common/src/main/java/org/wso2/carbon/identity/oauth/common/OAuthConstants.java index 2570ca109d..d632626b74 100644 --- a/components/org.wso2.carbon.identity.oauth.common/src/main/java/org/wso2/carbon/identity/oauth/common/OAuthConstants.java +++ b/components/org.wso2.carbon.identity.oauth.common/src/main/java/org/wso2/carbon/identity/oauth/common/OAuthConstants.java @@ -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. */ @@ -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 @@ -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"; diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/OAuthAuthzReqMessageContext.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/OAuthAuthzReqMessageContext.java index 80de0566ab..64668eea23 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/OAuthAuthzReqMessageContext.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/OAuthAuthzReqMessageContext.java @@ -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; /** @@ -69,6 +70,8 @@ public class OAuthAuthzReqMessageContext implements Serializable { private IDTokenDTO preIssueIDTokenActionDTO; private String tokenId; + private List audiences; + public OAuthAuthzReqMessageContext(OAuth2AuthorizeReqDTO authorizationReqDTO) { this.authorizationReqDTO = authorizationReqDTO; @@ -322,6 +325,16 @@ public void setPreIssueIDTokenActionDTO(IDTokenDTO preIssueIDTokenActionDTO) { this.preIssueIDTokenActionDTO = preIssueIDTokenActionDTO; } + public List getAudiences() { + + return audiences; + } + + public void setAudiences(List audiences) { + + this.audiences = audiences; + } + public String getTokenId() { return tokenId; diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/AccessTokenIssuer.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/AccessTokenIssuer.java index 728b46c0f8..565af409b0 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/AccessTokenIssuer.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/AccessTokenIssuer.java @@ -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; @@ -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."); } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/JWTTokenIssuer.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/JWTTokenIssuer.java index de7b62efd5..b70d83d750 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/JWTTokenIssuer.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/JWTTokenIssuer.java @@ -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; @@ -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())); } @@ -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())); } @@ -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 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 diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/OAuthTokenReqMessageContext.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/OAuthTokenReqMessageContext.java index 07225e9624..ba86984c6a 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/OAuthTokenReqMessageContext.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/OAuthTokenReqMessageContext.java @@ -61,6 +61,8 @@ public class OAuthTokenReqMessageContext { private boolean isImpersonationRequest; + private boolean isDelegationRequest; + private boolean preIssueAccessTokenActionsExecuted; private boolean preIssueIDTokenActionsExecuted; @@ -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; diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/handlers/TokenBindingExpiryEventHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/handlers/TokenBindingExpiryEventHandler.java index 661b040ca2..6791bce67a 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/handlers/TokenBindingExpiryEventHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/handlers/TokenBindingExpiryEventHandler.java @@ -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; /** @@ -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); @@ -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. * diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/claims/AgentAccessTokenClaimProvider.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/claims/AgentAccessTokenClaimProvider.java index e0f2abfb39..54d254f703 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/claims/AgentAccessTokenClaimProvider.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/claims/AgentAccessTokenClaimProvider.java @@ -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; + /** * A class that provides additional claims for JWT access tokens when the AI agent is used. */ @@ -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 @@ -40,8 +42,23 @@ public Map getAdditionalClaims(OAuthTokenReqMessageContext conte } else if ((GrantType.AUTHORIZATION_CODE.toString().equals(context.getOauth2AccessTokenReqDTO().getGrantType()) || CIBA_GRANT_TYPE.equals(context.getOauth2AccessTokenReqDTO().getGrantType())) && context.getRequestedActor() != null) { + + Map 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 agentMap = new HashMap<>(); - agentMap.put(ACT, Collections.singletonMap(SUB, context.getRequestedActor())); + agentMap.put(ACT, actClaimMap); return agentMap; } return null; diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AbstractAuthorizationGrantHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AbstractAuthorizationGrantHandler.java index b8ff625241..3a80efdfd4 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AbstractAuthorizationGrantHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AbstractAuthorizationGrantHandler.java @@ -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; @@ -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; } @@ -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); } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/ActorTokenValidator.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/ActorTokenValidator.java index 0b583ff6be..128538a29b 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/ActorTokenValidator.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/ActorTokenValidator.java @@ -24,6 +24,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.application.common.model.IdentityProvider; +import org.wso2.carbon.identity.oauth.common.OAuthConstants; import org.wso2.carbon.identity.oauth2.IdentityOAuth2ClientException; import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; import org.wso2.carbon.identity.oauth2.util.JWTSignatureValidationUtils; @@ -45,6 +46,35 @@ public class ActorTokenValidator { private ActorTokenValidator() { } + /** + * Holds the extracted claims from a validated actor token. + */ + public static class ActorTokenClaims { + + private final String subject; + private final String azp; + private final Object existingActClaim; + + ActorTokenClaims(String subject, String azp, Object existingActClaim) { + this.subject = subject; + this.azp = azp; + this.existingActClaim = existingActClaim; + } + + public String getSubject() { + return subject; + } + + public String getAzp() { + return azp; + } + + public Object getExistingActClaim() { + return existingActClaim; + } + } + + /** * Validates the actor token JWT and returns the actor's subject claim. * @@ -56,6 +86,22 @@ private ActorTokenValidator() { */ public static String validateAndGetSubject(String actorToken, String tenantDomain) throws IdentityOAuth2Exception { + return validateAndExtractClaims(actorToken, tenantDomain).getSubject(); + } + + /** + * Validates the actor token JWT and returns the extracted actor claims including + * the subject, {@code azp}/{@code client_id}, and existing {@code act} claim. + * + * @param actorToken Raw JWT string representing the actor token. + * @param tenantDomain Tenant domain used for IDP lookup and issuer validation. + * @return {@link ActorTokenClaims} containing the subject, azp, and existing act claim. + * @throws IdentityOAuth2Exception If the JWT is invalid, the signature fails, + * the token is expired, or the issuer is unexpected. + */ + + public static ActorTokenClaims validateAndExtractClaims(String actorToken, String tenantDomain) + throws IdentityOAuth2Exception { SignedJWT signedJWT; try { @@ -91,6 +137,19 @@ public static String validateAndGetSubject(String actorToken, String tenantDomai + ", Received: " + jwtIssuer); } - return claimsSet.getSubject(); + // Extract azp/client_id and existing act claim for delegation chain processing. + Object azpClaim = claimsSet.getClaim(OAuthConstants.OIDCClaims.AZP); + if (azpClaim == null) { + // Fallback to client_id if azp not present. + azpClaim = claimsSet.getClaim(OAuthConstants.OIDCClaims.CLIENT_ID); + } + Object existingActClaim = claimsSet.getClaim(OAuthConstants.ACT); + + return new ActorTokenClaims( + claimsSet.getSubject(), + azpClaim != null ? azpClaim.toString() : null, + existingActClaim + ); } + } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AuthorizationCodeGrantHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AuthorizationCodeGrantHandler.java index ba01f4b57b..5d7be13432 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AuthorizationCodeGrantHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AuthorizationCodeGrantHandler.java @@ -625,10 +625,26 @@ private void validateRequestedActor(AuthzCodeDO authzCodeBean, OAuthTokenReqMess throw new IdentityOAuth2Exception("Actor token is not provided in the request."); } else if (StringUtils.isNotBlank(requestedActor) && StringUtils.isNotBlank(actorToken)) { String tenantDomain = tokReqMsgCtx.getOauth2AccessTokenReqDTO().getTenantDomain(); - String actorSub = ActorTokenValidator.validateAndGetSubject(actorToken, tenantDomain); - if (!StringUtils.equals(actorSub, requestedActor)) { + ActorTokenValidator.ActorTokenClaims claims = + ActorTokenValidator.validateAndExtractClaims(actorToken, tenantDomain); + if (!StringUtils.equals(claims.getSubject(), requestedActor)) { throw new IdentityOAuth2Exception("Actor token subject does not match the requested actor."); } + tokReqMsgCtx.setImpersonationRequest(false); + tokReqMsgCtx.addProperty(OAuthConstants.IS_DELEGATION_REQUEST, true); + tokReqMsgCtx.addProperty(OAuthConstants.ACTOR_SUBJECT, claims.getSubject()); + if (claims.getAzp() != null) { + tokReqMsgCtx.addProperty(OAuthConstants.ACTOR_AZP, claims.getAzp()); + if (log.isDebugEnabled()) { + log.debug("Actor AZP extracted from actor token: " + claims.getAzp()); + } + } + if (claims.getExistingActClaim() != null) { + tokReqMsgCtx.addProperty(OAuthConstants.EXISTING_ACT_CLAIM, claims.getExistingActClaim()); + if (log.isDebugEnabled()) { + log.debug("Found existing act claim in actor token - will nest in delegation chain"); + } + } tokReqMsgCtx.setRequestedActor(requestedActor); } } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java index 1d28b2b717..16a8bb3899 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java @@ -86,6 +86,7 @@ import java.sql.Timestamp; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -313,7 +314,7 @@ private void setPropertiesForTokenGeneration(OAuthTokenReqMessageContext tokReqM tokReqMsgCtx.setScope(validationBean.getScope()); tokReqMsgCtx.getOauth2AccessTokenReqDTO().setAccessTokenExtendedAttributes( validationBean.getAccessTokenExtendedAttributes()); - propagateImpersonationInfo(tokReqMsgCtx); + propagateActorInfo(tokReqMsgCtx); // Store the old access token as a OAuthTokenReqMessageContext property, this is already // a preprocessed token. tokReqMsgCtx.addProperty(PREV_ACCESS_TOKEN, validationBean); @@ -337,20 +338,33 @@ private void setPropertiesForTokenGeneration(OAuthTokenReqMessageContext tokReqM } } - private void propagateImpersonationInfo(OAuthTokenReqMessageContext tokenReqMessageContext) { - - log.debug("Checking for impersonation information in token request"); - if (tokenReqMessageContext != null && tokenReqMessageContext.getOauth2AccessTokenReqDTO() != null && - tokenReqMessageContext.getOauth2AccessTokenReqDTO().getAccessTokenExtendedAttributes() != null) { - String impersonator = tokenReqMessageContext.getOauth2AccessTokenReqDTO() - .getAccessTokenExtendedAttributes().getParameters() - .get(OAuthConstants.IMPERSONATING_ACTOR); - if (StringUtils.isNotBlank(impersonator)) { - tokenReqMessageContext.setImpersonationRequest(true); - tokenReqMessageContext.addProperty(OAuthConstants.IMPERSONATING_ACTOR, impersonator); - if (log.isDebugEnabled()) { - log.debug("Impersonation request identified for the user: " + impersonator); - } + private void propagateActorInfo(OAuthTokenReqMessageContext tokenReqMessageContext) { + + log.debug("Checking for actor information in token request"); + if (tokenReqMessageContext == null || tokenReqMessageContext.getOauth2AccessTokenReqDTO() == null || + tokenReqMessageContext.getOauth2AccessTokenReqDTO().getAccessTokenExtendedAttributes() == null) { + return; + } + + Map params = tokenReqMessageContext.getOauth2AccessTokenReqDTO() + .getAccessTokenExtendedAttributes().getParameters(); + + String impersonator = params.get(OAuthConstants.IMPERSONATING_ACTOR); + if (StringUtils.isNotBlank(impersonator)) { + tokenReqMessageContext.setImpersonationRequest(true); + tokenReqMessageContext.addProperty(OAuthConstants.IMPERSONATING_ACTOR, impersonator); + if (log.isDebugEnabled()) { + log.debug("Impersonation request identified for the user: " + impersonator); + } + return; + } + + String delegatingActor = params.get(OAuthConstants.DELEGATING_ACTOR); + if (StringUtils.isNotBlank(delegatingActor)) { + tokenReqMessageContext.setDelegationRequest(true); + tokenReqMessageContext.addProperty(OAuthConstants.DELEGATING_ACTOR, delegatingActor); + if (log.isDebugEnabled()) { + log.debug("Delegation request identified for the user: " + delegatingActor); } } } diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/JWTTokenIssuerTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/JWTTokenIssuerTest.java index 5ad42cb209..5f2bb8518c 100644 --- a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/JWTTokenIssuerTest.java +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/JWTTokenIssuerTest.java @@ -245,6 +245,52 @@ public void testBuildJWTTokenFromTokenMsgContext(String requestScopes[], } } + /** + * Test that preset audiences on {@link OAuthTokenReqMessageContext} are preserved in the JWT + * (delegation scenario where token exchange pre-sets explicit audiences). + */ + @Test + public void testBuildJWTTokenWithPresetAudiencesForTokenContext() throws Exception { + + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain("DUMMY_TENANT.COM"); + try (MockedStatic oAuth2Util = mockStatic(OAuth2Util.class); + MockedStatic identityTenantUtil = mockStatic(IdentityTenantUtil.class)) { + + identityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(anyString())).thenReturn(-1234); + OAuth2AccessTokenReqDTO accessTokenReqDTO = new OAuth2AccessTokenReqDTO(); + accessTokenReqDTO.setGrantType(USER_ACCESS_TOKEN_GRANT_TYPE); + accessTokenReqDTO.setClientId(DUMMY_CLIENT_ID); + HttpServletRequestWrapper httpServletRequestWrapper = mock(HttpServletRequestWrapper.class); + when(httpServletRequestWrapper.getRequestURL()).thenReturn(new StringBuffer(DUMMY_TOKEN_ENDPOINT)); + accessTokenReqDTO.setHttpServletRequestWrapper(httpServletRequestWrapper); + + OAuthTokenReqMessageContext reqMessageContext = new OAuthTokenReqMessageContext(accessTokenReqDTO); + reqMessageContext.addProperty(OAuthConstants.UserType.USER_TYPE, OAuthConstants.UserType.APPLICATION_USER); + AuthenticatedUser authenticatedUser = new AuthenticatedUser(); + authenticatedUser.setUserName("DUMMY_USERNAME"); + authenticatedUser.setTenantDomain("DUMMY_TENANT.COM"); + authenticatedUser.setUserStoreDomain("DUMMY_DOMAIN"); + authenticatedUser.setUserId(DUMMY_USER_ID); + reqMessageContext.setAuthorizedUser(authenticatedUser); + + // Pre-set audiences — simulates delegation/token-exchange where the grant handler + // sets explicit audiences before token issuance. + List presetAudiences = Collections.singletonList("https://preset-audience.example.com"); + reqMessageContext.setAudiences(presetAudiences); + // Scope without "aud" to avoid scope-based audience override + reqMessageContext.setScope(new String[]{"openid"}); + + prepareForBuildJWTToken(oAuth2Util); + JWTTokenIssuer jwtTokenIssuer = getJWTTokenIssuer(NONE); + String jwtToken = jwtTokenIssuer.buildJWTToken(reqMessageContext); + + PlainJWT plainJWT = PlainJWT.parse(jwtToken); + assertNotNull(plainJWT); + assertEquals(plainJWT.getJWTClaimsSet().getAudience(), presetAudiences, + "Preset audiences should appear in the JWT when pre-set on the token request context"); + } + } + /** * Test for Plain JWT Building from {@link OAuthAuthzReqMessageContext} */ @@ -520,6 +566,111 @@ public void testCreateJWTClaimSet(Object authzReqMessageContext, } } + @DataProvider(name = "delegationActClaimDataProvider") + public Object[][] delegationActClaimDataProvider() { + + Map existingActClaimMap = new HashMap<>(); + existingActClaimMap.put("sub", "previous-actor"); + existingActClaimMap.put("azp", "previous-azp"); + + return new Object[][]{ + // isDelegationRequest, actorSubject, actorAzp, existingActClaim, expectActClaim, expectAzp, + // expectNested + {true, "actor-subject", "actor-azp", null, true, true, false}, + {true, "actor-subject", null, null, true, false, false}, + {true, "actor-subject", "actor-azp", existingActClaimMap, true, true, true}, + {true, null, "actor-azp", null, false, false, false}, + {false, "actor-subject", "actor-azp", null, false, false, false}, + }; + } + + @Test(dataProvider = "delegationActClaimDataProvider") + public void testCreateJWTClaimSetDelegationActClaim(boolean isDelegationRequest, String actorSubject, + String actorAzp, Map existingActClaim, + boolean expectActClaim, boolean expectAzpInAct, + boolean expectNestedAct) throws Exception { + + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain( + MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + try (MockedStatic oAuth2Util = mockStatic(OAuth2Util.class); + MockedStatic identityTenantUtil = mockStatic(IdentityTenantUtil.class)) { + + identityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(anyString())).thenReturn(-1234); + OAuthAppDO appDO = spy(new OAuthAppDO()); + mockGrantHandlers(); + mockCustomClaimsCallbackHandler(); + oAuth2Util.when(() -> OAuth2Util.getAppInformationByClientId(anyString(), anyString())).thenReturn(appDO); + oAuth2Util.when(OAuth2Util::getIDTokenIssuer).thenReturn(ID_TOKEN_ISSUER); + oAuth2Util.when(() -> OAuth2Util.getIdTokenIssuer(anyString(), anyBoolean())).thenReturn(ID_TOKEN_ISSUER); + oAuth2Util.when(() -> OAuth2Util.getOIDCAudience(anyString(), any())) + .thenReturn(Collections.singletonList(DUMMY_CLIENT_ID)); + oAuth2Util.when(OAuth2Util::isTokenPersistenceEnabled).thenReturn(true); + oAuth2Util.when(OAuth2Util::isPairwiseSubEnabledForAccessTokens).thenReturn(false); + when(mockOAuthServerConfiguration.getSignatureAlgorithm()).thenReturn(SHA256_WITH_HMAC); + when(mockOAuthServerConfiguration.getUserAccessTokenValidityPeriodInSeconds()) + .thenReturn(DEFAULT_USER_ACCESS_TOKEN_EXPIRY_TIME); + when(mockOAuthServerConfiguration.getApplicationAccessTokenValidityPeriodInSeconds()) + .thenReturn(DEFAULT_APPLICATION_ACCESS_TOKEN_EXPIRY_TIME); + + AuthenticatedUser authenticatedUser = new AuthenticatedUser(); + authenticatedUser.setUserName("DUMMY_USERNAME"); + authenticatedUser.setTenantDomain(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + authenticatedUser.setUserStoreDomain("PRIMARY"); + authenticatedUser.setUserId(DUMMY_USER_ID); + authenticatedUser.setFederatedUser(false); + authenticatedUser.setAuthenticatedSubjectIdentifier("DUMMY_USERNAME"); + + OAuth2AccessTokenReqDTO tokenReqDTO = new OAuth2AccessTokenReqDTO(); + tokenReqDTO.setGrantType(APPLICATION_ACCESS_TOKEN_GRANT_TYPE); + tokenReqDTO.setTenantDomain(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + HttpServletRequestWrapper httpServletRequestWrapper = mock(HttpServletRequestWrapper.class); + when(httpServletRequestWrapper.getRequestURL()).thenReturn(new StringBuffer(DUMMY_TOKEN_ENDPOINT)); + tokenReqDTO.setHttpServletRequestWrapper(httpServletRequestWrapper); + + OAuthTokenReqMessageContext tokenReqMsgCtx = new OAuthTokenReqMessageContext(tokenReqDTO); + tokenReqMsgCtx.setAuthorizedUser(authenticatedUser); + tokenReqMsgCtx.addProperty(OAuthConstants.UserType.USER_TYPE, OAuthConstants.UserType.APPLICATION_USER); + tokenReqMsgCtx.setAudiences(Collections.singletonList(DUMMY_CLIENT_ID)); + + if (isDelegationRequest) { + tokenReqMsgCtx.addProperty(OAuthConstants.IS_DELEGATION_REQUEST, true); + } + if (actorSubject != null) { + tokenReqMsgCtx.addProperty(OAuthConstants.ACTOR_SUBJECT, actorSubject); + } + if (actorAzp != null) { + tokenReqMsgCtx.addProperty(OAuthConstants.ACTOR_AZP, actorAzp); + } + if (existingActClaim != null) { + tokenReqMsgCtx.addProperty(OAuthConstants.EXISTING_ACT_CLAIM, existingActClaim); + } + + JWTTokenIssuer jwtTokenIssuer = spy(new JWTTokenIssuer()); + JWTClaimsSet jwtClaimsSet = jwtTokenIssuer.createJWTClaimSet(null, tokenReqMsgCtx, DUMMY_CLIENT_ID); + + assertNotNull(jwtClaimsSet); + Object actClaimObj = jwtClaimsSet.getClaim("act"); + if (expectActClaim) { + assertNotNull(actClaimObj, "act claim should be present for delegation request"); + assertTrue(actClaimObj instanceof Map, "act claim should be a Map"); + Map actClaim = (Map) actClaimObj; + assertEquals(actClaim.get("sub"), actorSubject, "act.sub should match actor subject"); + if (expectAzpInAct) { + assertEquals(actClaim.get("azp"), actorAzp, "act.azp should match actor azp"); + } else { + assertNull(actClaim.get("azp"), "act.azp should not be present when not provided"); + } + if (expectNestedAct) { + assertNotNull(actClaim.get("act"), "Nested act claim should be present for chained delegation"); + } else { + assertNull(actClaim.get("act"), "No nested act claim expected"); + } + } else { + assertNull(actClaimObj, "act claim should NOT be present"); + } + } + } + @Test(dataProvider = "createJWTClaimSetDataProvider") public void testSignJWTWithRSA(Object authzReqMessageContext, Object tokenReqMessageContext, diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AbstractAuthorizationGrantHandlerTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AbstractAuthorizationGrantHandlerTest.java index 6a14f290ab..9b3fbfb28a 100644 --- a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AbstractAuthorizationGrantHandlerTest.java +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AbstractAuthorizationGrantHandlerTest.java @@ -63,6 +63,7 @@ import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenRespDTO; import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; +import org.wso2.carbon.identity.oauth2.model.AccessTokenExtendedAttributes; import org.wso2.carbon.identity.oauth2.token.JWTTokenIssuer; import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; import org.wso2.carbon.identity.oauth2.token.OauthTokenIssuer; @@ -72,6 +73,7 @@ import org.wso2.carbon.identity.oauth2.util.OAuth2Util; import org.wso2.carbon.identity.oauth2.validators.OAuth2ScopeHandler; +import java.lang.reflect.Method; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -566,6 +568,36 @@ public void testIssueExistingAccessTokensWithoutConsent() throws Exception { } } + /** + * Verifies that when isDelegationRequest is true and DELEGATING_ACTOR is set on the context, + * getAccessTokenExtendedAttributes persists the DELEGATING_ACTOR in the extended attributes. + */ + @Test + public void testGetAccessTokenExtendedAttributesWithDelegatingActor() throws Exception { + + OAuth2AccessTokenReqDTO reqDTO = new OAuth2AccessTokenReqDTO(); + reqDTO.setClientId(clientId); + OAuthTokenReqMessageContext tokReqMsgCtx = new OAuthTokenReqMessageContext(reqDTO); + tokReqMsgCtx.setDelegationRequest(true); + tokReqMsgCtx.addProperty(OAuthConstants.DELEGATING_ACTOR, "delegating-actor-id"); + + Method method = AbstractAuthorizationGrantHandler.class.getDeclaredMethod( + "getAccessTokenExtendedAttributes", + AccessTokenExtendedAttributes.class, + OAuthTokenReqMessageContext.class); + method.setAccessible(true); + + AccessTokenExtendedAttributes result = + (AccessTokenExtendedAttributes) method.invoke(handler, null, tokReqMsgCtx); + + assertNotNull(result, "Extended attributes should not be null for a delegation request"); + assertNotNull(result.getParameters(), "Parameters map should not be null"); + assertTrue(result.getParameters().containsKey(OAuthConstants.DELEGATING_ACTOR), + "DELEGATING_ACTOR key should be present in extended attributes"); + assertEquals(result.getParameters().get(OAuthConstants.DELEGATING_ACTOR), "delegating-actor-id", + "DELEGATING_ACTOR value should match the property set on the token request context"); + } + private static class MockAuthzGrantHandler extends AbstractAuthorizationGrantHandler { } diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java index 7d552263ce..47aa3ddb75 100644 --- a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java @@ -57,6 +57,7 @@ import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenRespDTO; import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; +import org.wso2.carbon.identity.oauth2.model.AccessTokenExtendedAttributes; import org.wso2.carbon.identity.oauth2.model.RefreshTokenValidationDataDO; import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; import org.wso2.carbon.identity.oauth2.token.AccessTokenIssuer; @@ -72,6 +73,8 @@ import java.sql.Timestamp; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; @@ -495,4 +498,48 @@ public void testIssueToken() throws IdentityOAuth2Exception, OAuthSystemExceptio } } + + @Test + public void testPropagateActorInfoForDelegation() throws Exception { + + String delegatingActorId = "delegating-actor-id-123"; + MockAuthenticatedUser user = new MockAuthenticatedUser("test_user"); + + Map params = new HashMap<>(); + params.put(OAuthConstants.DELEGATING_ACTOR, delegatingActorId); + AccessTokenExtendedAttributes attrs = mock(AccessTokenExtendedAttributes.class); + when(attrs.getParameters()).thenReturn(params); + + when(refreshTokenGrantProcessor.validateRefreshToken(any())).thenReturn(refreshTokenValidationDataDO); + when(refreshTokenValidationDataDO.getAuthorizedUser()).thenReturn(user); + when(refreshTokenGrantProcessor.isLatestRefreshToken(any(), any(), any())).thenReturn(true); + when(oAuthServerConfiguration.isValidateAuthenticatedUserForRefreshGrantEnabled()).thenReturn(false); + when(oAuth2ServiceComponentHolder.getRefreshTokenGrantProcessor()).thenReturn(refreshTokenGrantProcessor); + when(oAuth2ServiceComponentHolder.getAuthorizationDetailsService()).thenReturn(authorizationDetailsService); + when(oAuthTokenReqMessageContext.getOauth2AccessTokenReqDTO()).thenReturn(oAuth2AccessTokenReqDTO); + when(oAuth2AccessTokenReqDTO.getAccessTokenExtendedAttributes()).thenReturn(attrs); + + try (MockedStatic oAuthServerConfigurationMockedStatic = mockStatic( + OAuthServerConfiguration.class); + MockedStatic oAuth2ServiceComponentHolderMockedStatic = mockStatic( + OAuth2ServiceComponentHolder.class); + MockedStatic frameworkUtilsMockedStatic = mockStatic(FrameworkUtils.class); + MockedStatic oAuth2Util = mockStatic(OAuth2Util.class)) { + + oAuth2Util.when(() -> OAuth2Util.getTenantId(anyString())).thenReturn(TENANT_ID); + oAuthServerConfigurationMockedStatic.when(OAuthServerConfiguration::getInstance) + .thenReturn(oAuthServerConfiguration); + oAuth2ServiceComponentHolderMockedStatic.when(OAuth2ServiceComponentHolder::getInstance) + .thenReturn(oAuth2ServiceComponentHolder); + frameworkUtilsMockedStatic.when(FrameworkUtils::getFederatedAssociationManager) + .thenReturn(mock(FederatedAssociationManager.class)); + + RefreshGrantHandler refreshGrantHandler = new RefreshGrantHandler(); + refreshGrantHandler.init(); + refreshGrantHandler.validateGrant(oAuthTokenReqMessageContext); + + verify(oAuthTokenReqMessageContext).setDelegationRequest(true); + verify(oAuthTokenReqMessageContext).addProperty(OAuthConstants.DELEGATING_ACTOR, delegatingActorId); + } + } }