Dynamic reCAPTCHA API keys in Primefaces

Goal

Changing the way the reCAPTCHA API (public and private) keys are read wiht PrimeFaces 5.3

Description

In a recent project, we had the opportunity to add the google reCAPTCHA in order to allow the user to have a better experience and safety navigation within our Primefaces@Liferay application.

According to the documentation, however, we need first to get 2 keys (a private and a public key – check here how to get the keys) to be able to use this feature which are defined in the web application descriptor (web.xml). A new requirement was to make it possible to change/define those keys in a per environment basis which was not allowed through the explicit definition of those keys in the descriptor. In this recipe you will learn a way to make it possible to read those keys from the portal-ext.properties file (6.2-ce-ga6). Before I forget, I’d like to say thanks to Rafael Martins, a colleague from Linkare TI with whom I implemented this mechanism and who, after the implementation, helped me writing/publishing this recipe.

How to

  1. Inside the portal-ext.properties insert the following keys (we opted to use the same keys as the ones that Primefaces defined to get those keys):
    # Google Keys Configuration
    primefaces.PUBLIC_CAPTCHA_KEY=<the_public_key>
    primefaces.PRIVATE_CAPTCHA_KEY=<the_private_key>
    
  2. Inside your faces-config.xml (portlet/src/main/webapp/WEB-INF/) add the following code (we will implement our own renderer which is where the public key is fetched on):
    ...
    <renderer>
    <component-family>org.primefaces.component</component-family>
    <renderer-type>org.primefaces.component.CaptchaRenderer</renderer-type>
    <renderer-class>com.linkare.myproject.web.renderer.captcha.MyProjectCaptchaRenderer</renderer-class>
    </renderer>
    ...
    

    Note: Doing this, will allow us to use a diferent renderer which will make it possible to read the public API key from portal-ext.properties.

  3. Our custom renderer simply needs to override the getPublicKey method, invoking an utility class that will load the key as defined in our portal-ext.properties file:
    package com.linkare.myproject.web.renderer.captcha;
    
    import javax.faces.context.FacesContext;
    
    import org.primefaces.component.captcha.Captcha;
    import org.primefaces.component.captcha.CaptchaRenderer;
    import com.linkare.myproject.web.config.RecaptchaKeyConfig;
    
    /**
     *
     * @author Paulo Zenida - Linkare TI
     *
     */
    public class MyProjectCaptchaRenderer extends CaptchaRenderer { 
    
      @Override protected String getPublicKey(FacesContext context, Captcha captcha) {
        return RecaptchaKeyConfig.getPublicKey();
      }
    }
    
  4. Create the RecaptchaKeyConfig:
    package com.linkare.myproject.web.config;
    
    import org.primefaces.component.captcha.Captcha;
    
    import com.liferay.portal.kernel.util.PropsUtil; 
    
    /**
     *
     * With this class, we now have access to the portal-ext properties such as the PUBLIC_CAPTCHA_KEY and the PRIVATE_CAPTCHA_KEY*/
     *
     * GO HERE FOR THE CAPTCHA: https://www.google.com/recaptcha/admin#list
     *
     * @author Paulo Zenida - Linkare TI
     *
     */
    public class RecaptchaKeyConfig {
    
      private RecaptchaKeyConfig() {
      }
    
      public static String getPrivateKey() {
        return PropsUtil.get(Captcha.PRIVATE_KEY);
      }
    
      public static String getPublicKey() {
        return PropsUtil.get(Captcha.PUBLIC_KEY);
      }
    }
    
  5. Now, for the private key, we need something a litle bit trickier because we need to extend the captcha component and, unfortunately, the method that gets the private key from the application’s descriptor by default is private which means we need to override the class in a different way. In this case, we will override the validateValue method which still needs to invoke its super method. The implementation on the original Captcha component from Primefaces invokes the validateValue on the UIInput which will check if the value is valid and, if that is so, it will use its own implementation to verify the private and public keys provided. Therefore, in order to extend from that component, we will invoke super in our implementation, and we will catcha the FacesException with the message that states that the private key is missing. If that happens, we are certain that the input is valid (that exception is thrown only when the input is valid) and after catching the exception, we may perform our validation with the private key that is fetched from portal-ext.properties:
    package com.linkare.myproject.web.renderer.captcha;
    
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.UnsupportedEncodingException;
    import java.net.URL;
    import java.net.URLConnection;
    import java.net.URLEncoder;
    
    import javax.faces.FacesException;
    import javax.faces.application.FacesMessage;
    import javax.faces.context.FacesContext;
    
    import org.primefaces.component.captcha.Captcha;
    import org.primefaces.context.RequestContext;
    import org.primefaces.json.JSONObject;
    import org.primefaces.util.MessageFactory;
    
    import com.linkare.myproject.web.config.RecaptchaKeyConfig;
    
    /**
     *
     * @author Paulo Zenida - Linkare TI
     *
     */
    public class myprojectCaptcha extends Captcha {
    
      private static final String PRIVATE_KEY_NOT_FOUND_EXCEPTION_MSG = "Cannot find private key for catpcha, use primefaces.PRIVATE_CAPTCHA_KEY context-param to define one";
    
      @Override
      protected void validateValue(FacesContext context, Object value) {
        try {
          super.validateValue(context, value);
        } catch (final FacesException e) {
          // if the issue on super is that it cannot find a private key, try to get it in the current class
          if (PRIVATE_KEY_NOT_FOUND_EXCEPTION_MSG.equals(e.getMessage())) {
            boolean result = false;
            try {
              URL url = new URL("https://www.google.com/recaptcha/api/siteverify");
              URLConnection conn = url.openConnection();
              conn.setDoInput(true);
              conn.setDoOutput(true);
              conn.setUseCaches(false);
              conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
              String postBody = createPostParameters(context, value);
    
              OutputStream out = conn.getOutputStream();
              out.write(postBody.getBytes());
              out.flush();
              out.close();
    
              BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
              String inputLine;
              StringBuffer response = new StringBuffer();
              while ((inputLine = rd.readLine()) != null) {
                response.append(inputLine);
              }
    
              JSONObject json = new JSONObject(response.toString());
              result = json.getBoolean("success");
    
              rd.close();
            } catch (Exception exception) {
              throw new FacesException(exception);
            }
    
            if (!result) {
              setValid(false);
    
              String validatorMessage = getValidatorMessage();
              FacesMessage msg = null;
    
              if (validatorMessage != null) {
                msg = new FacesMessage(FacesMessage.SEVERITY_ERROR, validatorMessage, validatorMessage);
              } else {
                Object[] params = new Object[2];
                params[0] = MessageFactory.getLabel(context, this);
                params[1] = (String) value;
                msg = MessageFactory.getMessage(Captcha.INVALID_MESSAGE_ID, FacesMessage.SEVERITY_ERROR, params);
              }
              context.addMessage(getClientId(context), msg);
            }
          }
          RequestContext requestContext = RequestContext.getCurrentInstance();
          if (requestContext.isAjaxRequest()) {
            requestContext.execute("grecaptcha.reset()");
          }
        }
      }
    
      private String createPostParameters(FacesContext facesContext, Object value) throws UnsupportedEncodingException {
        String privateKey = RecaptchaKeyConfig.getPrivateKey();
    
        if (privateKey == null) {
          throw new FacesException(PRIVATE_KEY_NOT_FOUND_EXCEPTION_MSG);
        }
    
        StringBuilder postParams = new StringBuilder();
        postParams.append("secret=").append(URLEncoder.encode(privateKey, "UTF-8"));
        postParams.append("&response=").append(URLEncoder.encode((String) value, "UTF-8"));
    
        String params = postParams.toString();
        postParams.setLength(0);
    
        return params;
      }
    }
    
  6. Finally, inside the web.xml we no longer need the previous parameters as defined in Primefaces documentation to use the reCAPTCHA component:
    <--
    <context-param>
    <param-name>primefaces.PRIVATE_CAPTCHA_KEY</param-name>
    <param-value>...</param-value>
    </context-param>
    <context-param>
    <param-name>primefaces.PUBLIC_CAPTCHA_KEY</param-name>
    <param-value>...</param-value>
    </context-param>
    -->
    

Explanations

All explanations were made in place. Therefore, there’s nothing useful to add here, I suppose…

3 comments

  1. Hello Paulo,

    I think there’s an easier solution to your problem.

    You could implement a ServletContextListener and change the init parameters dynamically.

    Example:

    package xyz.msimoes;

    import javax.servlet.ServletContextEvent;
    import javax.servlet.ServletContextListener;
    import javax.servlet.annotation.WebListener;

    import org.primefaces.component.captcha.Captcha;

    @WebListener
    public class MyListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent evt) {
    //Example
    evt.getServletContext().setInitParameter(Captcha.PRIVATE_KEY, “bu”);
    //Do the same for the other parameters
    }

    @Override
    public void contextDestroyed(ServletContextEvent evt) {

    }

    }

    Regards,

    Marcos

    1. Hello Marcos,

      Thank you very much for your input. It’s a very good idea, indeed. I’ll try it as soon as I can in the Liferay environment I had implemented this to make sure it doesn’t break anything..
      Best, PZ

      1. Hello again,

        I finally had some time to check your suggestion and it seems to work perfectly. All I had to do was change the dependency on the servlet-api (from 2.4, which is the default that one gets when creating the original project from the maven archetype, to 3.0.1), and add the listener as follows:

        @WebListener
        public class MyListener implements ServletContextListener {

        @Override
        public void contextInitialized(ServletContextEvent evt) {

        evt.getServletContext().setInitParameter(Captcha.PUBLIC_KEY, RecaptchaKeyConfig.getPublicKey());
        evt.getServletContext().setInitParameter(Captcha.PRIVATE_KEY, RecaptchaKeyConfig.getPrivateKey());
        }

        @Override
        public void contextDestroyed(ServletContextEvent evt) {

        }
        }

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s