Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -64,6 +64,9 @@ public static class TokenExchangeConstants {
public static final String ORG_ID = "org_id";
public static final String SUB = "sub";
public static final String SCOPE = "scope";
public static final String AZP = "azp";
public static final String CLIENT_ID = "client_id";
public static final String ACT = "act";

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
import static org.wso2.carbon.identity.oauth2.grant.token.exchange.Constants.TokenExchangeConstants.MAY_ACT;
import static org.wso2.carbon.identity.oauth2.grant.token.exchange.Constants.TokenExchangeConstants.SUB;
import static org.wso2.carbon.identity.oauth2.grant.token.exchange.Constants.TokenExchangeConstants.USER_ORG;
import static org.wso2.carbon.identity.oauth2.grant.token.exchange.Constants.TokenExchangeConstants.ACT;
import static org.wso2.carbon.identity.oauth2.grant.token.exchange.Constants.TokenExchangeConstants.SUBJECT_TOKEN;
import static org.wso2.carbon.identity.oauth2.grant.token.exchange.utils.TokenExchangeUtils.checkExpirationTime;
import static org.wso2.carbon.identity.oauth2.grant.token.exchange.utils.TokenExchangeUtils.checkNotBeforeTime;
import static org.wso2.carbon.identity.oauth2.grant.token.exchange.utils.TokenExchangeUtils.getClaimSet;
Expand Down Expand Up @@ -135,6 +137,32 @@ public boolean validateGrant(OAuthTokenReqMessageContext tokReqMsgCtx) throws Id
}

String tenantDomain = getTenantDomain(tokReqMsgCtx);
SignedJWT subjectSignedJWT = getSignedJWT(requestParams.get(SUBJECT_TOKEN));
JWTClaimsSet subjectClaimsSet = (subjectSignedJWT != null) ? getClaimSet(subjectSignedJWT) : null;

// Check for self-delegation first (application exchanging its own token without actor)
if (isSelfDelegationRequest(requestParams, tokReqMsgCtx, subjectClaimsSet)) {
validateSubjectTokenForSelfDelegation(tokReqMsgCtx, requestParams, tenantDomain, subjectSignedJWT, subjectClaimsSet);

Object existingActClaim = subjectClaimsSet.getClaim(ACT);

if (existingActClaim != null) {
// REMOVED: Authorization check - not needed for self-delegation
// In self-delegation, the token holder is refreshing their own token
// and should preserve the existing delegation chain

// Preserve existing act claim for self-delegation
tokReqMsgCtx.addProperty("IS_SELF_DELEGATION_WITH_ACT", true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

move the key to constant for best practise

tokReqMsgCtx.addProperty("EXISTING_ACT_CLAIM", existingActClaim);
}

// No need to validate actor token in self-delegation
// Set impersonation flag to false for self-delegation
tokReqMsgCtx.setImpersonationRequest(false);
setSubjectAsAuthorizedUser(tokReqMsgCtx, requestParams, tenantDomain);
return true;
}

if (isImpersonationRequest(requestParams)) {
validateSubjectToken(tokReqMsgCtx, requestParams, tenantDomain);
validateActorToken(tokReqMsgCtx, requestParams, tenantDomain);
Expand All @@ -147,7 +175,7 @@ public boolean validateGrant(OAuthTokenReqMessageContext tokReqMsgCtx) throws Id

if (Constants.TokenExchangeConstants.JWT_TOKEN_TYPE.equals(subjectTokenType) || (Constants
.TokenExchangeConstants.ACCESS_TOKEN_TYPE.equals(subjectTokenType)) && isJWT(requestParams
.get(Constants.TokenExchangeConstants.SUBJECT_TOKEN))) {
.get(SUBJECT_TOKEN))) {
handleJWTSubjectToken(requestParams, tokReqMsgCtx, tenantDomain, requestedAudience);
if (tokReqMsgCtx.getAuthorizedUser() != null && !tokReqMsgCtx.getAuthorizedUser().isFederatedUser()) {
validateLocalUser(tokReqMsgCtx, requestParams);
Expand All @@ -168,7 +196,7 @@ public boolean validateGrant(OAuthTokenReqMessageContext tokReqMsgCtx) throws Id
private boolean isImpersonationRequest(Map<String, String> requestParams) {

// Check if all required parameters are present
return requestParams.containsKey(TokenExchangeConstants.SUBJECT_TOKEN)
return requestParams.containsKey(SUBJECT_TOKEN)
&& requestParams.containsKey(TokenExchangeConstants.SUBJECT_TOKEN_TYPE)
&& requestParams.containsKey(TokenExchangeConstants.ACTOR_TOKEN)
&& requestParams.containsKey(TokenExchangeConstants.ACTOR_TOKEN_TYPE);
Expand All @@ -189,7 +217,7 @@ private void validateSubjectToken(OAuthTokenReqMessageContext tokReqMsgCtx, Map<
throws IdentityOAuth2Exception {

// Retrieve the signed JWT object from the request parameters
SignedJWT signedJWT = getSignedJWT(requestParams.get(TokenExchangeConstants.SUBJECT_TOKEN));
SignedJWT signedJWT = getSignedJWT(requestParams.get(SUBJECT_TOKEN));
if (signedJWT == null) {
// If no valid subject token found, handle the exception
handleException(OAuth2ErrorCodes.INVALID_REQUEST,
Expand Down Expand Up @@ -242,6 +270,139 @@ private void validateSubjectToken(OAuthTokenReqMessageContext tokReqMsgCtx, Map<
tokReqMsgCtx.setScope(getScopes(claimsSet, tokReqMsgCtx));
}

/**
* Validates the subject token for self-delegation scenarios where an
* application
* is exchanging a token issued to itself. Unlike impersonation, self-delegation
* does not require a may_act claim.
*
* @param tokReqMsgCtx OauthTokenReqMessageContext
* @param requestParams A Map<String, String> containing the request parameters.
* @param tenantDomain The tenant domain associated with the request.
* @throws IdentityOAuth2Exception If there's an error during token validation.
*/
private void validateSubjectTokenForSelfDelegation(OAuthTokenReqMessageContext tokReqMsgCtx, Map<String, String> requestParams, String tenantDomain, SignedJWT signedJWT,JWTClaimsSet claimsSet )
throws IdentityOAuth2Exception {

String subject = resolveSubject(claimsSet);
validateMandatoryClaims(claimsSet, subject);

// NOTE: We SKIP impersonator validation for self-delegation
// In self-delegation, there is no may_act claim because the application
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

where are we skipping this impersonator validation for self-delegation. Couldn't find the logic for that

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

as we discussed we consider that there is no maay_act claim in delegation or self delegation. may_act is only in impersonation

// is exchanging its own token, not acting on behalf of another party

// Get JWT issuer
String jwtIssuer = claimsSet.getIssuer();

// Get identity provider
IdentityProvider identityProvider = getIdentityProvider(tokReqMsgCtx, jwtIssuer, tenantDomain);

// Validate signature
try {
if (validateSignature(signedJWT, identityProvider, tenantDomain)) {
log.debug("Signature/MAC validated successfully for subject token.");
} else {
handleException(OAuth2ErrorCodes.INVALID_REQUEST, "Signature or Message Authentication "
+ "invalid for subject token.");
}
} catch (JOSEException e) {
handleException(OAuth2ErrorCodes.INVALID_REQUEST, "Error when verifying signature for subject token ", e);
}

// Check JWT validity (expiration time, not before time, issued at time)
checkJWTValidity(claimsSet);

// Validate the audience of the subject token
List<String> audiences = claimsSet.getAudience();
if (audiences == null || audiences.isEmpty()) {
TokenExchangeUtils.handleClientException(TokenExchangeConstants.INVALID_TARGET,
"Audience is empty in the subject token.");
}

// Check if issuer is in the audience list
String idpIssuerName = OAuth2Util.getIssuerLocation(tenantDomain);
boolean issuerInAudience = audiences.contains(idpIssuerName);

if (!issuerInAudience) {
// Fallback: Check if the issuer alias value is present in audience
String idpAlias = getIDPAlias(identityProvider, tenantDomain);
if (StringUtils.isNotEmpty(idpAlias)) {
issuerInAudience = audiences.stream().anyMatch(aud -> aud.equals(idpAlias));
}

// If still not found in audience, validate the iss claim as fallback
if (!issuerInAudience) {
if (log.isDebugEnabled()) {
log.debug("Issuer not found in audience list. Validating iss claim as fallback.");
}
validateTokenIssuer(jwtIssuer, tenantDomain);
}
}

// Validate that requesting client is in the audience list
if (!validateSubjectTokenAudience(audiences, tokReqMsgCtx)) {
TokenExchangeUtils.handleClientException(TokenExchangeConstants.INVALID_TARGET,
"Requesting client not found in audience list for subject token.");
}

// Set subject property in context
tokReqMsgCtx.addProperty(IMPERSONATED_SUBJECT, subject);

// Set scopes
tokReqMsgCtx.setScope(getScopes(claimsSet, tokReqMsgCtx));
}


/**
* Checks if the token request is a self-delegation request where an application
* is exchanging a token issued to itself, without requiring an actor token.
*
* @param requestParams A Map<String, String> containing the request parameters.
* @param tokReqMsgCtx OAuthTokenReqMessageContext
* @return true if the request is a self-delegation request, false otherwise.
*/
private boolean isSelfDelegationRequest(Map<String, String> requestParams, OAuthTokenReqMessageContext tokReqMsgCtx, JWTClaimsSet subjectClaimsSet) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

formatting issue

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

solved in pr #55


// Must have subject_token and subject_token_type
if (!requestParams.containsKey(SUBJECT_TOKEN) ||
!requestParams.containsKey(TokenExchangeConstants.SUBJECT_TOKEN_TYPE)) {
return false;
}

// Must NOT have actor_token (self-delegation doesn't need actor)
if (requestParams.containsKey(TokenExchangeConstants.ACTOR_TOKEN)) {
return false;
}

if (!requestParams.containsKey(TokenExchangeConstants.SUBJECT_TOKEN) ||
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this is already captured in line 366. Redundant line

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

will solve in pr #55

!requestParams.containsKey(TokenExchangeConstants.SUBJECT_TOKEN_TYPE)) {
return false;
}
if (requestParams.containsKey(TokenExchangeConstants.ACTOR_TOKEN)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Handled in 372. Redundant lines

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

will handle in pr #55

return false;
}

if (subjectClaimsSet == null) {
return false;
}

String requestingClientId = tokReqMsgCtx.getOauth2AccessTokenReqDTO().getClientId();

// Check if the azp (authorized party) claim matches the requesting client
Object azpClaim = subjectClaimsSet.getClaim(TokenExchangeConstants.AZP);
if (azpClaim != null && azpClaim.toString().equals(requestingClientId)) {
return true;
}

// Check if the client_id claim matches the requesting client
Object clientIdClaim = subjectClaimsSet.getClaim(TokenExchangeConstants.CLIENT_ID);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

azp is an optional claim. Hence we should be able to stick to client_id claim only. Do you have any reasons to fallback to azp and client_id?

if (clientIdClaim != null && clientIdClaim.toString().equals(requestingClientId)) {
return true;
}

return false;
}

/**
* Retrieves the scopes claim from the JWTClaimsSet object and splits it into an array of individual scope strings.
* Assumes that the scopes claim is represented as a space-delimited string.
Expand Down Expand Up @@ -376,7 +537,7 @@ private void setSubjectAsAuthorizedUser(OAuthTokenReqMessageContext tokReqMsgCtx
Map<String, String> requestParams,
String tenantDomain) throws IdentityOAuth2Exception {

SignedJWT signedJWT = getSignedJWT(requestParams.get(TokenExchangeConstants.SUBJECT_TOKEN));
SignedJWT signedJWT = getSignedJWT(requestParams.get(SUBJECT_TOKEN));

JWTClaimsSet claimsSet = getClaimSet(signedJWT);
String jwtIssuer = claimsSet.getIssuer();
Expand Down Expand Up @@ -654,7 +815,7 @@ private void handleJWTSubjectToken(Map<String, String> requestParams, OAuthToken
JWTClaimsSet claimsSet;
boolean audienceFound;

signedJWT = getSignedJWT(requestParams.get(Constants.TokenExchangeConstants.SUBJECT_TOKEN));
signedJWT = getSignedJWT(requestParams.get(SUBJECT_TOKEN));
if (signedJWT != null) {
claimsSet = getClaimSet(signedJWT);
if (claimsSet == null) {
Expand Down
Loading