diff --git a/components/org.wso2.carbon.identity.api.user.recovery/src/gen/java/org/wso2/carbon/identity/recovery/endpoint/dto/ReCaptchaPropertiesDTO.java b/components/org.wso2.carbon.identity.api.user.recovery/src/gen/java/org/wso2/carbon/identity/recovery/endpoint/dto/ReCaptchaPropertiesDTO.java
index 1e3ca858cd..7172a0ab70 100644
--- a/components/org.wso2.carbon.identity.api.user.recovery/src/gen/java/org/wso2/carbon/identity/recovery/endpoint/dto/ReCaptchaPropertiesDTO.java
+++ b/components/org.wso2.carbon.identity.api.user.recovery/src/gen/java/org/wso2/carbon/identity/recovery/endpoint/dto/ReCaptchaPropertiesDTO.java
@@ -10,18 +10,21 @@
@ApiModel(description = "")
public class ReCaptchaPropertiesDTO {
-
-
-
+
+
+
private Boolean reCaptchaEnabled = null;
-
-
+
+
+ private String reCaptchaType = null;
+
+
private String reCaptchaKey = null;
-
-
+
+
private String reCaptchaAPI = null;
-
+
/**
**/
@ApiModelProperty(value = "")
@@ -33,7 +36,19 @@ public void setReCaptchaEnabled(Boolean reCaptchaEnabled) {
this.reCaptchaEnabled = reCaptchaEnabled;
}
-
+
+ /**
+ **/
+ @ApiModelProperty(value = "")
+ @JsonProperty("reCaptchaType")
+ public String getReCaptchaType() {
+ return reCaptchaType;
+ }
+ public void setReCaptchaType(String reCaptchaType) {
+ this.reCaptchaType = reCaptchaType;
+ }
+
+
/**
**/
@ApiModelProperty(value = "")
@@ -45,7 +60,7 @@ public void setReCaptchaKey(String reCaptchaKey) {
this.reCaptchaKey = reCaptchaKey;
}
-
+
/**
**/
@ApiModelProperty(value = "")
@@ -57,14 +72,15 @@ public void setReCaptchaAPI(String reCaptchaAPI) {
this.reCaptchaAPI = reCaptchaAPI;
}
-
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class ReCaptchaPropertiesDTO {\n");
-
+
sb.append(" reCaptchaEnabled: ").append(reCaptchaEnabled).append("\n");
+ sb.append(" reCaptchaType: ").append(reCaptchaType).append("\n");
sb.append(" reCaptchaKey: ").append(reCaptchaKey).append("\n");
sb.append(" reCaptchaAPI: ").append(reCaptchaAPI).append("\n");
sb.append("}\n");
diff --git a/components/org.wso2.carbon.identity.api.user.recovery/src/main/java/org/wso2/carbon/identity/recovery/endpoint/Utils/RecoveryUtil.java b/components/org.wso2.carbon.identity.api.user.recovery/src/main/java/org/wso2/carbon/identity/recovery/endpoint/Utils/RecoveryUtil.java
index fd90f2821d..2b3569a29a 100644
--- a/components/org.wso2.carbon.identity.api.user.recovery/src/main/java/org/wso2/carbon/identity/recovery/endpoint/Utils/RecoveryUtil.java
+++ b/components/org.wso2.carbon.identity.api.user.recovery/src/main/java/org/wso2/carbon/identity/recovery/endpoint/Utils/RecoveryUtil.java
@@ -452,6 +452,7 @@ public static Properties getValidatedCaptchaConfigs() {
private static Properties validateCaptchaConfigs(Properties properties) {
boolean reCaptchaEnabled = Boolean.valueOf(properties.getProperty(CaptchaConstants.RE_CAPTCHA_ENABLED));
+ String reCaptchaType = properties.getProperty(CaptchaConstants.RE_CAPTCHA_TYPE);
if (reCaptchaEnabled && StringUtils.isBlank(properties.getProperty(CaptchaConstants.RE_CAPTCHA_SITE_KEY))) {
RecoveryUtil.handleBadRequest(String.format("%s is not found ", CaptchaConstants.RE_CAPTCHA_SITE_KEY),
@@ -469,6 +470,11 @@ private static Properties validateCaptchaConfigs(Properties properties) {
RecoveryUtil.handleBadRequest(String.format("%s is not found ", CaptchaConstants.RE_CAPTCHA_VERIFY_URL),
Constants.STATUS_INTERNAL_SERVER_ERROR_MESSAGE_DEFAULT);
}
+ if (CaptchaConstants.RE_CAPTCHA_TYPE_ENTERPRISE.equals(reCaptchaType) &&
+ StringUtils.isBlank(properties.getProperty(CaptchaConstants.RE_CAPTCHA_PROJECT_ID))) {
+ RecoveryUtil.handleBadRequest(String.format("%s is not found ", CaptchaConstants
+ .RE_CAPTCHA_PROJECT_ID), Constants.STATUS_INTERNAL_SERVER_ERROR_MESSAGE_DEFAULT);
+ }
return properties;
}
diff --git a/components/org.wso2.carbon.identity.api.user.recovery/src/main/java/org/wso2/carbon/identity/recovery/endpoint/impl/CaptchaApiServiceImpl.java b/components/org.wso2.carbon.identity.api.user.recovery/src/main/java/org/wso2/carbon/identity/recovery/endpoint/impl/CaptchaApiServiceImpl.java
index 3171ccb798..50d928c8ba 100644
--- a/components/org.wso2.carbon.identity.api.user.recovery/src/main/java/org/wso2/carbon/identity/recovery/endpoint/impl/CaptchaApiServiceImpl.java
+++ b/components/org.wso2.carbon.identity.api.user.recovery/src/main/java/org/wso2/carbon/identity/recovery/endpoint/impl/CaptchaApiServiceImpl.java
@@ -20,6 +20,7 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
@@ -42,7 +43,6 @@
*/
public class CaptchaApiServiceImpl extends CaptchaApiService {
- private static final String SUCCESS = "success";
private static final Log log = LogFactory.getLog(CaptchaApiServiceImpl.class);
private final String RECAPTCHA = "ReCaptcha";
@@ -64,11 +64,11 @@ public Response getCaptcha(String captchaType, String recoveryType, String tenan
reCaptchaPropertiesDTO.setReCaptchaEnabled(true);
reCaptchaPropertiesDTO.setReCaptchaKey(properties.getProperty(CaptchaConstants.RE_CAPTCHA_SITE_KEY));
reCaptchaPropertiesDTO.setReCaptchaAPI(properties.getProperty(CaptchaConstants.RE_CAPTCHA_API_URL));
- return Response.ok(reCaptchaPropertiesDTO).build();
+ reCaptchaPropertiesDTO.setReCaptchaType(properties.getProperty(CaptchaConstants.RE_CAPTCHA_TYPE));
} else {
reCaptchaPropertiesDTO.setReCaptchaEnabled(false);
- return Response.ok(reCaptchaPropertiesDTO).build();
}
+ return Response.ok(reCaptchaPropertiesDTO).build();
}
@Override
@@ -80,6 +80,7 @@ public Response verifyCaptcha(ReCaptchaResponseTokenDTO reCaptchaResponse, Strin
Properties properties = RecoveryUtil.getValidatedCaptchaConfigs();
boolean reCaptchaEnabled = Boolean.valueOf(properties.getProperty(CaptchaConstants.RE_CAPTCHA_ENABLED));
+ String reCaptchaType = properties.getProperty(CaptchaConstants.RE_CAPTCHA_TYPE);
if (!reCaptchaEnabled) {
RecoveryUtil.handleBadRequest("ReCaptcha is disabled", Constants.INVALID);
@@ -89,21 +90,44 @@ public Response verifyCaptcha(ReCaptchaResponseTokenDTO reCaptchaResponse, Strin
HttpEntity entity = response.getEntity();
ReCaptchaVerificationResponseDTO reCaptchaVerificationResponseDTO = new ReCaptchaVerificationResponseDTO();
- try {
+ if (CaptchaConstants.RE_CAPTCHA_TYPE_ENTERPRISE.equals(reCaptchaType)) {
+ // For ReCaptcha Enterprise.
if (entity == null) {
- RecoveryUtil.handleBadRequest("ReCaptcha verification response is not received.",
+ RecoveryUtil.handleBadRequest("ReCaptcha Enterprise verification response is not received.",
Constants.STATUS_INTERNAL_SERVER_ERROR_MESSAGE_DEFAULT);
- } else {
+ }
+ try {
+ try (InputStream in = entity.getContent()) {
+ JsonObject verificationResponse = new JsonParser().parse(IOUtils.toString(in)).getAsJsonObject();
+ JsonObject tokenProperties = verificationResponse.get(CaptchaConstants.CAPTCHA_TOKEN_PROPERTIES)
+ .getAsJsonObject();
+ boolean success = tokenProperties.get(CaptchaConstants.CAPTCHA_VALID).getAsBoolean();
+ reCaptchaVerificationResponseDTO.setSuccess(success);
+ }
+ } catch (IOException e) {
+ log.error("Unable to read the verification response.", e);
+ RecoveryUtil.handleBadRequest("Unable to read the verification response.",
+ Constants.STATUS_INTERNAL_SERVER_ERROR_MESSAGE_DEFAULT);
+ }
+ } else {
+ // For ReCaptcha v2 and v3.
+ try {
+ if (entity == null) {
+ RecoveryUtil.handleBadRequest("ReCaptcha verification response is not received.",
+ Constants.STATUS_INTERNAL_SERVER_ERROR_MESSAGE_DEFAULT);
+ }
try (InputStream in = entity.getContent()) {
JsonObject verificationResponse = new JsonParser().parse(IOUtils.toString(in)).getAsJsonObject();
- reCaptchaVerificationResponseDTO.setSuccess(verificationResponse.get(SUCCESS).getAsBoolean());
+ reCaptchaVerificationResponseDTO.setSuccess(verificationResponse.get(
+ CaptchaConstants.CAPTCHA_SUCCESS).getAsBoolean());
}
+ } catch (IOException e) {
+ log.error("Unable to read the verification response.", e);
+ RecoveryUtil.handleBadRequest("Unable to read the verification response.",
+ Constants.STATUS_INTERNAL_SERVER_ERROR_MESSAGE_DEFAULT);
}
- } catch (IOException e) {
- log.error("Unable to read the verification response.", e);
- RecoveryUtil.handleBadRequest("Unable to read the verification response.",
- Constants.STATUS_INTERNAL_SERVER_ERROR_MESSAGE_DEFAULT);
}
+
return Response.ok(reCaptchaVerificationResponseDTO).build();
}
}
diff --git a/components/org.wso2.carbon.identity.api.user.recovery/src/main/resources/api.identity.recovery.yaml b/components/org.wso2.carbon.identity.api.user.recovery/src/main/resources/api.identity.recovery.yaml
index f32bf762e4..770c4c24d8 100644
--- a/components/org.wso2.carbon.identity.api.user.recovery/src/main/resources/api.identity.recovery.yaml
+++ b/components/org.wso2.carbon.identity.api.user.recovery/src/main/resources/api.identity.recovery.yaml
@@ -790,6 +790,8 @@ definitions:
properties:
reCaptchaEnabled:
type: boolean
+ reCaptchaType:
+ type: string
reCaptchaKey:
type: string
reCaptchaAPI:
diff --git a/components/org.wso2.carbon.identity.api.user.recovery/src/test/java/org/wso2/carbon/identity/recovery/endpoint/impl/CaptchaApiServiceImplTest.java b/components/org.wso2.carbon.identity.api.user.recovery/src/test/java/org/wso2/carbon/identity/recovery/endpoint/impl/CaptchaApiServiceImplTest.java
index 70088128a9..354792e128 100644
--- a/components/org.wso2.carbon.identity.api.user.recovery/src/test/java/org/wso2/carbon/identity/recovery/endpoint/impl/CaptchaApiServiceImplTest.java
+++ b/components/org.wso2.carbon.identity.api.user.recovery/src/test/java/org/wso2/carbon/identity/recovery/endpoint/impl/CaptchaApiServiceImplTest.java
@@ -17,16 +17,18 @@
package org.wso2.carbon.identity.recovery.endpoint.impl;
+import org.apache.commons.lang.StringUtils;
import org.mockito.InjectMocks;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import org.wso2.carbon.identity.captcha.util.CaptchaConstants;
import org.wso2.carbon.identity.recovery.endpoint.Utils.RecoveryUtil;
-
+import org.wso2.carbon.identity.recovery.endpoint.dto.ReCaptchaPropertiesDTO;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
@@ -35,8 +37,9 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
-
+import javax.ws.rs.core.Response;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
/**
* Unit tests for CaptchaApiServiceImpl.java
@@ -61,9 +64,51 @@ public void tearDown() {
mockedRecoveryUtil.close();
}
- @Test(description = "This method test, getReCaptcha method for username recovery")
- public void testGetCaptcha() throws IOException {
+ @DataProvider(name = "captchaTestDataProvider")
+ public static Object[][] getCaptchaTestDataProvider() {
+
+ String reCaptchaEnterprise = CaptchaConstants.RE_CAPTCHA_TYPE_ENTERPRISE;
+
+ return new Object[][]{
+ {false, "", ""},
+ {true, "", "https://www.google.com/recaptcha/api.js"},
+ {true, reCaptchaEnterprise, "https://www.google.com/recaptcha/enterprise.js"}
+ };
+ }
+
+ @Test(description = "This method test, getReCaptcha method for username recovery",
+ dataProvider = "captchaTestDataProvider")
+ public void testGetCaptcha(boolean reCaptchaEnabled, String reCaptchaType,
+ String reCaptchaAPI) throws IOException {
+ Properties sampleProperties = getSampleConfigFile();
+
+ sampleProperties.setProperty(CaptchaConstants.RE_CAPTCHA_ENABLED, String.valueOf(reCaptchaEnabled));
+ sampleProperties.setProperty(CaptchaConstants.RE_CAPTCHA_TYPE, reCaptchaType);
+ sampleProperties.setProperty(CaptchaConstants.RE_CAPTCHA_API_URL, reCaptchaAPI);
+
+ mockedRecoveryUtil.when(RecoveryUtil::getValidatedCaptchaConfigs).thenReturn(sampleProperties);
+ mockedRecoveryUtil.when(() -> RecoveryUtil.checkCaptchaEnabledResidentIdpConfiguration(Mockito.anyString(),
+ Mockito.anyString())).thenReturn(true);
+ Response response = captchaApiService.getCaptcha("ReCaptcha", "username-recovery",
+ "test.org");
+
+ ReCaptchaPropertiesDTO reCaptchaPropertiesDTO = response.readEntity(ReCaptchaPropertiesDTO.class);
+
+ assertEquals(reCaptchaPropertiesDTO.getReCaptchaEnabled().booleanValue(), reCaptchaEnabled);
+ if (reCaptchaPropertiesDTO.getReCaptchaType() == null) {
+ assertNull(reCaptchaPropertiesDTO.getReCaptchaType());
+ } else {
+ assertEquals(reCaptchaPropertiesDTO.getReCaptchaType(), reCaptchaType);
+ }
+ if (StringUtils.isBlank(reCaptchaAPI)) {
+ assertNull(reCaptchaPropertiesDTO.getReCaptchaAPI());
+ } else {
+ assertEquals(reCaptchaPropertiesDTO.getReCaptchaAPI(), reCaptchaAPI);
+ }
+ }
+
+ public Properties getSampleConfigFile() throws IOException {
Path path = Paths.get("src/test/resources", "repository", "conf", "identity",
CaptchaConstants.CAPTCHA_CONFIG_FILE_NAME);
Properties sampleProperties = new Properties();
@@ -74,11 +119,6 @@ public void testGetCaptcha() throws IOException {
throw new IOException("Unable to read the captcha configuration file.", e);
}
}
-
- mockedRecoveryUtil.when(RecoveryUtil::getValidatedCaptchaConfigs).thenReturn(sampleProperties);
- mockedRecoveryUtil.when(() -> RecoveryUtil.checkCaptchaEnabledResidentIdpConfiguration(Mockito.anyString(),
- Mockito.anyString())).thenReturn(true);
- assertEquals(captchaApiService.getCaptcha("ReCaptcha", "username-recovery",
- null).getStatus(), 200);
+ return sampleProperties;
}
}
diff --git a/components/org.wso2.carbon.identity.api.user.recovery/src/test/resources/repository/conf/identity/captcha-config.properties b/components/org.wso2.carbon.identity.api.user.recovery/src/test/resources/repository/conf/identity/captcha-config.properties
index 7980a19a37..b920c175db 100644
--- a/components/org.wso2.carbon.identity.api.user.recovery/src/test/resources/repository/conf/identity/captcha-config.properties
+++ b/components/org.wso2.carbon.identity.api.user.recovery/src/test/resources/repository/conf/identity/captcha-config.properties
@@ -21,6 +21,9 @@
# Enable Google reCAPTCHA
recaptcha.enabled=true
+# Google reCAPTCHA type
+recaptcha.type=default
+
# reCaptcha API URL
recaptcha.api.url=https://www.google.com/recaptcha/api.js
diff --git a/components/org.wso2.carbon.identity.captcha/pom.xml b/components/org.wso2.carbon.identity.captcha/pom.xml
index fda4b2af16..4337e4c97c 100644
--- a/components/org.wso2.carbon.identity.captcha/pom.xml
+++ b/components/org.wso2.carbon.identity.captcha/pom.xml
@@ -114,6 +114,22 @@
org.wso2.securevault
org.wso2.securevault
+
+ org.testng
+ testng
+ test
+
+
+ org.jacoco
+ org.jacoco.agent
+ runtime
+ test
+
+
+ org.mockito
+ mockito-inline
+ test
+
@@ -185,6 +201,16 @@
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven.surefire.plugin.version}
+
+
+ src/test/resources/testng.xml
+
+
+
com.github.spotbugs
spotbugs-maven-plugin
diff --git a/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/internal/CaptchaDataHolder.java b/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/internal/CaptchaDataHolder.java
index 0ddf99ea64..260bb81417 100644
--- a/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/internal/CaptchaDataHolder.java
+++ b/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/internal/CaptchaDataHolder.java
@@ -37,6 +37,8 @@ public class CaptchaDataHolder {
private boolean reCaptchaEnabled;
+ private String reCaptchaType;
+
private String reCaptchaAPIUrl;
private String reCaptchaVerifyUrl;
@@ -45,6 +47,8 @@ public class CaptchaDataHolder {
private String reCaptchaSecretKey;
+ private String reCaptchaProjectID;
+
private String reCaptchaErrorRedirectUrls;
private String reCaptchaRequestWrapUrls;
@@ -84,6 +88,26 @@ public void setReCaptchaEnabled(boolean reCaptchaEnabled) {
this.reCaptchaEnabled = reCaptchaEnabled;
}
+ public String getReCaptchaType() {
+
+ return reCaptchaType;
+ }
+
+ public void setReCaptchaType(String reCaptchaType) {
+
+ this.reCaptchaType = reCaptchaType;
+ }
+
+ public String getReCaptchaProjectID() {
+
+ return reCaptchaProjectID;
+ }
+
+ public void setReCaptchaProjectID(String reCaptchaProjectID) {
+
+ this.reCaptchaProjectID = reCaptchaProjectID;
+ }
+
public String getReCaptchaAPIUrl() {
return reCaptchaAPIUrl;
}
diff --git a/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/util/CaptchaConstants.java b/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/util/CaptchaConstants.java
index 985ef64f7e..19f59e3e1d 100644
--- a/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/util/CaptchaConstants.java
+++ b/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/util/CaptchaConstants.java
@@ -34,6 +34,8 @@ public class CaptchaConstants {
public static final String RE_CAPTCHA_ENABLED = "recaptcha.enabled";
+ public static final String RE_CAPTCHA_TYPE = "recaptcha.type";
+
public static final String FORCEFULLY_ENABLED_RECAPTCHA_FOR_ALL_TENANTS = "recaptcha" +
".forcefullyEnabledForAllTenants";
@@ -43,6 +45,8 @@ public class CaptchaConstants {
public static final String RE_CAPTCHA_SITE_KEY = "recaptcha.site.key";
+ public static final String RE_CAPTCHA_PROJECT_ID = "recaptcha.project.id";
+
public static final String RE_CAPTCHA_SECRET_KEY = "recaptcha.secret.key";
public static final String RE_CAPTCHA_REQUEST_WRAP_URLS = "recaptcha.request.wrap.urls";
@@ -66,6 +70,11 @@ public class CaptchaConstants {
public static final String AUTH_FAILURE_MSG = "authFailureMsg";
public static final String RECAPTCHA_FAIL_MSG_KEY = "recaptcha.fail.message";
public static final String TRUE = "true";
+ public static final String CAPTCHA_VALID = "valid";
+ public static final String CAPTCHA_TOKEN_PROPERTIES = "tokenProperties";
+ public static final String CAPTCHA_RISK_ANALYSIS = "riskAnalysis";
+ // Captcha Types.
+ public static final String RE_CAPTCHA_TYPE_ENTERPRISE = "enterprise";
// Default value for threshold for score in reCAPTCHA v3.
public static final double CAPTCHA_V3_DEFAULT_THRESHOLD = 0.5;
diff --git a/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/util/CaptchaUtil.java b/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/util/CaptchaUtil.java
index 4e091b19d9..4a5bb6a1f2 100644
--- a/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/util/CaptchaUtil.java
+++ b/components/org.wso2.carbon.identity.captcha/src/main/java/org/wso2/carbon/identity/captcha/util/CaptchaUtil.java
@@ -18,17 +18,19 @@
package org.wso2.carbon.identity.captcha.util;
+import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.apache.commons.collections.MapUtils;
-import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
+import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
+import org.apache.http.entity.StringEntity;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
@@ -124,6 +126,7 @@ public static void buildReCaptchaFilterProperties() {
CaptchaDataHolder.getInstance().setReCaptchaEnabled(false);
}
+
}
}
@@ -269,36 +272,98 @@ public static Map getClaimValues(User user, int tenantId,
return claimValues;
}
- public static boolean isValidCaptcha(String reCaptchaResponse) throws CaptchaException {
+ private static HttpPost createReCaptchaEnterpriseVerificationHttpPost(String reCaptchaResponse) {
- CloseableHttpClient httpclient = HttpClientBuilder.create().useSystemProperties().build();
- HttpPost httppost = new HttpPost(CaptchaDataHolder.getInstance().getReCaptchaVerifyUrl());
- final double scoreThreshold = CaptchaDataHolder.getInstance().getReCaptchaScoreThreshold();
+ HttpPost httpPost;
+ String recaptchaUrl = CaptchaDataHolder.getInstance().getReCaptchaVerifyUrl();
+ String projectID = CaptchaDataHolder.getInstance().getReCaptchaProjectID();
+ String siteKey = CaptchaDataHolder.getInstance().getReCaptchaSiteKey();
+ String secretKey = CaptchaDataHolder.getInstance().getReCaptchaSecretKey();
+
+ String verifyUrl = recaptchaUrl + "/v1/projects/" + projectID + "/assessments?key=" + secretKey;
+ httpPost = new HttpPost(verifyUrl);
+ httpPost.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
+
+ String json = String.format("{ \"event\": { \"token\": \"%s\", \"siteKey\": \"%s\" } }", reCaptchaResponse,
+ siteKey);
+
+ StringEntity entity = new StringEntity(json, StandardCharsets.UTF_8);
+
+ httpPost.setEntity(entity);
+
+ return httpPost;
+ }
+
+ private static HttpPost createReCaptchaVerificationHttpPost(String reCaptchaResponse) {
+
+ HttpPost httpPost;
+ httpPost = new HttpPost(CaptchaDataHolder.getInstance().getReCaptchaVerifyUrl());
List params = Arrays.asList(new BasicNameValuePair("secret", CaptchaDataHolder
- .getInstance().getReCaptchaSecretKey()), new BasicNameValuePair("response", reCaptchaResponse));
- httppost.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
+ .getInstance().getReCaptchaSecretKey()),
+ new BasicNameValuePair("response", reCaptchaResponse));
+ httpPost.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
+
+ return httpPost;
+ }
+
+ private static void verifyReCaptchaEnterpriseResponse(HttpEntity entity)
+ throws CaptchaServerException, CaptchaClientException {
+
+ final double scoreThreshold = CaptchaDataHolder.getInstance().getReCaptchaScoreThreshold();
- HttpResponse response;
try {
- response = httpclient.execute(httppost);
+ try (InputStream in = entity.getContent()) {
+ JsonElement jsonElement = JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8));
+ JsonObject verificationResponse = jsonElement.getAsJsonObject();
+ if (verificationResponse == null) {
+ throw new CaptchaClientException("Error receiving reCaptcha response from the server");
+ }
+ JsonObject tokenProperties = verificationResponse.get(CaptchaConstants.CAPTCHA_TOKEN_PROPERTIES).
+ getAsJsonObject();
+ boolean success = tokenProperties.get(CaptchaConstants.CAPTCHA_VALID).getAsBoolean();
+
+ JsonObject riskAnalysis = verificationResponse.get(CaptchaConstants.CAPTCHA_RISK_ANALYSIS).
+ getAsJsonObject();
+
+ // Whether this request was a valid reCAPTCHA token.
+ if (!success) {
+ throw new CaptchaClientException("reCaptcha token is invalid. Error:" +
+ verificationResponse.get("error-codes"));
+ }
+ if (riskAnalysis.get(CaptchaConstants.CAPTCHA_SCORE) != null) {
+ double score = riskAnalysis.get(CaptchaConstants.CAPTCHA_SCORE).getAsDouble();
+ // reCAPTCHA enterprise response contains score.
+ if (log.isDebugEnabled()) {
+ log.debug("reCAPTCHA Enterprise response { timestamp:" +
+ tokenProperties.get("createTime") + ", action: " +
+ tokenProperties.get("action") + ", score: " + score + " }");
+ }
+ if (score < scoreThreshold) {
+ throw new CaptchaClientException("reCaptcha score is less than the threshold.");
+ }
+ }
+ }
} catch (IOException e) {
- throw new CaptchaServerException("Unable to get the verification response.", e);
+ throw new CaptchaServerException("Unable to read the verification response.", e);
+ } catch (ClassCastException e) {
+ throw new CaptchaServerException("Unable to cast the response value.", e);
}
+ }
- HttpEntity entity = response.getEntity();
- if (entity == null) {
- throw new CaptchaServerException("reCaptcha verification response is not received.");
- }
+ private static void verifyReCaptchaResponse(HttpEntity entity)
+ throws CaptchaServerException, CaptchaClientException {
+
+ final double scoreThreshold = CaptchaDataHolder.getInstance().getReCaptchaScoreThreshold();
try {
try (InputStream in = entity.getContent()) {
- JsonObject verificationResponse = new JsonParser().parse(IOUtils.toString(in)).getAsJsonObject();
+ JsonElement jsonElement = JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8));
+ JsonObject verificationResponse = jsonElement.getAsJsonObject();
if (verificationResponse == null) {
throw new CaptchaClientException("Error receiving reCaptcha response from the server");
}
- boolean success = verificationResponse.get(CaptchaConstants.CAPTCHA_SUCCESS) != null
- && verificationResponse.get(CaptchaConstants.CAPTCHA_SUCCESS).getAsBoolean();
+ boolean success = verificationResponse.get(CaptchaConstants.CAPTCHA_SUCCESS).getAsBoolean();
// Whether this request was a valid reCAPTCHA token.
if (!success) {
throw new CaptchaClientException("reCaptcha token is invalid. Error:" +
@@ -306,7 +371,7 @@ public static boolean isValidCaptcha(String reCaptchaResponse) throws CaptchaExc
}
if (verificationResponse.get(CaptchaConstants.CAPTCHA_SCORE) != null) {
double score = verificationResponse.get(CaptchaConstants.CAPTCHA_SCORE).getAsDouble();
- // reCAPTCHA v3 response contains score
+ // reCAPTCHA v3 response contains score.
if (log.isDebugEnabled()) {
log.debug("reCAPTCHA v3 response { timestamp:" +
verificationResponse.get("challenge_ts") + ", action: " +
@@ -327,6 +392,45 @@ public static boolean isValidCaptcha(String reCaptchaResponse) throws CaptchaExc
} catch (ClassCastException e) {
throw new CaptchaServerException("Unable to cast the response value.", e);
}
+ }
+
+ public static boolean isValidCaptcha(String reCaptchaResponse) throws CaptchaException {
+
+ CloseableHttpClient httpclient = HttpClientBuilder.create().useSystemProperties().build();
+ String reCaptchaType = CaptchaDataHolder.getInstance().getReCaptchaType();
+
+ HttpPost httpPost;
+
+ // If the reCaptcha type is defined and, it is enterprise, the enterprise process will be done. Otherwise,
+ // the reCaptcha v2/v3 process will be done.
+ if (CaptchaConstants.RE_CAPTCHA_TYPE_ENTERPRISE.equals(reCaptchaType)) {
+ // For ReCaptcha Enterprise.
+ httpPost = createReCaptchaEnterpriseVerificationHttpPost(reCaptchaResponse);
+ } else {
+ // For ReCaptcha v2 and v3.
+ httpPost = createReCaptchaVerificationHttpPost(reCaptchaResponse);
+ }
+
+ HttpResponse response;
+ try {
+ response = httpclient.execute(httpPost);
+ } catch (IOException e) {
+ throw new CaptchaServerException("Unable to get the verification response.");
+ }
+
+ HttpEntity responseEntity = response.getEntity();
+
+ if (responseEntity == null) {
+ throw new CaptchaServerException("reCaptcha verification response is not received.");
+ }
+
+ if (CaptchaConstants.RE_CAPTCHA_TYPE_ENTERPRISE.equals(reCaptchaType)) {
+ // For ReCaptcha Enterprise.
+ verifyReCaptchaEnterpriseResponse(responseEntity);
+ } else {
+ // For Recaptcha v2 and v3.
+ verifyReCaptchaResponse(responseEntity);
+ }
return true;
}
@@ -456,6 +560,20 @@ private static UserStoreManager getUserStoreManagerForUser(String userName,
private static void setReCaptchaConfigs(Properties properties) {
+ String reCaptchaType = properties.getProperty(CaptchaConstants.RE_CAPTCHA_TYPE);
+ if (!StringUtils.isBlank(reCaptchaType)) {
+ CaptchaDataHolder.getInstance().setReCaptchaType(reCaptchaType);
+ }
+
+ if (CaptchaConstants.RE_CAPTCHA_TYPE_ENTERPRISE.equals(reCaptchaType)) {
+ // ReCaptcha Enterprise require Project ID.
+ String reCaptchaProjectID = properties.getProperty(CaptchaConstants.RE_CAPTCHA_PROJECT_ID);
+ if (StringUtils.isBlank(reCaptchaProjectID)) {
+ throw new RuntimeException(getValidationErrorMessage(CaptchaConstants.RE_CAPTCHA_PROJECT_ID));
+ }
+ CaptchaDataHolder.getInstance().setReCaptchaProjectID(reCaptchaProjectID);
+ }
+
String reCaptchaAPIUrl = properties.getProperty(CaptchaConstants.RE_CAPTCHA_API_URL);
if (StringUtils.isBlank(reCaptchaAPIUrl)) {
throw new RuntimeException(getValidationErrorMessage(CaptchaConstants.RE_CAPTCHA_API_URL));
@@ -692,6 +810,15 @@ public static Boolean isReCaptchaEnabled() {
return CaptchaDataHolder.getInstance().isReCaptchaEnabled();
}
+ /**
+ * Get the ReCaptcha Type.
+ * @return ReCaptcha Type as a String.
+ */
+ public static String reCaptchaType() {
+
+ return CaptchaDataHolder.getInstance().getReCaptchaType();
+ }
+
/**
* Check whether ReCaptcha is enabled for the given flow.
*
diff --git a/components/org.wso2.carbon.identity.captcha/src/test/java/org/wso2/carbon/identity/captcha/util/CaptchaUtilTest.java b/components/org.wso2.carbon.identity.captcha/src/test/java/org/wso2/carbon/identity/captcha/util/CaptchaUtilTest.java
new file mode 100644
index 0000000000..37cbdc497e
--- /dev/null
+++ b/components/org.wso2.carbon.identity.captcha/src/test/java/org/wso2/carbon/identity/captcha/util/CaptchaUtilTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (c) 2023, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
+ *
+ * Licensed 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.identity.captcha.util;
+
+import com.google.gson.JsonObject;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.wso2.carbon.identity.captcha.internal.CaptchaDataHolder;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import static org.testng.Assert.*;
+
+/**
+ * Unit tests for CaptchaUtil.java
+ */
+public class CaptchaUtilTest {
+
+ private final String RECAPTCHA_API_URL = "https://www.google.com/recaptcha/api/siteverify";
+
+ @BeforeMethod
+ public void setUp() {
+
+ MockitoAnnotations.openMocks(this);
+ }
+
+ private Method getCreateReCaptchaEnterpriseVerificationHttpPostMethod() throws NoSuchMethodException {
+
+ Method method = CaptchaUtil.class.getDeclaredMethod("createReCaptchaEnterpriseVerificationHttpPost",
+ String.class);
+ method.setAccessible(true);
+ return method;
+ }
+
+ private Method getCreateReCaptchaVerificationHttpPostMethod() throws NoSuchMethodException {
+
+ Method method = CaptchaUtil.class.getDeclaredMethod("createReCaptchaVerificationHttpPost",
+ String.class);
+ method.setAccessible(true);
+ return method;
+ }
+
+ private Method getVerifyReCaptchaEnterpriseResponseMethod() throws NoSuchMethodException {
+
+ Method method = CaptchaUtil.class.getDeclaredMethod("verifyReCaptchaEnterpriseResponse",
+ HttpEntity.class);
+ method.setAccessible(true);
+ return method;
+ }
+
+ private Method getVerifyReCaptchaResponseMethod() throws NoSuchMethodException {
+
+ Method method = CaptchaUtil.class.getDeclaredMethod("verifyReCaptchaResponse",
+ HttpEntity.class);
+ method.setAccessible(true);
+ return method;
+ }
+
+ private JsonObject getReCaptchaEnterpriseJsonObject(boolean valid, double score) {
+
+ JsonObject verificationResponse = new JsonObject();
+ JsonObject tokenProperties = new JsonObject();
+ tokenProperties.addProperty(CaptchaConstants.CAPTCHA_VALID, valid);
+ verificationResponse.add(CaptchaConstants.CAPTCHA_TOKEN_PROPERTIES, tokenProperties);
+ JsonObject riskAnalysis = new JsonObject();
+ riskAnalysis.addProperty(CaptchaConstants.CAPTCHA_SCORE, score);
+ verificationResponse.add(CaptchaConstants.CAPTCHA_RISK_ANALYSIS, riskAnalysis);
+ return verificationResponse;
+ }
+
+ private JsonObject getReCaptchaJsonObject(boolean valid, double score) {
+
+ JsonObject verificationResponse = new JsonObject();
+ verificationResponse.addProperty(CaptchaConstants.CAPTCHA_SUCCESS, valid);
+ verificationResponse.addProperty(CaptchaConstants.CAPTCHA_SCORE, score);
+ return verificationResponse;
+ }
+
+ @Test (description = "This method is used to test the createReCaptchaEnterpriseVerificationHttpPost method")
+ public void testCreateReCaptchaEnterpriseVerificationHttpPost() throws NoSuchMethodException,
+ InvocationTargetException, IllegalAccessException {
+
+ CaptchaDataHolder.getInstance().setReCaptchaVerifyUrl(RECAPTCHA_API_URL);
+ CaptchaDataHolder.getInstance().setReCaptchaSecretKey("dummyKey");
+ CaptchaDataHolder.getInstance().setReCaptchaSiteKey("dummySiteKey");
+ CaptchaDataHolder.getInstance().setReCaptchaProjectID("dummyProjectId");
+
+
+
+ Method method = getCreateReCaptchaEnterpriseVerificationHttpPostMethod();
+ HttpPost httpPost = (HttpPost) method.invoke(null, "reCaptchaEnterpriseResponse");
+ String expectedURI = RECAPTCHA_API_URL+ "/v1/projects/dummyProjectId/assessments?key=dummyKey";
+ Assert.assertEquals(httpPost.getURI().toString(), expectedURI);
+
+ }
+
+ @Test (description = "This method is used to test the createReCaptchaEnterpriseVerificationHttpPost method")
+ public void testCreateReCaptchaVerificationHttpPost() throws NoSuchMethodException,
+ InvocationTargetException, IllegalAccessException {
+
+ CaptchaDataHolder.getInstance().setReCaptchaVerifyUrl(RECAPTCHA_API_URL);
+ CaptchaDataHolder.getInstance().setReCaptchaSecretKey("dummyKey");
+ CaptchaDataHolder.getInstance().setReCaptchaSiteKey("dummySiteKey");
+ CaptchaDataHolder.getInstance().setReCaptchaProjectID("dummyProjectId");
+
+ Method method = getCreateReCaptchaVerificationHttpPostMethod();
+ HttpPost httpPost = (HttpPost) method.invoke(null, "reCaptchaEnterpriseResponse");
+ Assert.assertEquals(httpPost.getURI().toString(), RECAPTCHA_API_URL);
+ }
+
+
+ @Test (description = "This method is used to test the verifyReCaptchaEnterpriseResponse method, " +
+ "with high captcha score")
+ public void testVerifyReCaptchaEnterpriseResponseWithHighScore() throws IOException, NoSuchMethodException,
+ InvocationTargetException, IllegalAccessException {
+
+ CaptchaDataHolder.getInstance().setReCaptchaScoreThreshold(CaptchaConstants.CAPTCHA_V3_DEFAULT_THRESHOLD);
+
+ JsonObject verificationResponse = getReCaptchaEnterpriseJsonObject(true, 0.7);
+ Method method = getVerifyReCaptchaEnterpriseResponseMethod();
+ HttpEntity httpEntity = Mockito.mock(HttpEntity.class);
+ Mockito.when(httpEntity.getContent()).thenReturn(new ByteArrayInputStream(
+ verificationResponse.toString().getBytes()));
+ // verify no exception is thrown for high score
+ method.invoke(null, httpEntity);
+ }
+
+ @Test (description = "This method is used to test the verifyReCaptchaEnterpriseResponse method, " +
+ "with low captcha score")
+ public void testVerifyReCaptchaEnterpriseResponseWithLowScore() throws IOException, NoSuchMethodException {
+
+ CaptchaDataHolder.getInstance().setReCaptchaScoreThreshold(CaptchaConstants.CAPTCHA_V3_DEFAULT_THRESHOLD);
+
+ JsonObject verificationResponse = getReCaptchaEnterpriseJsonObject(true, 0.4);
+ Method method = getVerifyReCaptchaEnterpriseResponseMethod();
+ HttpEntity httpEntity = Mockito.mock(HttpEntity.class);
+ Mockito.when(httpEntity.getContent()).thenReturn(new ByteArrayInputStream(verificationResponse.toString().
+ getBytes()));
+ // verify an exception is thrown for low score
+ assertThrows(InvocationTargetException.class, () -> method.invoke(null, httpEntity));
+ }
+
+ @Test (description = "This method is used to test the verifyReCaptchaEnterpriseResponse method, " +
+ "with invalid response")
+ public void testVerifyReCaptchaEnterpriseResponseWithInvalidResponse() throws IOException, NoSuchMethodException {
+
+ CaptchaDataHolder.getInstance().setReCaptchaScoreThreshold(CaptchaConstants.CAPTCHA_V3_DEFAULT_THRESHOLD);
+
+ JsonObject verificationResponse = getReCaptchaEnterpriseJsonObject(false, 0.7);
+ Method method = getVerifyReCaptchaEnterpriseResponseMethod();
+ HttpEntity httpEntity = Mockito.mock(HttpEntity.class);
+ Mockito.when(httpEntity.getContent()).thenReturn(new ByteArrayInputStream(verificationResponse.
+ toString().getBytes()));
+ // verify an exception is thrown for invalid response
+ assertThrows(InvocationTargetException.class, () -> method.invoke(null, httpEntity));
+ }
+
+ @Test (description = "This method is used to test the verifyReCaptchaResponse method, " +
+ "with high captcha score")
+ public void testVerifyReCaptchaResponseWithHighScore() throws IOException, NoSuchMethodException,
+ InvocationTargetException, IllegalAccessException {
+
+ CaptchaDataHolder.getInstance().setReCaptchaScoreThreshold(CaptchaConstants.CAPTCHA_V3_DEFAULT_THRESHOLD);
+
+ JsonObject verificationResponse = getReCaptchaJsonObject(true, 0.7);
+ Method method = getVerifyReCaptchaResponseMethod();
+ HttpEntity httpEntity = Mockito.mock(HttpEntity.class);
+ Mockito.when(httpEntity.getContent()).thenReturn(new ByteArrayInputStream(verificationResponse.toString().
+ getBytes()));
+ // verify no exception is thrown for high score
+ method.invoke(null, httpEntity);
+ }
+
+ @Test (description = "This method is used to test the verifyReCaptchaResponse method, " +
+ "with low captcha score")
+ public void testVerifyReCaptchaResponseWithLowScore() throws IOException, NoSuchMethodException {
+
+ CaptchaDataHolder.getInstance().setReCaptchaScoreThreshold(CaptchaConstants.CAPTCHA_V3_DEFAULT_THRESHOLD);
+
+ JsonObject verificationResponse = getReCaptchaJsonObject(true, 0.4);
+ Method method = getVerifyReCaptchaResponseMethod();
+ HttpEntity httpEntity = Mockito.mock(HttpEntity.class);
+ Mockito.when(httpEntity.getContent()).thenReturn(new ByteArrayInputStream(verificationResponse.toString().
+ getBytes()));
+ // verify no exception is thrown for low score
+ assertThrows(InvocationTargetException.class, () -> method.invoke(null, httpEntity));
+ }
+
+ @Test (description = "This method is used to test the verifyReCaptchaResponse method, " +
+ "with invalid response")
+ public void testVerifyReCaptchaResponseWithInvalidResponse() throws IOException, NoSuchMethodException {
+
+ CaptchaDataHolder.getInstance().setReCaptchaScoreThreshold(CaptchaConstants.CAPTCHA_V3_DEFAULT_THRESHOLD);
+
+ JsonObject verificationResponse = getReCaptchaJsonObject(false, 0.7);
+ Method method = getVerifyReCaptchaResponseMethod();
+ HttpEntity httpEntity = Mockito.mock(HttpEntity.class);
+ Mockito.when(httpEntity.getContent()).thenReturn(new ByteArrayInputStream(verificationResponse.toString().
+ getBytes()));
+ // verify no exception is thrown for invalid response
+ assertThrows(InvocationTargetException.class, () -> method.invoke(null, httpEntity));
+ }
+}
diff --git a/components/org.wso2.carbon.identity.captcha/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.captcha/src/test/resources/testng.xml
new file mode 100644
index 0000000000..9dbe5a2ae6
--- /dev/null
+++ b/components/org.wso2.carbon.identity.captcha/src/test/resources/testng.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/features/org.wso2.carbon.identity.captcha.server.feature/resources/conf/captcha-config.properties.j2 b/features/org.wso2.carbon.identity.captcha.server.feature/resources/conf/captcha-config.properties.j2
index 45078ab9ef..bd014187fa 100644
--- a/features/org.wso2.carbon.identity.captcha.server.feature/resources/conf/captcha-config.properties.j2
+++ b/features/org.wso2.carbon.identity.captcha.server.feature/resources/conf/captcha-config.properties.j2
@@ -21,6 +21,9 @@
# Enable Google reCAPTCHA
recaptcha.enabled={{recaptcha.enabled}}
+# captcha type
+recaptcha.type={{recaptcha.type}}
+
# Forcefully enable Google reCAPTCHA for all tenants
recaptcha.forcefullyEnabledForAllTenants={{recaptcha.forcefully_enabled_for_all_tenants}}
@@ -36,6 +39,9 @@ recaptcha.site.key={{recaptcha.site_key}}
# reCaptcha secret key
recaptcha.secret.key={{recaptcha.secret_key}}
+# reCaptcha Enterprise project id
+recaptcha.project.id={{recaptcha.project_id}}
+
# login.do URL paths
{% if recaptcha.redirect_urls is defined %}
recaptcha.failed.redirect.urls={{recaptcha.redirect_urls}}