How to protect your forms with cloudflare turnstile

How to protect your forms with cloudflare turnstile

Introduction

Spambots have been a major problem when dealing with forms, These bots cause major problems like submitting fake data, damage your email reputation, and slow down websites. These bots are also a major problem on social media platforms like Twitter and Reddit, These bots spam posts, replies, and comments to users. To protect forms against spambots, Completely Automated Public Turing test to tell Computers and Humans Apart (CAPTCHA) is used. Google CAPTCHA and Cloudflare turnstile are the two most used captcha solutions. Cloudflare turnstile is a user-friendly captcha solution used to protect websites from spambots. The whole idea of Cloudflare turnstile is to provide an easy-to-use human verification without showing complex puzzles to visitors.

Why is it important to protect forms from spambots?

  • Spam and unwanted submissions: Spambots populate forms with unwanted data, these data fill up the database, making it difficult to manage the real data.

  • Slow down website: The activities of spambots flood the server with illegitimate traffic that can slow down the server and make the user experience very poor.

  • Manipulate analytics: These spambots can make the analytics data not reflect the real users' activities.

  • Damage email reputation: Bots can submit fake email addresses to your forms, which can affect the reputation of your email and lead to your emails ending up in the spam folder which is not good for marketing.

How to set up Cloudflare turnstile

To get started with this tutorial, create a Cloudflare account if you do not have one. There are two parts to setting up Cloudflare turnstile.

  1. Setting up the widget from the Cloudflare dashboard

  2. Adding the widget to your form

Setting up the widget from the Cloudflare dashboard

  1. Login to your Cloudflare dashboard

  2. Select turnstile from the side nav

    Select turnstile

  3. Click add widget and add the widget's name - this is any name you want. Click add hostname to add your domain or subdomain. This is the website's domain or subdomain containing the form to be protected. Note that this domain should not contain any scheme, port or path, for example https, http, 5000, /home. example.com and www.example.com are valid inputs.

    Add widget name

  4. Select the widget mode

    Select widget mode

    • Managed: this is the basic widget mode where the visitor would have to check the box that comes with the widget to be verified.

    • Non-interactive: in this non-interactive mode, the verification runs on its own, there is no need for the visitor to interact with the widget

    • Invisible: the widget does not appear on the form as a result of this, there is no interaction needed.

      The pre-clearance option should be left on the default selection, which is no.

  5. Create widget

    Create widget

    Click the create button after filling in the widget details, the application will take some time to create the widget, and the page containing your secret and site will show, copy the keys as these keys would be used to add the widget and verify the visitors.

Adding the widget to your form

In this part of the tutorial, Node.js and Express.js are used to create the application.

  1. To get started, create a new folder with any name of choice and initialize a new Node.js application inside the folder that you created.

     npm init -y
    

    This command sets all the prompts to the default value.

  2. Install the necessary packages.

     npm i express dotenv axios
    

    This command installs all the required packages needed for the application. Express is the Node.js framework, dotenv will be used to load the environment variables, axios for API requests. Nodemon is also required in the development environment, this package helps restart the server whenever a change is made.

     npm i -D nodemon
    
  3. On the root of the application directory, create an index.js file that will serve as the main entry point of the application. This is where the express server is set up.

     const express = require("express");
     const axios = require("axios");
     require('dotenv').config();
     const app = express();
    
     app.use(express.json());
     app.use(express.static("public"));
    
     const PORT = 5000;
     app.listen(PORT, ()=> console.log(`server started on ${PORT}`))
    

    This is a basic express server set up running on port 5000. The static files are located inside the public directory.

     "scripts": {
        "dev": "nodemon index.js"
     },
    

    update the package.json file with the following code, this enables nodemon to work properly.

  4. On the root directory, create a folder named public, this folder would contain all the frontend codes. Inside the public folder create three (3) files index.html, style.css, and index.js.

     <!DOCTYPE html> 
     <html lang="en">
    
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <link rel="stylesheet" href="style.css">
            <title>Cloudfare turnstile</title>
        </head>
    
        <body>
           <main>
               <form>
                   <div>
                       <label for="username">Username</label>
                       <input type="text" id="username" name="username" required>
                   </div>
                   <div>
                       <label for="email">Email</label>
                       <input type="email" id="email" name="email" required>
                   </div>
                   <div>
                       <label for="password">Password</label>
                       <input type="password" id="password" name="password" required>
                   </div>
    
                   <button>Submit</button>
               </form>
           </main>
           <script src="index.js"></script>
        </body>
     </html>
    

    This is a basic HTML form with three (3) input fields and a submit button.

     @import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap");
    
     * {
        padding: 0;
        margin: 0;
        box-sizing: border-box;
     }
    
     main {
          font-family: "Noto Sans", sans-serif;
          height: 100vh;
          width: 100vw;
          display: flex;
          align-items: center;
          justify-content: center;
          overflow: hidden;
          background-color: #eee;
     }
    
     form {
          width: 400px;
          background-color: white;
          padding: 16px;
          box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.301);
     }
    
     form div {
          display: flex;
          flex-direction: column;
          margin-bottom: 10px;
     }
    
     form > div > input {
          border: 1px solid #a0a0a0;
          height: 30px;
          border-radius: 6px;
          padding: 4px;
     }
    
     input:focus {
           outline: 1px solid #000;
           border: none;
     }
    
      button {
          width: 100%;
          background-color: #6294f1;
          color: #fff;
          border: none;
          padding: 10px 0;
          border-radius: 6px;
      }
    

    This is the CSS styling for the form.

    This what the complete form looks like

  5. The next step after creating the form is to add the Cloudflare turnstile widget to the form. First, add the Cloudflare turnstile script tag to the head of the document.

     <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=loadWidget" defer></script>
    

    This is the turnstile script tag, the onload query parameter is set to the function that adds the widget to the HTML, and the defer attribute is used to load the script before the page loads. Then add the turnstile HTML where the widget is to be displayed on the form. In this case, the widget would be displayed between the last input field and the submit button.

     <form>
             <div>
                 <label for="username">Username</label>
                 <input type="text" id="username" name="username" required>
             </div>
             <div>
                 <label for="email">Email</label>
                 <input type="email" id="email" name="email" required>
             </div>
             <div>
                 <label for="password">Password</label>
                 <input type="password" id="password" name="password" required>
             </div>
             <div id="turnstile_widget"></div> // turnstile widget
             <button>Submit</button>
      </form>
    

    Between the last input field and the submit button add a div with the id of turnstile_widget, this is where the widget would be rendered using JavaScript.

     function loadWidget() {
        turnstile.render("#turnstile_widget", {
           sitekey: "your site key",
           theme: "light",
           callback: async function (token) {},
        });
     }
    

    The loadWidget function handles the rendering of the widget on the HTML. This function must have the same name with was added as the value of the onload query parameter on the script tag. This function calls the turnstile.render() function that takes in the id of the div where the widget would be rendered as the first argument and an object that contains the site and other configurations as the second argument. The callback is the function that runs when the verification is completed.

    Complete form with verification

    At this stage, this is what the form looks like.

  6. The Cloudflare turnstile verification happens in two steps. The first is the front-end verification, where the visitor checks the box and the verification runs and returns a token that is cryptographically secured. The second is server-side validation, where the token returned from the front end is used for extra validation. This is to make sure that the token is not made up by a malicious visitor. The Cloudflare siteverify API is used for this verification.

The proper verification approach according to Cloudflare

  1. Send the turnstile token alongside the form data to the server when a visitor submits the form.

  2. Verify the turnstile token first.

  3. Handle the submission based on the Cloudflare siteverify API response.

Open public/index.js this is where the form submission is handled.

const form = document.querySelector("form");

form.addEventListener("submit", async (e) => {
        e.preventDefault();

        const formData = new FormData(e.target);
        const formProps = Object.fromEntries(formData);

        const res = await fetch("http://localhost:5000/register", {
           headers: {
               "Content-Type": "application/json",
           },
           method: "POST",
           body: JSON.stringify({
               username: formProps.username,
               email: formProps.email,
               password: formProps.password,
               cf_turnstile_response: formProps["cf-turnstile-response"], // turnstile token
           }),
        });
        if (res.ok) {
           const data = await res.json();
           // do whatever    
        } else {
           // handle error 
        }
 });

Open public/index.js this is where the form submission is handled. The form is submitted using the onSubmit event handler. The fetch API is used to make the POST request to the register endpoint of this application. The turnstile token is submitted together with the form credentials, this has the key of cf_turnstile_response.

async function validateToken(cf_turnstile_response, ip_address, secret_key) {
  const {data} = await axios.post(
     "https://challenges.cloudflare.com/turnstile/v0/siteverify",
       {
            secret: secret_key,
            response: cf_turnstile_response,
            remoteip: ip_address,
       }
     );
     return data.success
   }


app.post("/register", async (req, res) => {
   const { cf_turnstile_response, username, email, password } = req.body;
   const ip = req.ip;
   try {
       const isValidToken = await validateToken(cf_turnstile_response, ip, process.env.SECRET_KEY);
       if (isValidToken) {
         // registration logic with the user details
         res.status(200).json({ msg: "Registration success" });
       } else {
          res.status(400).json({ msg: "token verification failed" });
       }
    } catch (e) {
       res.status(500).json({ msg: "Something went wrong" });
    }
});

Open the root index.js and set up the registration endpoint. This is a POST endpoint that accepts the registration details and the turnstile token. As explained earlier, the turnstile token is validated first using Cloudflare siteverify API this is to make sure the token is authentic, and then the registration is handled based on the response from siteverify API. The siteverify API accepts four parameters.

ParameterRequired/optionalDescription
secretrequiredThis is the secret key associated with the widget on the front end.
responserequiredThis is the token returned by the widget on the front end.
remoteipoptionalThe IP address of the visitor/user.
idempotency_keyoptionalA UUID associated with the turnstile token.

Cloudflare turnstile does a very good job when it comes to protecting websites from malicious bots. Its approach to verification is very user-friendly as it does not give visitors a very complex puzzle to solve for them to be verified as humans. Other ways to protect forms from spambots include adding email verification, asking a simple human question, and adding hidden input fields known as honeypots. All these other methods come with their limitations, this makes the Cloudflare turnstile a better option when it comes to protecting against spambots.