How to use reCaptcha with Jahia
Question
How can I use Google's reCaptcha with Jahia?
Solution
Instead of using its own implementation, Jahia recommends using a more recent alternative, such as Google's reCaptcha.
What are captchas in a few words?
Captchas are an essential tool in web security that help protect websites from spam and abuse.
Captchas work by requiring website visitors to complete a task that is easy for humans but difficult for automated bots, such as identifying letters or numbers in an image or audio file.
There are three types of captchas:
- Image-based captcha: A captcha that requires users to identify and select specific objects, such as cars or street signs, within an image.
- Audio-based captcha: A captcha that requires users to listen to a series of numbers or words and then type them into a text field.
- Text-based captcha: A captcha that requires users to type in a sequence of letters or numbers that are displayed in a distorted or obfuscated way.
Google reCaptcha
reCaptcha is a free captcha service offered by Google that is designed to protect websites from spam and abuse. It is also designed to be more user-friendly and accessible than traditional captchas.
The reCaptcha service is available in two versions: reCaptcha v2 and reCaptcha v3.
Both versions are designed to be easy to use and accessible to a wide range of users.
Obtain an API key
To activate reCAPTCHA you will need to get API keys from Google.
Go to reCAPTCHA Administration to create your keys.
You will then obtain a SITE KEY and a SECRET KEY to use in your web site related to your site domain.
Implement reCAPTCHA in Jahia
First, include the reCaptcha API script in your view:
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
Then, the reCaptcha widget to your form where you want to be displayed:
<div class="g-recaptcha" data-sitekey="YOUR_SITE_KEY"></div>
Make sure to replace YOUR_SITE_KEY
with the site key provided by Google.
Using the contact module (source: https://github.com/Jahia/contact/blob/master/src/main/resources/jnt_contactForm/html/contactForm.jsp ) as an example we have made some changes (in blue) in the jsp like:
Since we don't want to submit our form before the captcha check, we update the submit entry and add an id to our form to explicitly identify it.
<template:tokenizedForm>
<form action="<c:url value='${url.base}${currentNode.path}/*'/>" method="post" id="contactForm">
<input type="hidden" name="jcrNodeType" value="jnt:contact"/>
<input type="hidden" name="jcrRedirectTo" value="<c:url value='${url.base}${renderContext.mainResource.node.path}'/>"/>
<%-- Define the output format for the newly created node by default html or by jcrRedirectTo--%>
<input type="hidden" name="jcrNewNodeOutputFormat" value="html"/>
<c:set var="props" value="${currentNode.properties}"/>
<jcr:nodeType name="jnt:contact" var="contactType"/>
<c:set var="propDefs" value="${contactType.declaredPropertyDefinitionsAsMap}"/>
<fieldset>
<legend><fmt:message key="jnt_contact"/></legend>
............
<c:if test="${props.firstname.boolean}">
<p>
<label class="left" for="firstname">${fn:escapeXml(jcr:label(props.firstname.definition,currentResource.locale))}</label><input id="firstname" type="text"
name="firstname" ${disabled} />
</p>
</c:if>
<c:if test="${props.lastname.boolean}">
<p>
<label class="left" for="lastname">${fn:escapeXml(jcr:label(props.lastname.definition,currentResource.locale))}</label><input id="lastname" type="text" name="lastname" ${disabled} />
</p>
</c:if>
<c:if test="${props.captcha.boolean}">
<p class="field">
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="YOUR_SITE_KEY"></div>
</p>
</c:if>
<div class="divButton"><br />
<input type="button" onclick="recaptchaVerifier()" tabindex="28" value="<fmt:message key='save'/>" class="button" ${disabled}/>
<input type="reset" tabindex="29" value="<fmt:message key='reset'/>" class="button" id="reset" ${disabled}/>
</div>
</fieldset>
</form>
</template:tokenizedForm>
Now, we’ll need to create a custom Jahia action to verify server-side the captcha response before submitting your form:
package org.jahia.modules.contact.actions;
import org.apache.commons.lang.StringUtils;
import org.jahia.bin.Action;
import org.jahia.services.content.JCRSessionWrapper;
import org.jahia.bin.ActionResult;
import org.jahia.services.render.RenderContext;
import org.jahia.services.render.Resource;
import org.jahia.services.render.URLResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONObject;
import org.json.JSONException;
import org.slf4j.Logger;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
@Component(immediate = true, service = Action.class)
public class RecaptchaVerifierAction extends Action {
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(RecaptchaVerifierAction.class);
private static final String NAME = "recaptchaVerifier";
@Activate
public void onActivate() {
setRequireAuthenticatedUser(false);
setRequiredWorkspace("live");
setName(NAME);
}
@Override
public ActionResult doExecute(
HttpServletRequest httpServletRequest,
RenderContext renderContext,
Resource resource,
JCRSessionWrapper jcrSessionWrapper,
Map<String, List<String>> map,
URLResolver urlResolver
) throws JSONException {
String gcaptchaResponse = httpServletRequest.getParameter("gcaptchaResponse");
if (StringUtils.isEmpty(gcaptchaResponse)) {
logger.error("missing mandatory query parameter.");
return null;
}
String secret = "YOUR_SECRET_KEY"; // Replace with your actual secret key
String url = "https://www.google.com/recaptcha/api/siteverify";
String params = "secret=" + secret + "&response=" + gcaptchaResponse;
try {
String response = sendRequest(url, params);
JSONObject responseJson = new JSONObject(response);
boolean success = responseJson.getBoolean("success");
JSONObject jsonAnswer = new JSONObject();
if (success) {
// The captcha was verified successfully
jsonAnswer.put("code", HttpServletResponse.SC_OK);
return new ActionResult(HttpServletResponse.SC_OK, null, jsonAnswer);
} else {
// The captcha verification failed
jsonAnswer.put("code", HttpServletResponse.SC_NOT_ACCEPTABLE);
return new ActionResult(HttpServletResponse.SC_NOT_ACCEPTABLE, null, jsonAnswer);
}
} catch (IOException | JSONException e) {
// There was an error verifying the captcha
JSONObject jsonAnswer = new JSONObject();
jsonAnswer.put("code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return new ActionResult(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null, jsonAnswer);
}
}
private String sendRequest(String url, String params) throws IOException {
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
con.setDoOutput(true);
try (DataOutputStream wr = new DataOutputStream(con.getOutputStream())) {
wr.writeBytes(params);
wr.flush();
}
int responseCode = con.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
String inputLine;
StringBuffer responseBuffer = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
responseBuffer.append(inputLine);
}
return responseBuffer.toString();
}
} else {
throw new IOException("HTTP request failed with response code: " + responseCode);
}
}
}
Make sure to replace YOUR_SECRET_KEY
with the site key provided by Google.
Then to ensure you can call this action, we’ll need to add OSGi capability in your project pom.xml
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<_dsannotations>*</_dsannotations>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
To finish, in your form submission function we’ll add a call to your new custom Jahia action.
Actions are protected against CSRF attacks and must contain a valid CSRF token to be executed. So we’ll need to manage this when we call the custom Jahia action.
<script>
const currentPath = '${url.baseLive}${currentNode.path}';
const recaptchaVerifier = function() {
// Get the user's response
let gcaptchaResponse = grecaptcha.getResponse();
// Check if the user completed the captcha
if (gcaptchaResponse.length === 0) {
// The user did not complete the captcha
alert('Please complete the captcha to submit the form.');
return;
}
const token = document.querySelector("input[name='CSRFTOKEN']").value;
let actionPath = currentPath + ".recaptchaVerifier.do?CSRFTOKEN=" + token;
let data = 'gcaptchaResponse=' + encodeURIComponent(gcaptchaResponse);
let xhr = new XMLHttpRequest();
xhr.open("POST", actionPath, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onload = function() {
if (xhr.status === 200) {
// Submit your form
let form = document.getElementById("contactForm");
form.submit();
} else {
// Handle wrong user answer for captcha
}
};
xhr.onerror = function() {
// Handle error during captcha verification
};
xhr.send(data);
}
</script>