-
Notifications
You must be signed in to change notification settings - Fork 54
Implement the self delegation flow with azp claim inside the act/sub claim #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e1da2e5
10feaf9
2936336
69055df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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); | ||
| tokReqMsgCtx.addProperty("EXISTING_ACT_CLAIM", existingActClaim); | ||
| } | ||
Bin4yi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // 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); | ||
|
|
@@ -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); | ||
|
|
@@ -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); | ||
|
|
@@ -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, | ||
|
|
@@ -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 ) | ||
Bin4yi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| throws IdentityOAuth2Exception { | ||
|
|
||
| String subject = resolveSubject(claimsSet); | ||
Bin4yi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| validateMandatoryClaims(claimsSet, subject); | ||
|
|
||
| // NOTE: We SKIP impersonator validation for self-delegation | ||
| // In self-delegation, there is no may_act claim because the application | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where are we skipping this
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. formatting issue
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) || | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is already captured in line 366. Redundant line
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handled in 372. Redundant lines
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -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(); | ||
|
|
@@ -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) { | ||
|
|
||
There was a problem hiding this comment.
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