Skip to content

Commit

Permalink
Merge pull request #12247 from Akila94/resource-access-validation-fea…
Browse files Browse the repository at this point in the history
…ture

Resource access validation feature
  • Loading branch information
npamudika authored Feb 15, 2024
2 parents c37f920 + 4662eca commit a142c93
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class APIMgtGatewayConstants {
public static final String REVOKED_ACCESS_TOKEN = "RevokedAccessToken";
public static final String DEACTIVATED_ACCESS_TOKEN = "DeactivatedAccessToken";
public static final String SCOPES = "Scopes";
public static final String JWT_CLAIMS = "jwt_token_claims";
public static final String REQUEST_EXECUTION_START_TIME = "request.execution.start.time";
public static final String SYNAPSE_ENDPOINT_ADDRESS = "ENDPOINT_ADDRESS";
public static final String DUMMY_ENDPOINT_ADDRESS = "dummy_endpoint_address";
Expand Down Expand Up @@ -190,5 +191,8 @@ public class APIMgtGatewayConstants {

//This will be a reserved name for the synapse message context properties.
public static final String ADDITIONAL_ANALYTICS_PROPS = "ADDITIONAL_ANALYTICS_PROPS_TO_PUBLISH";
public static final String ACCESS_GRANT_CLAIM_NAME = "grantVerificationClaim";
public static final String ACCESS_GRANT_CLAIM_VALUE = "grantVerificationClaimValue";
public static final String SHOULD_ALLOW_ACCESS_VALIDATION = "shouldAllowValidation";
}

Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ public AuthenticationContext authenticate(SignedJWTInfo signedJWTInfo, MessageCo
// Validate scopes
validateScopes(apiContext, apiVersion, matchingResource, httpMethod, jwtValidationInfo, signedJWTInfo);
synCtx.setProperty(APIMgtGatewayConstants.SCOPES, jwtValidationInfo.getScopes().toString());
synCtx.setProperty(APIMgtGatewayConstants.JWT_CLAIMS, jwtValidationInfo.getClaims());
if (apiKeyValidationInfoDTO.isAuthorized()) {
/*
* Set api.ut.apiPublisher of the subscribed api to the message context.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com) All Rights Reserved.
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.wso2.carbon.apimgt.gateway.mediators;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.Mediator;
import org.apache.synapse.MessageContext;
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.mediators.AbstractMediator;
import org.wso2.carbon.apimgt.gateway.APIMgtGatewayConstants;
import org.wso2.carbon.apimgt.gateway.handlers.Utils;
import org.wso2.carbon.apimgt.gateway.handlers.security.APISecurityConstants;

import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* This mediator checks for a value of a pre-configured claim which should be sent in the
* JWT access token. This claim value is also configured in the policy which is uploaded to each API resource
* in the API Manager publisher. For the mediator to return true, the claim name and the claim value sent in the JWT
* access token must be identical to the configured claim name and the value. There is also an option to configure a
* so the claim values will be matched against that regex. If one of those are not identical,
* the mediator will return false. The value sent in the token claim must be equal or match with the optionally
* provided regex. If the "shouldAllowValidation" is true, the flow will be allowed if the claim values are matched. If
* it is false, the flow will be allowed if the claim values are not matched.
*/
public class ClaimBasedResourceAccessValidationMediator extends AbstractMediator {

private static final Log log = LogFactory.getLog(ClaimBasedResourceAccessValidationMediator.class);
private String accessVerificationClaim;
private String accessVerificationClaimValue;
private String accessVerificationClaimValueRegex;
private boolean shouldAllowValidation;
public static final String CLAIMS_MISMATCH_ERROR_MSG = "Configured claim and claim " +
"sent in token do not match.";

@Override
public boolean mediate(MessageContext messageContext) {

String claimValueSentInToken;
Map<String, String> jwtTokenClaims = (Map<String, String>) messageContext
.getProperty(APIMgtGatewayConstants.JWT_CLAIMS);

claimValueSentInToken = jwtTokenClaims.get(accessVerificationClaim);

if (StringUtils.isBlank(claimValueSentInToken)) {
log.error("The configured resource access validation claim is " +
"not present in the token.");
handleFailure(HttpStatus.SC_FORBIDDEN, messageContext, String.format("Token doesn't contain the " +
"claim \"%s\"", accessVerificationClaim), null);
return false;
}

if (StringUtils.isNotBlank(accessVerificationClaimValueRegex)) {
log.debug("A regex is provided, hence, validating the claim values using the provided regex.");
Pattern pattern = Pattern.compile(accessVerificationClaimValueRegex);
Matcher configuredClaimValueMatcher = pattern.matcher(accessVerificationClaimValue);
Matcher tokenSentClaimValueMatcher = pattern.matcher(claimValueSentInToken);

if ((configuredClaimValueMatcher.matches() && tokenSentClaimValueMatcher.matches())
|| shouldAllowValidation) {
log.debug("Claim values match or the flow is configured to allow when claims doesn't match. " +
"Hence the flow is allowed.");
return true;
} else {
log.debug("Claim values don't match. Hence the flow is not allowed.");
handleFailure(HttpStatus.SC_FORBIDDEN, messageContext, CLAIMS_MISMATCH_ERROR_MSG, null);
return false;
}
} else {
log.debug("A regex is not provided, validating the claim values based on equality.");
if ((StringUtils.equals(accessVerificationClaimValue, claimValueSentInToken)) || shouldAllowValidation) {
log.debug("Claim values match or the flow is configured to allow when claims doesn't match. " +
"Hence the flow is allowed.");
return true;
} else {
log.debug("Claim values don't match. Hence the flow is not allowed.");
handleFailure(HttpStatus.SC_FORBIDDEN, messageContext, CLAIMS_MISMATCH_ERROR_MSG, null);
return false;
}
}
}

/**
* Sends a fault response to the client.
*
* @param errorCode error code of the failure
* @param messageContext message context of the request
* @param errorMessage error message of the failure
* @param errorDescription error description of the failure
*/
private void handleFailure(int errorCode, MessageContext messageContext,
String errorMessage, String errorDescription) {

messageContext.setProperty(SynapseConstants.ERROR_CODE, errorCode);
messageContext.setProperty(SynapseConstants.ERROR_MESSAGE, errorMessage);
messageContext.setProperty(SynapseConstants.ERROR_DETAIL, errorDescription);
Mediator sequence = messageContext.getSequence(APISecurityConstants.BACKEND_AUTH_FAILURE_HANDLER);
if (sequence != null && !sequence.mediate(messageContext)) {
return;
}
Utils.sendFault(messageContext, errorCode);
}

public void setAccessVerificationClaim(String accessVerificationClaim) {
this.accessVerificationClaim = accessVerificationClaim;
}

public void setAccessVerificationClaimValue(String accessVerificationClaimValue) {
this.accessVerificationClaimValue = accessVerificationClaimValue;
}

public void setShouldAllowValidation(boolean shouldAllowValidation) {
this.shouldAllowValidation = shouldAllowValidation;
}

public void setAccessVerificationClaimValueRegex(String accessVerificationClaimValueRegex) {
this.accessVerificationClaimValueRegex = accessVerificationClaimValueRegex;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com) All Rights Reserved.
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.wso2.carbon.apimgt.gateway.mediators;

import org.apache.synapse.MessageContext;
import org.apache.synapse.core.axis2.Axis2MessageContext;
import org.junit.Test;
import org.mockito.Mockito;
import org.testng.Assert;
import org.wso2.carbon.apimgt.gateway.APIMgtGatewayConstants;

import java.util.HashMap;
import java.util.Map;

public class ClaimBasedResourceAccessValidationMediatorTest {

MessageContext messageContext = Mockito.mock(Axis2MessageContext.class);
private final ClaimBasedResourceAccessValidationMediator mediator
= Mockito.spy(new ClaimBasedResourceAccessValidationMediator());
public static final String APPLICATION = "APPLICATION";
public static final String APPLICATION_USER = "APPLICATION_USER";
public static final String AUT = "aut";

@Test
public void testFlowWhenClaimsMatchingWithRegex() throws Exception {

Map<String, String> jwtTokenClaimsMap = new HashMap<>();
jwtTokenClaimsMap.put(AUT, APPLICATION);
Mockito.when(messageContext.getProperty(APIMgtGatewayConstants.JWT_CLAIMS))
.thenReturn(jwtTokenClaimsMap);
String claimValueRegex = "^[A-Za-z]+$";
mediator.setAccessVerificationClaim(AUT);
mediator.setAccessVerificationClaimValue(APPLICATION);
mediator.setAccessVerificationClaimValueRegex(claimValueRegex);
mediator.setShouldAllowValidation(true);
Assert.assertTrue(mediator.mediate(messageContext));
}

@Test
public void testFlowWhenClaimsMatchingWithTickFalseWithRegex() throws Exception {

Map<String, String> jwtTokenClaimsMap = new HashMap<>();
jwtTokenClaimsMap.put(AUT, APPLICATION);
Mockito.when(messageContext.getProperty(APIMgtGatewayConstants.JWT_CLAIMS))
.thenReturn(jwtTokenClaimsMap);
String claimValueRegex = "^[A-Za-z]+$";
mediator.setAccessVerificationClaim(AUT);
mediator.setAccessVerificationClaimValue(APPLICATION);
mediator.setAccessVerificationClaimValueRegex(claimValueRegex);
mediator.setShouldAllowValidation(false);
Assert.assertTrue(mediator.mediate(messageContext));
}

@Test
public void testFlowWhenClaimsNotMatchingWithTickTrueWithRegex() throws Exception {

Map<String, String> jwtTokenClaimsMap = new HashMap<>();
jwtTokenClaimsMap.put(AUT, APPLICATION);
Mockito.when(messageContext.getProperty(APIMgtGatewayConstants.JWT_CLAIMS))
.thenReturn(jwtTokenClaimsMap);
String claimValueRegex = "^[A-Za-z]+$";
mediator.setAccessVerificationClaim(AUT);
mediator.setAccessVerificationClaimValue(APPLICATION_USER);
mediator.setAccessVerificationClaimValueRegex(claimValueRegex);
mediator.setShouldAllowValidation(true);
Assert.assertTrue(mediator.mediate(messageContext));
}

@Test
public void testFlowWhenClaimsMatchingWithTickTrueWithoutRegex() throws Exception {

Map<String, String> jwtTokenClaimsMap = new HashMap<>();
jwtTokenClaimsMap.put(AUT, APPLICATION);
Mockito.when(messageContext.getProperty(APIMgtGatewayConstants.JWT_CLAIMS))
.thenReturn(jwtTokenClaimsMap);
String claimValueRegex = "";
mediator.setAccessVerificationClaim(AUT);
mediator.setAccessVerificationClaimValue(APPLICATION);
mediator.setAccessVerificationClaimValueRegex(claimValueRegex);
mediator.setShouldAllowValidation(true);
Assert.assertTrue(mediator.mediate(messageContext));
}

@Test
public void testFlowWhenClaimsMatchingWithTickFalseWithoutRegex() throws Exception {

Map<String, String> jwtTokenClaimsMap = new HashMap<>();
jwtTokenClaimsMap.put(AUT, APPLICATION);
Mockito.when(messageContext.getProperty(APIMgtGatewayConstants.JWT_CLAIMS))
.thenReturn(jwtTokenClaimsMap);
String claimValueRegex = "";
mediator.setAccessVerificationClaim(AUT);
mediator.setAccessVerificationClaimValue(APPLICATION);
mediator.setAccessVerificationClaimValueRegex(claimValueRegex);
mediator.setShouldAllowValidation(false);
Assert.assertTrue(mediator.mediate(messageContext));
}

@Test
public void testFlowWhenClaimsNotMatchingWithTickTrueWithoutRegex() throws Exception {

Map<String, String> jwtTokenClaimsMap = new HashMap<>();
jwtTokenClaimsMap.put(AUT, APPLICATION);
Mockito.when(messageContext.getProperty(APIMgtGatewayConstants.JWT_CLAIMS))
.thenReturn(jwtTokenClaimsMap);
String claimValueRegex = "";
mediator.setAccessVerificationClaim(AUT);
mediator.setAccessVerificationClaimValue(APPLICATION_USER);
mediator.setAccessVerificationClaimValueRegex(claimValueRegex);
mediator.setShouldAllowValidation(true);
Assert.assertTrue(mediator.mediate(messageContext));
}
}

0 comments on commit a142c93

Please sign in to comment.