{"id":50,"date":"2023-07-03T16:50:59","date_gmt":"2023-07-03T08:50:59","guid":{"rendered":"https:\/\/clifford.io\/blog\/?p=50"},"modified":"2024-05-06T07:55:37","modified_gmt":"2024-05-05T23:55:37","slug":"implement-cloudflare-turnstile-with-php","status":"publish","type":"post","link":"https:\/\/clifford.io\/blog\/implement-cloudflare-turnstile-with-php\/","title":{"rendered":"How to Implement Cloudflare Turnstile with PHP to Protect Your Webforms &#8211; A Super Simple Example"},"content":{"rendered":"\n<p>There are many (free) solutions to prevent SPAM coming through your webforms. One of the (common) ways is to use a CAPTCHA, such as <a href=\"https:\/\/www.google.com\/recaptcha\/\" target=\"_blank\" rel=\"noreferrer noopener\">reCAPTCHA<\/a> or <a href=\"https:\/\/www.hcaptcha.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">hCaptcha<\/a>. One main problem that CAPTCHAs bring is a non-intuitive user experience. Challenges are hard to solve, sometimes impossible to solve (even after switching to audio mode! Hey Amazon).<\/p>\n\n\n\n<p>To tackle this problem, Cloudflare has developed a CAPTCHA alternative called <a href=\"https:\/\/www.cloudflare.com\/products\/turnstile\/\" target=\"_blank\" rel=\"noreferrer noopener\">Turnstile<\/a>. Turnstile does not require user interaction and yet can validate that your users are real. Also, you do not need to be routing your network via Cloudflare to be able to use Turnstile.<\/p>\n\n\n\n<p>In this post, I will show you how to use Cloudflare Turnstile with PHP in an example to secure a basic web form. This tutorial is intentionally made simple and does not require any framework of library to implement. You can however adapt it anyway you want. Do note that Cloudflare Turnstile is still in beta, as of the date of this writing.<\/p>\n\n\n\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_60 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title \" >Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\" role=\"button\"><label for=\"item-69f19111003ad\" ><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/label><input aria-label=\"Toggle\" aria-label=\"item-69f19111003ad\"  type=\"checkbox\" id=\"item-69f19111003ad\"><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/clifford.io\/blog\/implement-cloudflare-turnstile-with-php\/#Step_1_Register_for_a_Cloudflare_account_and_sign_in\" title=\"Step 1 : Register for a Cloudflare account and sign in\">Step 1 : Register for a Cloudflare account and sign in<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/clifford.io\/blog\/implement-cloudflare-turnstile-with-php\/#Step_2_Click_on_Turnstile_and_add_new_site\" title=\"Step 2 : Click on Turnstile and add new site\">Step 2 : Click on Turnstile and add new site<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/clifford.io\/blog\/implement-cloudflare-turnstile-with-php\/#Step_3_Setting_up_the_web_form\" title=\"Step 3 : Setting up the web form\">Step 3 : Setting up the web form<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/clifford.io\/blog\/implement-cloudflare-turnstile-with-php\/#Step_4_Setting_up_server-side_validation_with_PHP\" title=\"Step 4 : Setting up server-side validation with PHP\">Step 4 : Setting up server-side validation with PHP<\/a><\/li><\/ul><\/nav><\/div>\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_1_Register_for_a_Cloudflare_account_and_sign_in\"><\/span>Step 1 : Register for a Cloudflare account and sign in<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>This is pretty simple. Just go to <a href=\"https:\/\/www.cloudflare.com\/products\/turnstile\/\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/www.cloudflare.com\/products\/turnstile\/<\/a> and click &#8220;Sign Up For Free&#8221;. Once done, login to your account. You will be redirected to the Cloudflare dashboard.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_2_Click_on_Turnstile_and_add_new_site\"><\/span>Step 2 : Click on Turnstile and add new site<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Next, on the left hand menu, click on &#8220;Turnstile&#8221; and click the &#8220;Add Site&#8221; button. Follow the on-screen instructions to add your site name.<\/p>\n\n\n\n<p>Under &#8220;Domain&#8221;, you can enter all the domains that you want to use Turnstile on. In my case, I added clifford.io and clifford.local (dev env).<\/p>\n\n\n\n<p>Under &#8220;Widget Mode&#8221;, there are 3 options. Below is a quick explanation of each option. Note that while each option is different, the implementation method is the same. You can switch the widget mode at any time from the dashboard.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Managed<\/strong> &#8211; this is the only option that Cloudflare <span style=\"text-decoration: underline;\">may<\/span> deterministically use user information to show a checkbox for users to check. Don&#8217;t worry, no hard image or audio challenges.<\/li>\n\n\n\n<li><strong>Non-interactive<\/strong> &#8211; for this option, user will only see a loading indicator while Turnstile is running the browser challenge behind the scenes.<\/li>\n\n\n\n<li><strong>Hidden<\/strong> &#8211; this option is completely invisible to users.<\/li>\n<\/ul>\n\n\n\n<p>For this tutorial, let&#8217;s go with the &#8220;Managed&#8221; option. Click &#8220;Create&#8221;.<\/p>\n\n\n\n<p>You should now see a Site Key and a Secret Key. The Site Key is what you will use to embed in your web form. The Secret Key is what you will use to authenticate the request with Cloudflare on the server side. <\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"628\" src=\"https:\/\/clifford.io\/blog\/wp-content\/uploads\/2023\/07\/cf-create-site.png\" alt=\"Cloudflare site and secret key\" class=\"wp-image-53\" srcset=\"https:\/\/clifford.io\/blog\/wp-content\/uploads\/2023\/07\/cf-create-site.png 800w, https:\/\/clifford.io\/blog\/wp-content\/uploads\/2023\/07\/cf-create-site-300x236.png 300w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/figure>\n\n\n\n<p>Please store the Secret Key safely and do not share it. If you think your Secret Key has been compromised or leaked, you can return to the dashboard to reset it. Just click on &#8220;Rotate Secret Key&#8221;.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"572\" src=\"https:\/\/clifford.io\/blog\/wp-content\/uploads\/2023\/07\/cf-rotate-key.png\" alt=\"cloudflare turnstile with php\" class=\"wp-image-54\" srcset=\"https:\/\/clifford.io\/blog\/wp-content\/uploads\/2023\/07\/cf-rotate-key.png 800w, https:\/\/clifford.io\/blog\/wp-content\/uploads\/2023\/07\/cf-rotate-key-300x215.png 300w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/figure>\n\n\n\n<p>With the Site and Secret Keys on hand, let&#8217;s jump to the implementation!<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_3_Setting_up_the_web_form\"><\/span>Step 3 : Setting up the web form<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>First, we will set up a simple web form from scratch, with a single form field to simulate submission of form data after the Turnstile challenge. As the Turnstile widget is in &#8220;Managed&#8221; mode, you will see a physical widget right appearing below that single form field, above the submit button. Of course, you can place the widget wherever you like. See a demo <a href=\"https:\/\/clifford.io\/demo\/cloudflare-turnstile\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/p>\n\n\n\n<p><strong>Here&#8217;s the full code for the web form page &#8211; index.php<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;!doctype html>\n&lt;html lang=\"en\">\n  &lt;head>\n    &lt;meta charset=\"utf-8\">\n    &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    &lt;meta name=\"description\" content=\"\">\n    &lt;meta name=\"author\" content=\"Cliff\">\n    &lt;title>Cloudflare Turnstile Demo&lt;\/title>\n    &lt;style>\n      body{\n        background: #ffffff;\n        box-sizing:border-box;\n        font-size:16px;\n      }\n      form{\n        width:100%;\n        max-width:500px;\n        margin:auto;\n        border:1px solid #cccccc;\n        border-radius:12px;\n        padding:20px;\n      }\n      h1{\n        text-align:center;\n        max-width:500px;\n        margin:auto;\n      }\n      p{\n        text-align:center;\n        max-width:500px;\n        margin:10px auto;\n      }\n      input[type=text]{\n        width:80%;\n        padding:10px;\n        margin-bottom:10px;\n      }\n      label{\n        font-weight:bold;\n        margin-bottom:10px;\n        width:100%;\n        display:block;\n      }\n      button{\n        padding:10px;\n        margin-top:10px;\n      }\n    &lt;\/style>\n    &lt;!-- include the cloudflare turnstile library here  -->\n    &lt;script src=\"https:\/\/challenges.cloudflare.com\/turnstile\/v0\/api.js\" defer>&lt;\/script>\n  &lt;\/head>\n&lt;body>\n  &lt;h1>Cloudflare Turnstile Simple PHP Demo&lt;\/h1>\n  &lt;p>This is an example of the Cloudflare Turnstile &lt;b>managed mode&lt;\/b> widget implemented with PHP validation on the server side.&lt;\/p>\n  &lt;p>Field 1 is just a regular field in your form.&lt;\/p>\n  &lt;form action=\"submit.php\" method=\"post\">\n      &lt;label for=\"field1\">Field 1&lt;\/label>\n      &lt;input type=\"text\" name=\"field1\" id=\"field1\">\n      &lt;!-- your site key goes here -->\n      &lt;div class=\"cf-turnstile\" data-sitekey=\"[use your own site key]\">&lt;\/div>\n      &lt;button type=\"submit\">Submit&lt;\/button>\n  &lt;\/form>\n\n&lt;\/body>\n&lt;\/html>\n<\/pre>\n\n\n\n<p>From the code above, take note of these 2 important lines:<\/p>\n\n\n\n<p>Important Note 1 : The Cloudflare Turnstile Javascript library must be included.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;script src=\"https:\/\/challenges.cloudflare.com\/turnstile\/v0\/api.js\" defer>&lt;\/script><\/pre>\n\n\n\n<p>Important Note 2 : The HTML div element with <strong><span style=\"text-decoration: underline;\">your own Site Key<\/span><\/strong> must be included somewhere in your web form.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;div class=\"cf-turnstile\" data-sitekey=\"[use your own site key]\">&lt;\/div><\/pre>\n\n\n\n<p>In this step, Turnstile will generate a &#8220;cf-turnstile-response&#8221; hidden form field to be submitted with your web form. This will need to be retrieved in the next step for server-side validation. Let&#8217;s go to step 4.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_4_Setting_up_server-side_validation_with_PHP\"><\/span>Step 4 : Setting up server-side validation with PHP<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>In this step, we will send a HTTP POST request to Cloudflare servers to perform the Turnstile challenge. This requires a few information:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Your Secret Key<\/li>\n\n\n\n<li>The remote IP address of the server making the request ($_SERVER[&#8216;REMOTE_ADDR&#8217;])<\/li>\n\n\n\n<li>Response from the hidden field in the previous step ($_POST[&#8216;cf-turnstile-response&#8217;])<\/li>\n<\/ul>\n\n\n\n<p><strong>Here&#8217;s the full code for the web form submission page &#8211; submit.php<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"php\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;?php\n    $secret = '[your own secret key]'; \/* Store this somewhere secure *\/\n    $remote_addr = $_SERVER['REMOTE_ADDR'];\n    $cf_url = 'https:\/\/challenges.cloudflare.com\/turnstile\/v0\/siteverify';\n    $token = $_POST['cf-turnstile-response'];\n\n    \/\/ Request data\n    $data = array(\n        \"secret\" => $secret,\n        \"response\" => $token,\n        \"remoteip\" => $remote_addr\n    );\n\n    \/\/ Initialize cURL\n    $curl = curl_init();\n\n    \/\/ Set the cURL options\n    curl_setopt($curl, CURLOPT_URL, $cf_url);\n    curl_setopt($curl, CURLOPT_POST, true);\n    curl_setopt($curl, CURLOPT_POSTFIELDS, $data);\n    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);\n\n    \/\/ Execute the cURL request\n    $response = curl_exec($curl);\n\n    \/\/ Check for errors\n    if (curl_errno($curl)) {\n        $error_message = curl_error($curl);\n        \/\/ Handle the error the way you like it\n        echo 'cURL Error: ' . $error_message.'&lt;br>';\n    }else{\n        \/* Parse Cloudflare's response and check if there are any validation errors *\/\n        $response = json_decode($response,true);\n        if ($response['error-codes'] &amp;&amp; count($response['error-codes']) > 0){\n            echo 'Cloudflare Turnstile check failed. Error codes:&lt;br>';\n            echo '&lt;ul>';\n            foreach($response['error-codes'] as $e){\n                echo '&lt;li>'.$e.'&lt;\/li>';\n            }\n            echo '&lt;\/ul>';\n            echo '&lt;br>&lt;br>Output from Cloudflare:&lt;br>&lt;br>';\n            print_r($response);\n        }else{\n            echo 'Passed Cloudflare Turnstile check.&lt;br>&lt;br>Output from Cloudflare:&lt;br>&lt;br>';\n            print_r($response);\n            echo '&lt;hr>';\n            \/\/ Process the response\n            echo 'The submitted form data is : '.$_POST['field1'];\n        }\n    }\n\n    \/\/ Close cURL\n    curl_close($curl);\n?><\/pre>\n\n\n\n<p>Perform the HTTP POST request with the required information and you should get the response from Cloudflare. If the challenge passes, just process the form submission.<\/p>\n\n\n\n<p>That&#8217;s all! Here&#8217;s the <a href=\"https:\/\/clifford.io\/demo\/cloudflare-turnstile\" target=\"_blank\" rel=\"noreferrer noopener\">demo<\/a> again. Enjoy!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>There are many (free) solutions to prevent SPAM coming through your webforms. One of the (common) ways is to use a CAPTCHA, such as reCAPTCHA or hCaptcha. One main problem that CAPTCHAs bring is a non-intuitive user experience. Challenges are hard to solve, sometimes impossible to solve (even after switching to audio mode! Hey Amazon). <a href=\"https:\/\/clifford.io\/blog\/implement-cloudflare-turnstile-with-php\/\" class=\"more-link\">&#8230;<span class=\"screen-reader-text\">  How to Implement Cloudflare Turnstile with PHP to Protect Your Webforms &#8211; A Super Simple Example<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":64,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[15,8],"class_list":["post-50","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-web-development","tag-captcha","tag-php"],"_links":{"self":[{"href":"https:\/\/clifford.io\/blog\/wp-json\/wp\/v2\/posts\/50","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/clifford.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/clifford.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/clifford.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/clifford.io\/blog\/wp-json\/wp\/v2\/comments?post=50"}],"version-history":[{"count":5,"href":"https:\/\/clifford.io\/blog\/wp-json\/wp\/v2\/posts\/50\/revisions"}],"predecessor-version":[{"id":229,"href":"https:\/\/clifford.io\/blog\/wp-json\/wp\/v2\/posts\/50\/revisions\/229"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/clifford.io\/blog\/wp-json\/wp\/v2\/media\/64"}],"wp:attachment":[{"href":"https:\/\/clifford.io\/blog\/wp-json\/wp\/v2\/media?parent=50"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/clifford.io\/blog\/wp-json\/wp\/v2\/categories?post=50"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/clifford.io\/blog\/wp-json\/wp\/v2\/tags?post=50"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}