DEV Community

Cover image for Cloudflare Turnstile plugin for Deno Fresh (p1)
Duy K. Bui
Duy K. Bui

Posted on

Cloudflare Turnstile plugin for Deno Fresh (p1)

TL/DR: If you read the title and know exactly what I mean already, simply go to https://deno.land/x/fresh_turnstile and follow the README.

DISCLAIMER: Hi all, this is my first post after 3 years of reading exclusively on dev.to, so all criticisms are welcome! Coincidentally, I'm also writing about my first open-source project release, so again provide feedback by all means.

Background

Alright, now that the "noob" disclaimer is out of the way, let me quickly clarify all the nouns I mentioned in the post title:

  1. Cloudflare: CDN, DOS-protection, 1.1.1.1, VPN, etc.
  2. Turnstile: Cloudflare's recently released CAPTCHA alternative.
  3. Deno: The hopeful successor of NodeJS.
  4. Fresh: Deno's recently released web framework with a focus on island architecture.
  5. plugin: 3rd-party library to inject CSS and JS into every page in Fresh.
  6. CAPTCHA (not in the title, but for completeness' sake): a common instrument to guard against machine-driven web interactions.

OK, class is dismissed; you will be quizzed on these terms next week.

Kidding 😂

Now that we are absolutely on the same page (we are, right?) about what I made, let's talk about how it's supposed to be used.

In this first release, I'm covering three basic use cases of Turnstile:

  1. Protecting a traditional form submission.
  2. Protecting a form submitted with AJAX / fetch / etc.
  3. Protecting a custom interaction.

Use Case 1: traditional form submission.

Err not so fast, before you can do anything with the plugin you need to have your Turnstile configured, which leads us to ...

Prerequisites!

  1. You need to give up an email address (to Cloudflare, not me).
  2. You need a Cloudflare account (not my call, don't argue).
  3. You need a Turnstile site (free for now, again not my call).

Follow the instructions on the official page and you will be good to go in 30 seconds: https://www.cloudflare.com/lp/turnstile/

Oh, and you also need a Fresh app (not any fresh app, the Deno Fresh app, ok?).

Simply run this command to create one if you haven't.

deno run -A -r https://fresh.deno.dev sample-app
Enter fullscreen mode Exit fullscreen mode

Now that you have created your Fresh app, add fresh-turnstile to your import_map.json for convenience:

{
  "imports": {
    "$turnstile/": "https://deno.land/x/fresh_turnstile@1.0.0-0/"
  }
}
Enter fullscreen mode Exit fullscreen mode

Then consume the plugin in your app's main.ts.

import { TurnstilePlugin } from "$turnstile/index.ts";

await start(manifest, {
  plugins: [
    // ...
    TurnstilePlugin(),
    // ...
  ],
});
Enter fullscreen mode Exit fullscreen mode

Now you are ready to use the plugin.

Use case 1 (for real this time)

This is the most common use case for Turnstile.

The basic idea is you have a form, such as a "leave us a note" form, which you only want human beings to be able to submit as that should largely reduce the amount of spam you receive from the form.

To achieve that with Turnstile, on the client side, you put a Turnstile widget inside your form, which will talk to Turnstile API, do the magic, and decide whether the actor is a human. There are several modes of operation for the widget: interactive, non-interactive, and completely hidden, which you chose when you created the Turnstile site in your Cloudflare dashboard. Once the widget has verified (or it thinks it has) that the actor about to submit the form is a human, it will append a hidden input to your form with a token from Turnstile, which gets included as the form is submitted. Then, on the server side, you can validate the token by making a call to the Turnstile API.

This plugin provides a <CfTurnstile/> component for you to use on the client side (fill in sitekey from your Turnstile site, which can always be retrieved from your Cloudflare dashboard):

import CfTurnstile from "$turnstile/components/CfTurnstile.tsx";
// ...
<form action="..." method="POST">
  // other form fields such as name, email, message, etc.
  <CfTurnstile sitekey="..." />
  <input type="submit" />
</form>
Enter fullscreen mode Exit fullscreen mode

Once the page is fully rendered, <CfTurnstile/> will be replaced with a DOM tree similar to this:

<div class="cf-turnstile">
  <iframe><!-- THE WIDGET --><iframe>
  <input type="hidden" name="cf-turnstile-response" value="<!-- THE TOKEN -->" />
</div>
Enter fullscreen mode Exit fullscreen mode

As the form gets submitted, you can look for cf-turnstile-response in your route handler and follow Turnstile's instructions for validation.

Or, if your form is submitted to a POST route, you can use the provided handler generator like this (again, fill in cf_turnstile_secret_key with your Turnstile site's secret, also retrievable from your Cloudflare dashboard):

import { CfTurnstileValidationResult, generatePostHandler } from "$turnstile/handlers/CfTurnstileValidation.ts";

import Page from "$flowbite/components/Page.tsx";

export const handler = { POST: generatePostHandler(cf_turnstile_secret_key) };

export default function CfTurnstileValidation({ data }: PageProps<CfTurnstileValidationResult | null>) {
  /* 3 scenarios can occur here:
   * 1. data is null => the form was not submitted correctly, or the secret key was not provided.
   * 2. data.success is false => the form was submitted correctly, but validation failed. data["error-codes"] should be a list of error codes (as strings).
   * 3. data.success is true => the form was submitted correctly and validated successfully. data.challenge_ts and data.hostname should be available for inspection.
   */
}
Enter fullscreen mode Exit fullscreen mode

A side note on the interactivity of the widget:

If you look at the <CfTurnstile/> component source code, you will not see an IS_BROWSER guard, which means that it will be interactive even outside an island. That might sound like a violation of the island architecture at first, but with implicit rendering, the actual augmentation of the widget on top of the placeholder <div/> is done by Turnstile's own JavaScript, making it very difficult to render a "disabled" widget.

Therefore, I made a decision to leave that alone and rely on the plugin consumers' mental efforts to only use this component inside their islands. If you strongly believe that this decision is a mistake, please file an issue on the plugin repo:
https://github.com/khuongduybui/fresh-turnstile/issues/new


That's it for tonight; thank you very much for your time.
In the following posts, I will discuss the remaining use cases.

Top comments (1)

Collapse
 
naltun profile image
Noah

Wonderful writeup. 🎉👏