From 628c8523cfb186a8b48698f4e3e956d113313bc3 Mon Sep 17 00:00:00 2001 From: Hidetaka Okamoto Date: Thu, 7 Nov 2024 15:41:13 +0900 Subject: [PATCH 1/4] Adding a new tutorial for Turnstile [Developer Spotlight program] --- src/assets/images/turnstile/payment-form.png | Bin 0 -> 69301 bytes ...rm-from-attackers-bots-using-turnstile.mdx | 582 ++++++++++++++++++ 2 files changed, 582 insertions(+) create mode 100644 src/assets/images/turnstile/payment-form.png create mode 100644 src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx diff --git a/src/assets/images/turnstile/payment-form.png b/src/assets/images/turnstile/payment-form.png new file mode 100644 index 0000000000000000000000000000000000000000..fd559c25c4ca711d2d70a2a726d73e7c3105fe66 GIT binary patch literal 69301 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 I?xxTG0GiXO6aWAK literal 0 HcmV?d00001 diff --git a/src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx b/src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx new file mode 100644 index 000000000000000..8453ca41c7c6a92 --- /dev/null +++ b/src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx @@ -0,0 +1,582 @@ +--- +title: Protecting your payment form from attackers' bots using Turnstile +pcx_content_type: tutorial +updated: 2024-11-11 +difficulty: Beginner +content_type: ๐Ÿ“ Tutorial +languages: + - JavaScript +tags: + - Node.js + - Hono + - Stripe +spotlight: + author: Hidetaka Okamoto + author_bio_link: https://www.linkedin.com/in/hideokamoto/ + author_bio_source: LinkedIn +sidebar: + order: 2 + +--- + +import { Render, TabItem, Tabs } from "~/components"; + +This tutorial shows how you can build a more secure payment form using Turnstile. You can learn how to block bot access on the checkout page and trigger additional authentication flows by integrating Turnstile with Stripe + + +## Before you begin + +- You must have a Cloudflare account +- You must have a Stripe account + + +## Get Your Turnstile sitekey and secret key + +1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com/) and select your account. +2. Go to **Turnstile** and [create a new Turnstile widget](/turnstile/get-started/). +3. Copy the sitekey and the secret key to use in the next step. + +## 1. Create a new Worker project + + +First, let's create a Cloudflare Workers project. + + + +To efficiently create and manage multiple APIs, let's use [`Hono`](https://hono.dev). Hono is an open-source application framework released by a Cloudflare Developer Advocate. It is lightweight and allows for the creation of multiple API paths, as well as efficient request and response handling. +Open your command line interface (CLI) and run the following command: + + + +```sh +npm create cloudflare@latest secure-payment-form -- --framework hono +``` + + + +```sh +yarn create cloudflare@latest secure-payment-form -- --framework hono +``` + + + +```sh +pnpm create cloudflare@latest secure-payment-form -- --framework hono +``` + + + +If this is your first time running the `C3` command, you will be asked whether you want to install it. Confirm that the package name for installation is `create-cloudflare` and answer `y`. + +```sh +Need to install the following packages: +create-cloudflare@latest +Ok to proceed? (y) +``` + +Additionally, you need to install the `create-hono` package. + +```sh +Need to install the following packages: +create-hono@0.14.2 +Ok to proceed? (y) y +``` + + +During the setup, you will be asked if you want to manage your project source code with `Git`. It is recommended to answer `Yes` as it helps in recording your work and rolling back changes. You can also choose `No`, which will not affect the tutorial progress. + +```sh +โ•ฐ Do you want to use git for version control? +โ€Šโ€ŠYes / No +``` + +Finally, you will be asked if you want to deploy the application to your Cloudflare account. For now, select `No` and start development locally. + +```sh +โ•ญ Deploy with Cloudflare Step 3 of 3 +โ”‚ +โ•ฐ Do you want to deploy your application? +โ€Šโ€ŠYes / No +``` + +If you see a message like the one below, the project setup is complete. You can open the `secure-payment-form` directory in your preferred IDE to start development. + +```sh + +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +๐ŸŽ‰ SUCCESS Application created successfully! + +๐Ÿ’ป Continue Developing +Change directories: cd secure-payment-form +Start dev server: npm run dev +Deploy: npm run deploy + +๐Ÿ“– Explore Documentation +https://developers.cloudflare.com/workers + +๐Ÿ› Report an Issue +https://github.com/cloudflare/workers-sdk/issues/new/choose + +๐Ÿ’ฌ Join our Community +https://discord.cloudflare.com +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +``` + +Cloudflare Workers applications can be developed and tested in a local environment. On your CLI, change directory into your newly created Workers and run `npx wrangler dev` to start the application. Using `Wrangler`, the application will start, and you'll see a URL beginning with `localhost`. + +```sh + โ›…๏ธ wrangler 3.84.1 +------------------- + +โŽ” Starting local server... +[wrangler:inf] Ready on http://localhost:8787 +โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ [b] open a browser โ”‚ +โ”‚ [d] open devtools โ”‚ +โ”‚ [l] turn off local mode โ”‚ +โ”‚ [c] clear console โ”‚ +โ”‚ [x] to exit โ”‚ +โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ + +``` + +You can send a request to the API using the `curl` command. If you see the text `Hello Hono!`, the API is running correctly. + +```sh +curl http://localhost:8787 +``` + +```sh output +Hello Hono! +``` + +So far, we've covered how to create a Cloudflare Worker project and introduced tools and open-source projects like the `C3` command and the `Hono` framework that streamline development with Cloudflare. Leveraging these features will help you develop applications on Cloudflare Workers more smoothly. + +## 2. Preparation for the building web application + +By the default, we need to update the Hono project for supporting to web application. + +### Change the file extension to support JSX + +Since we'll use JSX to dynamically create HTML, let's change `src/index.ts` to `src/index.tsx`. + +``` +mv src/index.ts src/index.tsx +``` + +At the same time, let's change the filename specified in the `wrangler.toml`. + +```diff +#:schema node_modules/wrangler/config-schema.json +name = "secure-payment-form" +-main = "src/index.ts" ++main = "src/index.tsx" +compatibility_date = "2024-10-22" +``` + +### Register Stripe API key and Turnstile key as environment variables + +Let's register API keys as environment variables to use both Stripe and Cloudflare Turnstile. + +You can obtain test site keys and secret keys for Cloudflare Turnstile from the documentation. + +https://developers.cloudflare.com/turnstile/reference/testing/ + +Get the publishable key and secret key for Stripe from the Stripe dashboard. + +https://dashboard.stripe.com/test/apikeys + +And place each keys into the `.dev.vars` file like the following: + +``` +TURNSTILE_SITE_KEY = '1x00000000000000000000AA' +TURNSTILE_SECRET_KEY = '1x0000000000000000000000000000000AA' +STRIPE_PUBLISHABLE_KEY='Publishable key starting with pk_test_' +STRIPE_SECRET_KEY='Secret key starting with sk_test_' +``` + +After that, you can generate TypeScript type definition by running the `npm run cf-typegen` command. +```sh +$ npm run cf-typegen + +Generating project types... + +interface CloudflareBindings { + TURNSTILE_SITE_KEY: string; + TURNSTILE_SECRET_KEY: string; + STRIPE_PUBLISHABLE_KEY: string; + STRIPE_SECRET_KEY: string; +} +``` + +In local development using Hono and Wrangler, you can retrieve values set in `.dev.vars` like this: + +```ts +app.get('/hello', async c => { + console.log(c.env.TURNSTILE_SITE_KEY); + return c.json({ message: 'test' }); +}) +``` + +Now we're ready for application development. In the next steps, we'll develop a payment form using Turnstile and Stripe. + +## 3. Implementing Bot Detection with Turnstile + +Let's start by creating a form that uses Turnstile to detect bot access. Add the following code to `src/index.tsx` to create a simple form: + +```ts +import { Hono } from "hono"; + +const app = new Hono<{ + Bindings: CloudflareBindings; +}>(); + +app.get("/", async (c) => { + return c.html( +

+
+
+
+ +
+
, + ); +}); + +export default app; + +``` + +Let's add JavaScript code to our application to implement bot detection using Turnstile. +By adding this implementation, the order form submission process will be disabled until the Turnstile bot detection process is completed and it is confirmed that the access is not from a bot. + +```diff +import { Hono } from "hono"; ++import { env } from "hono/adapter"; ++import { html } from "hono/html"; + +const app = new Hono<{ + Bindings: CloudflareBindings; +}>(); + +app.get("/", async (c) => { + const { TURNSTILE_SITE_KEY } = env(c); + return c.html( +
+
+
+
++
++ +- +
++ {html` ++ ++ ++ `} +
, + ); +}); + +export default app; + +``` + +Here, we're loading the Turnstile script file with a `script` tag. The `_turnstileCB` function is executed when the script file loading is complete, triggered by the `onload=_turnstileCB` in the query string. + +In the `_turnstileCB` function, `turnstile.render()` is executed. The `callback` set here removes the `disabled` attribute from the submit `button` of the `form`. + +This simple implementation prevents order operations for accesses that Cloudflare identifies as bots. + +## 4. Integrating Turnstile with a Stripe Payment Form + +For a more practical example, let's integrate Turnstile with a Stripe payment form. First, install the Stripe SDK: + + + +```sh +npm install stripe +``` + + + +```sh +yarn add stripe +``` + + + +```sh +pnpm add stripe +``` + + + +Next, let's implement the code to create a payment form in `src/index.tsx`. +First, create a [Payment Intent](https://docs.stripe.com/api/payment_intents) on the server side: + +```diff +import { Hono } from "hono"; +import { env } from "hono/adapter"; +import { html } from "hono/html"; ++import Stripe from "stripe"; + +const app = new Hono<{ + Bindings: CloudflareBindings; +}>(); + +app.get("/", async (c) => { +- const { TURNSTILE_SITE_KEY } = env(c); ++ const { TURNSTILE_SITE_KEY, STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY } = + env(c); ++ const stripe = new Stripe(STRIPE_SECRET_KEY, { ++ apiVersion: "2024-10-28.acacia", ++ appInfo: { ++ name: "example/cloudflare-turnstile", ++ }, ++ }); ++ const paymentIntent = await stripe.paymentIntents.create({ ++ amount: 100, ++ currency: "usd", ++ }); + return c.html( + +``` + +And adding JavaScript code to display the payment form. Edit `src/index.tsx`: + +```diff + {html` ++ + + + `} +``` + +The payment form is now ready. To experience how it behaves when a bot is detected, change `.dev.vars` as follows: + +```diff +-TURNSTILE_SITE_KEY = '1x00000000000000000000AA' ++TURNSTILE_SITE_KEY = '2x00000000000000000000AB' +``` + +If you restart the application now, you'll notice that you can't submit the payment form. + +![Failed challenge](~/assets/images/turnstile/payment-form.png) + +This way, you can block requests trying to manipulate the payment form using bots, such as card testing attacks. +By verifying whether the `turnstileToken` is set by the `callback` of `turnstile.render()`, you can use Turnstile's result when processing the `form`'s `submit` event. + +Note: After completing the tests, let's make sure to revert the Turnstile SITE_KEY back to its original value. +```diff ++TURNSTILE_SITE_KEY = '1x00000000000000000000AA' +-TURNSTILE_SITE_KEY = '2x00000000000000000000AB' +``` + +## 5. Adding Server-Side Validation + +Let's add a step to verify that the token generated by the Turnstile widget is valid and not forged. +In this case, we'll add an API that performs additional validation and server-side processing based on the result of `turnstile.render`. + +First, for easier testing, let's remove the `disabled` attribute from the `button` tag: + +```diff +- ++ +``` + +Next, let's add an API for server-side verification. Please add the following code to `src/index.tsx`. +This API validates the Turnstile token generated by the client application and incorporates the result into Stripe's Payment Intent. + +```ts +import { HTTPException } from "hono/http-exception"; + +type TurnstileResult = { + success: boolean; + challenge_ts: string; + hostname: string; + "error-codes": Array; + action: string; + cdata: string; +}; + +app.post("/pre-confirm", async (c) => { + const { TURNSTILE_SECRET_KEY, STRIPE_SECRET_KEY } = env(c); + const stripe = new Stripe(STRIPE_SECRET_KEY, { + apiVersion: "2024-10-28.acacia", + appInfo: { + name: "example/cloudflare-turnstile", + }, + }); + + const body = await c.req.json(); + const ip = c.req.header("CF-Connecting-IP"); + const paymentIntentId = body.payment_intent_id + + const formData = new FormData(); + formData.append("secret", TURNSTILE_SECRET_KEY); + formData.append("response", body.turnstile_token); + formData.append("remoteip", ip || ""); + const turnstileResult = await fetch( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + { + body: formData, + method: "POST", + }, + ); + const outcome = await turnstileResult.json(); + + await stripe.paymentIntents.update(paymentIntentId, { + metadata: { + turnstile_result: outcome.success ? 'success' : 'failed', + turnstile_challenge_ts: outcome.challenge_ts, + }, + }) + + if (!outcome.success) { + throw new HTTPException(401, { + res: new Response( + JSON.stringify({ + success: outcome.success, + message: "Unauthorized", + error_codes: outcome["error-codes"], + }), + ), + }); + } + return c.json({ + success: outcome.success + }); +}); + +``` + +Then, add the process to call the created API. +By executing this before calling Stripe's JavaScript SDK in the form's submit event, we can decide whether to proceed with the payment based on the server-side validation result: + +```diff +paymentForm.addEventListener("submit", async (e) => { + e.preventDefault(); + if (!turnstileToken) { + return; + } + if (submitButon) { + submitButon.setAttribute("disabled", true); + } + resultElement.innerHTML = ""; + ++ const preConfirmationResponse = await fetch("/pre-confirm", { ++ method: "POST", ++ headers: { ++ "Content-Type": "application/json", ++ }, ++ body: JSON.stringify({ ++ turnstile_token: turnstileToken, ++ payment_intent_id: "${paymentIntent.id}", ++ }), ++ }); ++ const preConfirmationResult = await preConfirmationResponse.json(); ++ if (!preConfirmationResult.success) { ++ submitButon.removeAttribute("disabled"); ++ resultElement.innerHTML = JSON.stringify( ++ preConfirmationResult, ++ null, ++ 2, ++ ); ++ return; ++ } + + const { error: submitError } = await elements.submit(); + if (submitError) { + console.log(submitError); + submitButon.removeAttribute("disabled"); + return; + } + const { error: confirmationError } = await stripe.confirmPayment({ + elements, + confirmParams: { + return_url: "http://localhost:8787", + }, + }); +``` + +By adding this step, we now perform a two-stage check using Turnstile before the payment process. +Since we're saving the Turnstile authentication result in the Stripe data, it's also easier to investigate if a user reports a payment failure. + +If you want more strict control, you could add a process to invalidate the Stripe Payment Intent if authentication fails in the `POST /pre-confirm` API. + +## Conclusion + +In online payments, it's necessary to protect applications from bot attacks such as card testing and DDoS. +While payment services like Stripe are increasingly implementing bot prevention measures, adding Turnstile can provide an extra layer of security for your payment forms. \ No newline at end of file From 15124eec4ef66418e8ba66070005dc8764be6c16 Mon Sep 17 00:00:00 2001 From: Hidetaka Okamoto Date: Mon, 18 Nov 2024 22:19:43 +0900 Subject: [PATCH 2/4] update the Turnstile tutorial --- ...rm-from-attackers-bots-using-turnstile.mdx | 210 +++++++++--------- 1 file changed, 104 insertions(+), 106 deletions(-) diff --git a/src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx b/src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx index 8453ca41c7c6a92..2c4c9f71f9f69e3 100644 --- a/src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx +++ b/src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx @@ -1,5 +1,5 @@ --- -title: Protecting your payment form from attackers' bots using Turnstile +title: Protect payment forms from malicious bots using Turnstile pcx_content_type: tutorial updated: 2024-11-11 difficulty: Beginner @@ -16,34 +16,33 @@ spotlight: author_bio_source: LinkedIn sidebar: order: 2 - --- import { Render, TabItem, Tabs } from "~/components"; -This tutorial shows how you can build a more secure payment form using Turnstile. You can learn how to block bot access on the checkout page and trigger additional authentication flows by integrating Turnstile with Stripe - +This tutorial shows how you can build a more secure payment form using Turnstile. You can learn how to block bot access on the checkout page and trigger additional authentication flows by integrating Turnstile with Stripe. ## Before you begin -- You must have a Cloudflare account -- You must have a Stripe account + +3. Sign up for a [Stripe](https://stripe.com) account. +## 1. Get Your Turnstile sitekey and secret key -## Get Your Turnstile sitekey and secret key +First, you will need to prepare a Cloudflare Turnstile widget to use for this application. 1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com/) and select your account. 2. Go to **Turnstile** and [create a new Turnstile widget](/turnstile/get-started/). 3. Copy the sitekey and the secret key to use in the next step. -## 1. Create a new Worker project +## 2. Create a new Worker project - -First, let's create a Cloudflare Workers project. +Now that your Turnstile widget it ready to use, you can create your Worker application. To efficiently create and manage multiple APIs, let's use [`Hono`](https://hono.dev). Hono is an open-source application framework released by a Cloudflare Developer Advocate. It is lightweight and allows for the creation of multiple API paths, as well as efficient request and response handling. + Open your command line interface (CLI) and run the following command: @@ -82,7 +81,6 @@ create-hono@0.14.2 Ok to proceed? (y) y ``` - During the setup, you will be asked if you want to manage your project source code with `Git`. It is recommended to answer `Yes` as it helps in recording your work and rolling back changes. You can also choose `No`, which will not affect the tutorial progress. ```sh @@ -150,21 +148,19 @@ curl http://localhost:8787 Hello Hono! ``` -So far, we've covered how to create a Cloudflare Worker project and introduced tools and open-source projects like the `C3` command and the `Hono` framework that streamline development with Cloudflare. Leveraging these features will help you develop applications on Cloudflare Workers more smoothly. - -## 2. Preparation for the building web application +So far, we've covered how to create a Worker project using `C3` and introduced the open source `Hono` framework that streamlines web-application development with Workers. -By the default, we need to update the Hono project for supporting to web application. +At the next step, we need to update the Hono project for supporting to web application. -### Change the file extension to support JSX +## 3. Change index script file extension to .JSX -Since we'll use JSX to dynamically create HTML, let's change `src/index.ts` to `src/index.tsx`. +Since we will use JSX to dynamically create HTML, you will need to change `src/index.ts` to `src/index.tsx`. ``` mv src/index.ts src/index.tsx ``` -At the same time, let's change the filename specified in the `wrangler.toml`. +At the same time, change the filename specified in the `wrangler.toml`. ```diff #:schema node_modules/wrangler/config-schema.json @@ -174,19 +170,15 @@ name = "secure-payment-form" compatibility_date = "2024-10-22" ``` -### Register Stripe API key and Turnstile key as environment variables - -Let's register API keys as environment variables to use both Stripe and Cloudflare Turnstile. +## 4. Add the Stripe API key and Turnstile key as environment variables -You can obtain test site keys and secret keys for Cloudflare Turnstile from the documentation. +To connect your web application to both Stripe and Turnstile, you must register the necessary API keys for Stripe and Turnstile as environment variables within your application. -https://developers.cloudflare.com/turnstile/reference/testing/ +You can obtain test site keys and secret keys for Turnstile from the [Turnstile documentation](/turnstile/reference/testing/). -Get the publishable key and secret key for Stripe from the Stripe dashboard. +Get the publishable key and secret key for Stripe from the [Stripe dashboard].(https://dashboard.stripe.com/test/apikeys) -https://dashboard.stripe.com/test/apikeys - -And place each keys into the `.dev.vars` file like the following: +Then, place each key into the `.dev.vars` file like the following: ``` TURNSTILE_SITE_KEY = '1x00000000000000000000AA' @@ -196,6 +188,7 @@ STRIPE_SECRET_KEY='Secret key starting with sk_test_' ``` After that, you can generate TypeScript type definition by running the `npm run cf-typegen` command. + ```sh $ npm run cf-typegen @@ -212,17 +205,17 @@ interface CloudflareBindings { In local development using Hono and Wrangler, you can retrieve values set in `.dev.vars` like this: ```ts -app.get('/hello', async c => { - console.log(c.env.TURNSTILE_SITE_KEY); - return c.json({ message: 'test' }); -}) +app.get("/hello", async (c) => { + console.log(c.env.TURNSTILE_SITE_KEY); + return c.json({ message: "test" }); +}); ``` -Now we're ready for application development. In the next steps, we'll develop a payment form using Turnstile and Stripe. +Now we are ready for application development. In the next steps, we will develop a payment form using Turnstile and Stripe. -## 3. Implementing Bot Detection with Turnstile +## 5. Implement Bot Detection with Turnstile -Let's start by creating a form that uses Turnstile to detect bot access. Add the following code to `src/index.tsx` to create a simple form: +Start by creating a form that uses Turnstile to detect bot access. Add the following code to `src/index.tsx` to create a simple form: ```ts import { Hono } from "hono"; @@ -247,8 +240,8 @@ export default app; ``` -Let's add JavaScript code to our application to implement bot detection using Turnstile. -By adding this implementation, the order form submission process will be disabled until the Turnstile bot detection process is completed and it is confirmed that the access is not from a bot. +Add JavaScript code to our application to implement bot detection using Turnstile. +By adding this implementation, the order form submission process will be disabled until the Turnstile bot detection process is completed and it is confirmed that the access request is not from a bot. ```diff import { Hono } from "hono"; @@ -297,15 +290,15 @@ export default app; ``` -Here, we're loading the Turnstile script file with a `script` tag. The `_turnstileCB` function is executed when the script file loading is complete, triggered by the `onload=_turnstileCB` in the query string. +This will load the Turnstile script file with a `script` tag. The `_turnstileCB` function is executed when the script file loading is complete, triggered by the `onload=_turnstileCB` in the query string. In the `_turnstileCB` function, `turnstile.render()` is executed. The `callback` set here removes the `disabled` attribute from the submit `button` of the `form`. -This simple implementation prevents order operations for accesses that Cloudflare identifies as bots. +This implementation blocks order operations for any requests that Cloudflare identifies as being made by a bots. -## 4. Integrating Turnstile with a Stripe Payment Form +## 6. Integrate Turnstile with a Stripe payment form -For a more practical example, let's integrate Turnstile with a Stripe payment form. First, install the Stripe SDK: +To integrate Turnstile with a Stripe payment form, first you will need to install the Stripe SDK: @@ -327,8 +320,7 @@ pnpm add stripe -Next, let's implement the code to create a payment form in `src/index.tsx`. -First, create a [Payment Intent](https://docs.stripe.com/api/payment_intents) on the server side: +Next, implement the code to create a payment form in `src/index.tsx`. The following code creates a [Payment Intent](https://docs.stripe.com/api/payment_intents) on the server side: ```diff import { Hono } from "hono"; @@ -358,7 +350,7 @@ app.get("/", async (c) => { ``` -And adding JavaScript code to display the payment form. Edit `src/index.tsx`: +Then, add the following code to display the payment form. Edit `src/index.tsx`: ```diff {html` @@ -431,98 +423,102 @@ The payment form is now ready. To experience how it behaves when a bot is detect +TURNSTILE_SITE_KEY = '2x00000000000000000000AB' ``` -If you restart the application now, you'll notice that you can't submit the payment form. +If you restart the application now, you will notice that you cannot submit the payment form. ![Failed challenge](~/assets/images/turnstile/payment-form.png) -This way, you can block requests trying to manipulate the payment form using bots, such as card testing attacks. +This way, you can block requests that use bots to try and manipulate the payment form, such as card testing attacks. By verifying whether the `turnstileToken` is set by the `callback` of `turnstile.render()`, you can use Turnstile's result when processing the `form`'s `submit` event. -Note: After completing the tests, let's make sure to revert the Turnstile SITE_KEY back to its original value. +:::note +After completing the tests, make sure to revert the Turnstile `SITE_KEY` back to its original value. + ```diff +TURNSTILE_SITE_KEY = '1x00000000000000000000AA' -TURNSTILE_SITE_KEY = '2x00000000000000000000AB' ``` -## 5. Adding Server-Side Validation +::: + +## 7. Add Server-Side Validation -Let's add a step to verify that the token generated by the Turnstile widget is valid and not forged. +Next, add a step to verify that the token generated by the Turnstile widget is valid and not forged. In this case, we'll add an API that performs additional validation and server-side processing based on the result of `turnstile.render`. -First, for easier testing, let's remove the `disabled` attribute from the `button` tag: +For easier testing, remove the `disabled` attribute from the `button` tag: ```diff - + ``` -Next, let's add an API for server-side verification. Please add the following code to `src/index.tsx`. +Next, add an API for server-side verification. Please add the following code to `src/index.tsx`. This API validates the Turnstile token generated by the client application and incorporates the result into Stripe's Payment Intent. ```ts import { HTTPException } from "hono/http-exception"; type TurnstileResult = { - success: boolean; - challenge_ts: string; - hostname: string; - "error-codes": Array; - action: string; - cdata: string; + success: boolean; + challenge_ts: string; + hostname: string; + "error-codes": Array; + action: string; + cdata: string; }; app.post("/pre-confirm", async (c) => { - const { TURNSTILE_SECRET_KEY, STRIPE_SECRET_KEY } = env(c); - const stripe = new Stripe(STRIPE_SECRET_KEY, { - apiVersion: "2024-10-28.acacia", - appInfo: { - name: "example/cloudflare-turnstile", - }, - }); - - const body = await c.req.json(); - const ip = c.req.header("CF-Connecting-IP"); - const paymentIntentId = body.payment_intent_id - - const formData = new FormData(); - formData.append("secret", TURNSTILE_SECRET_KEY); - formData.append("response", body.turnstile_token); - formData.append("remoteip", ip || ""); - const turnstileResult = await fetch( - "https://challenges.cloudflare.com/turnstile/v0/siteverify", - { - body: formData, - method: "POST", - }, - ); - const outcome = await turnstileResult.json(); - - await stripe.paymentIntents.update(paymentIntentId, { - metadata: { - turnstile_result: outcome.success ? 'success' : 'failed', - turnstile_challenge_ts: outcome.challenge_ts, - }, - }) - - if (!outcome.success) { - throw new HTTPException(401, { - res: new Response( - JSON.stringify({ - success: outcome.success, - message: "Unauthorized", - error_codes: outcome["error-codes"], - }), - ), - }); - } - return c.json({ - success: outcome.success - }); + const { TURNSTILE_SECRET_KEY, STRIPE_SECRET_KEY } = env(c); + const stripe = new Stripe(STRIPE_SECRET_KEY, { + apiVersion: "2024-10-28.acacia", + appInfo: { + name: "example/cloudflare-turnstile", + }, + }); + + const body = await c.req.json(); + const ip = c.req.header("CF-Connecting-IP"); + const paymentIntentId = body.payment_intent_id; + + const formData = new FormData(); + formData.append("secret", TURNSTILE_SECRET_KEY); + formData.append("response", body.turnstile_token); + formData.append("remoteip", ip || ""); + const turnstileResult = await fetch( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + { + body: formData, + method: "POST", + }, + ); + const outcome = await turnstileResult.json(); + + await stripe.paymentIntents.update(paymentIntentId, { + metadata: { + turnstile_result: outcome.success ? "success" : "failed", + turnstile_challenge_ts: outcome.challenge_ts, + }, + }); + + if (!outcome.success) { + throw new HTTPException(401, { + res: new Response( + JSON.stringify({ + success: outcome.success, + message: "Unauthorized", + error_codes: outcome["error-codes"], + }), + ), + }); + } + return c.json({ + success: outcome.success, + }); }); - ``` Then, add the process to call the created API. + By executing this before calling Stripe's JavaScript SDK in the form's submit event, we can decide whether to proceed with the payment based on the server-side validation result: ```diff @@ -572,11 +568,13 @@ paymentForm.addEventListener("submit", async (e) => { ``` By adding this step, we now perform a two-stage check using Turnstile before the payment process. -Since we're saving the Turnstile authentication result in the Stripe data, it's also easier to investigate if a user reports a payment failure. + +Since we're saving the Turnstile authentication result in the Stripe data, it is also easier to investigate if a user reports a payment failure. If you want more strict control, you could add a process to invalidate the Stripe Payment Intent if authentication fails in the `POST /pre-confirm` API. ## Conclusion -In online payments, it's necessary to protect applications from bot attacks such as card testing and DDoS. -While payment services like Stripe are increasingly implementing bot prevention measures, adding Turnstile can provide an extra layer of security for your payment forms. \ No newline at end of file +In online payments, it ss necessary to protect applications from bot attacks such as card testing and DDoS attacks. + +While payment services like Stripe are increasingly implementing bot prevention measures, adding Turnstile can provide an extra layer of security for your payment forms. From 1ef2386b7900df78aca42f082b8105df8ddf557d Mon Sep 17 00:00:00 2001 From: Hidetaka Okamoto Date: Fri, 13 Dec 2024 09:20:49 +0900 Subject: [PATCH 3/4] fix: update turnstile tutorial based on the feedback --- ...nt-form-from-attackers-bots-using-turnstile.mdx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx b/src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx index 2c4c9f71f9f69e3..46f238120573042 100644 --- a/src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx +++ b/src/content/docs/turnstile/tutorials/protecting-your-payment-form-from-attackers-bots-using-turnstile.mdx @@ -41,7 +41,7 @@ Now that your Turnstile widget it ready to use, you can create your Worker appli -To efficiently create and manage multiple APIs, let's use [`Hono`](https://hono.dev). Hono is an open-source application framework released by a Cloudflare Developer Advocate. It is lightweight and allows for the creation of multiple API paths, as well as efficient request and response handling. +To efficiently create and manage multiple APIs, let's use [`Hono`](https://hono.dev). It is lightweight and allows for the creation of multiple API paths, as well as efficient request and response handling. Open your command line interface (CLI) and run the following command: @@ -176,7 +176,7 @@ To connect your web application to both Stripe and Turnstile, you must register You can obtain test site keys and secret keys for Turnstile from the [Turnstile documentation](/turnstile/reference/testing/). -Get the publishable key and secret key for Stripe from the [Stripe dashboard].(https://dashboard.stripe.com/test/apikeys) +Get the publishable key and secret key for Stripe from the [Stripe dashboard](https://dashboard.stripe.com/test/apikeys). Then, place each key into the `.dev.vars` file like the following: @@ -243,7 +243,7 @@ export default app; Add JavaScript code to our application to implement bot detection using Turnstile. By adding this implementation, the order form submission process will be disabled until the Turnstile bot detection process is completed and it is confirmed that the access request is not from a bot. -```diff +```diff lang="ts" import { Hono } from "hono"; +import { env } from "hono/adapter"; +import { html } from "hono/html"; @@ -322,7 +322,7 @@ pnpm add stripe Next, implement the code to create a payment form in `src/index.tsx`. The following code creates a [Payment Intent](https://docs.stripe.com/api/payment_intents) on the server side: -```diff +```diff lang="ts" import { Hono } from "hono"; import { env } from "hono/adapter"; import { html } from "hono/html"; @@ -352,7 +352,7 @@ app.get("/", async (c) => { Then, add the following code to display the payment form. Edit `src/index.tsx`: -```diff +```diff lang="ts" {html` +