maven repository sources Developer Jahia 8

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.

recaptcha-setup.png

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>