DEV Community

Duy K. Bui
Duy K. Bui

Posted on • Edited on

Cloudflare Turnstile plugin for Deno Fresh (p2)

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.

This is part 2 of a series about my Deno Fresh plugins. If you are not sure about some of the topics mentioned here, you are very likely to find them explained in part 1. In this post, I will continue to discuss the remaining use cases supported by the first release of my Turnstile plugin.

Use Case 2: JavaScript-based form submission.

In the previous post, we discussed how to protect a form traditionally submitted. Now imagine the same form, but submitted via AJAX / fetch / REST APIs / GraphQL / etc.

You can use the implicit rendered <CfTurnstile/> component similar to use case 1, but this time we will add an ID and a callback property.

import { useId, useState } from "preact/hooks";
import CfTurnstile from "$turnstile/components/CfTurnstile.tsx";
// ...
const turnstileElementId = useId();
const [turnstileToken, setTurnstileToken] = useState("");
// ...
<CfTurnstile id={turnstileElementId} sitekey="..." callback={setTurnstileToken} />
Enter fullscreen mode Exit fullscreen mode

By simply adding the callback, you can easily access the token at your turnstileToken state variable, which will be set as soon as the Turnstile widget decides that the actor is a human.

However, the token can only be validated at most once, so if the form submission fails for some reason when validating on the server side, you will need a way to retrieve another token. That's where the ID is useful. Get access to the turnstile object with the getTurnstileAsync helper function and call reset with the ID.

import { getTurnstileAsync } from "$turnstile/plugin.ts";
// ... imagine this is inside your form submission action
try {
  if (!turnstileToken) {
    throw new Error("Turnstile is not ready");
  }
  await submit_the_form_with_ajax_or_fetch({
    // other form fields
    "cf-turnstile-response": turnstileToken
  });
} catch (e) {
  // form submission fails, get a new Turnstile token to be ready for the next try
  setTurnstileToken("");
  const turnstile = await getTurnstileAsync();
  turnstile.reset(turnstileElementId); 
}
Enter fullscreen mode Exit fullscreen mode

Now your form is ready for the next submission without having to reload / re-render the page.

Use Case 3: custom interactions.

By now you should have had a fairly good idea on how to do this already, but for completeness's sake let's put it down in writing so that there won't be any misunderstanding.

Similar to the previous case, use the component with an ID and a callback. To demonstrate the idea that you can use the component as freely as any preact component you write yourself, I will use a signal in this example instead of a state variable.

import { useId } from "preact/hooks";
import { useSignal } from "@preact/signals";
import CfTurnstile from "$turnstile/components/CfTurnstile.tsx";
// ...
const turnstileElementId = useId();
const turnstileToken = useSignal("");
// ...
<CfTurnstile id={turnstileElementId} sitekey="..." callback={(token) => turnstileToken.value = token} />
Enter fullscreen mode Exit fullscreen mode

And again, inspect your signal and use the getTurnstileAsync be able to reset the widget as needed.

import { getTurnstileAsync } from "$turnstile/plugin.ts";
// ... imagine this is inside your custom interaction
try {
  if (!turnstileToken.value) {
    throw new Error("Turnstile is not ready");
  }
  await validate_token_on_server_side({
    "token": turnstileToken.value}
  );
} catch (e) {
  // validation fails, get a new Turnstile token to be ready for the next try
  turnstileToken.value = "";
  const turnstile = await getTurnstileAsync();
  turnstile.reset(turnstileElementId); 
}
Enter fullscreen mode Exit fullscreen mode

Note that in order to validate the token, you need to make a call to Turnstile API with your Turnstile site's secret key (see official instructions here). It is not a good idea to retrieve this secret client-side at any point in time. Instead, I highly recommend sending the token to the server side to perform the validation step.

BONUS: explicit rendering

The token from Turnstile has a relatively short validity period (5 minutes in v0). Therefore, if you want to use it with a blog post, an email, or an otherwise time-consuming form, you may want to reset the widget right before form submission.

Alternatively, you may choose to control when the widget gets rendered and time it according to your specificities. The idea is similar to what we've done before: leveraging the useId hook and the getTurnstileAsync helper. I am going to show a very simple example resembling use case 1 except that our user needs to click the "Check my form" button first before zhe can "submit" it.

import { useId } from "preact/hooks";
import { useSignal, computed } from "@preact/signals";
// ...
const turnstileElementId = useId();
const turnstileToken = useSignal("");
const formReady = computed(() => turnstileToken !== "");
const startTurnstile = async () => {
  const turnstile = await getTurnstileAsync();
  turnstile.render(
    turnstileElementId,
    {
      sitekey: "...",
      callback: (token) => turnstileToken.value = token
    }
  ); 
// ...
<form action="..." method="POST">
  // other form fields such as name, email, message, etc.
  <div id={turnstileElementId} />
  <button type="button" onClick={startTurnstile}>Check my form</button>
  <input type="submit" disabled={!formReady} />
</form>
Enter fullscreen mode Exit fullscreen mode

Once the "check my form" button is clicked, the widget rendering will kick in. Once it has determined that the actor is a human, it will invoke the callback with the token, which is sent to the turnstileToken signal, which is chained to the formReady signal, turning the "submit" button from its disabled state into enabled. At this point, you have the token to use dynamically as needed; you also have the hidden input named cf-turnstile-token inside your <div/> ready to be submitted alongside your other form fields.

This approach is very flexible. I'm sure you will find a creative way to solve most of your captcha use cases with this approach if the basic use cases don't fit your bill. Feel free to drop a line and share your insight, or prove me wrong with an unsolvable use case, I challenge you 😉.


That's it, my friends. Thank you for staying with me till the end of it. As I've stated in the beginning this is the first release, so I am sure there are a lot more things I have not covered. Feel free to start a discussion or file an issue! You may also hit me up on dev.to messaging anytime too!

Top comments (1)

Collapse
 
dunkbing profile image
dunkbing

Can a fresh app be hosted on cloudflare?