From b0e11a78ee963ed3328e1ed3d72bea10a1119344 Mon Sep 17 00:00:00 2001 From: Ethan Konkolowicz Date: Thu, 12 Mar 2026 13:46:07 -0400 Subject: [PATCH] added docs for RWK + core for captcha protection --- category/advanced.mdx | 3 + docs.json | 5 +- sdks/advanced/captchas.mdx | 54 ++++++ sdks/react/captcha.mdx | 241 +++++++++++++++++++++++++ sdks/typescript-frontend/captcha.mdx | 256 +++++++++++++++++++++++++++ 5 files changed, 558 insertions(+), 1 deletion(-) create mode 100644 sdks/advanced/captchas.mdx create mode 100644 sdks/react/captcha.mdx create mode 100644 sdks/typescript-frontend/captcha.mdx diff --git a/category/advanced.mdx b/category/advanced.mdx index 45fba0b5..f2eb93bb 100644 --- a/category/advanced.mdx +++ b/category/advanced.mdx @@ -24,4 +24,7 @@ sidebarTitle: "Overview" Guide on using the IndexedDbStamper + + Guide on using Captcha Protection + diff --git a/docs.json b/docs.json index 0b265f0c..81e1045c 100644 --- a/docs.json +++ b/docs.json @@ -338,6 +338,7 @@ "sdks/react/sub-organization-customization", "sdks/react/advanced-api-requests", "sdks/react/advanced-backend-authentication", + "sdks/react/captcha", "sdks/react/migrating-sdk-react", "sdks/react/troubleshooting", "sdks/react/legacy", @@ -592,6 +593,7 @@ "sdks/typescript-frontend/auth", "sdks/typescript-frontend/advanced-backend-authentication", "sdks/typescript-frontend/advanced-api-requests", + "sdks/typescript-frontend/captcha", "sdks/typescript-frontend/legacy", { "group": "SDK reference", @@ -691,7 +693,8 @@ "sdks/advanced/api-key-stamper", "sdks/advanced/wallet-stamper", "sdks/advanced/webauthn-stamper", - "sdks/advanced/iframe-stamper" + "sdks/advanced/iframe-stamper", + "sdks/advanced/captchas" ] }, "sdks/migration-path" diff --git a/sdks/advanced/captchas.mdx b/sdks/advanced/captchas.mdx new file mode 100644 index 00000000..57332fe2 --- /dev/null +++ b/sdks/advanced/captchas.mdx @@ -0,0 +1,54 @@ +--- +title: "Captcha Protection" +description: "Overview of captcha support across Turnkey SDKs, including supported versions and setup guides." +sidebarTitle: "Captcha Protection" +--- + +## Overview + +Turnkey supports [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) captcha protection to help prevent automated abuse of authentication endpoints. When enabled, captcha tokens are required for Signup and InitOtp requests made through the Auth Proxy. + +Captcha is enabled per-organization in the [Turnkey Dashboard](https://app.turnkey.com) under the **Wallet Kit** settings page. + + + Once captcha is enabled in the dashboard, **all** Signup and InitOtp requests will require valid captcha tokens. Make sure your application is updated to a supported SDK version and has captcha integrated **before** enabling this setting. + + +## Supported SDKs + + {/* TODO: UPDATE THE VERSION */} + + + `@turnkey/react-wallet-kit` — **v.x.x.x+** + + + `@turnkey/core` — **v.x.x.x+** + + + +## How It Works + +1. **Enable captcha** in the Turnkey Dashboard under Wallet Kit settings. +2. The SDK automatically receives a `turnstileSiteKey` from the Auth Proxy via the `getWalletKitClientParams` request. +3. Your application renders a Cloudflare Turnstile widget that generates captcha tokens. +4. Captcha tokens are attached to Signup and InitOtp requests as an `X-Captcha-Token` header. + +For detailed integration guides, see the SDK-specific pages linked above. + +## Recommended Rollout + + + + Upgrade to a captcha-supported version of the SDK (see table above). + + + Follow the setup guide for your SDK. The captcha widget will idle silently in the background until captcha is enabled — there is no impact to your users. + + + Once your app is deployed with captcha support, toggle the **Captcha** option in the Turnkey Dashboard. From this point on, all Signup and InitOtp requests will require valid tokens. + + + + + You can verify that your app is ready by checking for the presence of `turnstileSiteKey` in the SDK config. If the key is present, captcha is enabled and your app should be providing tokens with each request. + diff --git a/sdks/react/captcha.mdx b/sdks/react/captcha.mdx new file mode 100644 index 00000000..72b5d819 --- /dev/null +++ b/sdks/react/captcha.mdx @@ -0,0 +1,241 @@ +--- +title: "Captcha Protection" +description: "Learn how to enable Cloudflare Turnstile captcha protection for your Turnkey Embedded Wallet Kit integration." +sidebarTitle: "Captcha Protection" +--- + + + {/* TODO: UPDATE THE VERSION */} + Captcha support requires `@turnkey/react-wallet-kit` version **x.x.x** or later. Make sure to update your package before proceeding. + + +## Overview + +The Embedded Wallet Kit supports [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) captcha protection to help prevent abuse and ensure that authentication requests are performed by real users. When enabled, fresh captcha tokens are required for **every Signup request and every InitOtp request** (including OTP resends). + +There are two ways to use captcha with the Embedded Wallet Kit: + +1. **Using the built-in Auth component** — captcha is handled automatically, no additional code required. +2. **Building a custom auth UI** — you'll need to integrate Turnstile components yourself in the right places. + +## Enabling Captcha in the Dashboard + + + As soon as captcha is enabled in the dashboard, **all** Signup and InitOtp requests will require captcha tokens. If your frontend is not set up to provide tokens, authentication will fail. It is **strongly recommended** to deploy your app with captcha components integrated first (they will idle silently in the background), and **then** enable the captcha toggle in the dashboard. This ensures a seamless transition with no downtime. + + + + + Go to the [Turnkey Dashboard](https://app.turnkey.com), navigate to the **Wallet Kit** page. + + {/* */} + + + + Toggle the **Captcha** option to enable it. + + {/* */} + + + + Click **Save**. From this point on, all Signup and InitOtp requests will require valid captcha tokens. + + {/* */} + + + +### Detecting captcha status on the frontend + +Once captcha is enabled in the dashboard, the `turnstileSiteKey` field will be present in the config returned by the Embedded Wallet Kit (populated from the `getWalletKitClientParams` request). You can use this to determine whether captcha is active: + +```tsx +const { config } = useTurnkey(); + +const isCaptchaEnabled = !!config?.turnstileSiteKey; +``` + +## Option 1: Using the Built-in Auth Component + +If you're using the Embedded Wallet Kit's built-in auth component (via `handleLogin`), captcha is handled entirely for you. The auth component automatically: + +- Pre-warms a captcha token in the background when the user is not authenticated +- Shows a visible Turnstile challenge only if Cloudflare requires user interaction +- Attaches captcha tokens to all Signup and InitOtp requests +- Manages token refresh for the OTP verification step + +No code changes are required — just enable captcha in the dashboard and update to the latest package version. + +## Option 2: Custom Auth UI + +If you're building your own auth modal or layout, you'll need to integrate Turnstile components in **two places**: + +1. **The initial auth screen** — where users enter their email/phone or choose a sign-up method +2. **The OTP verification screen** — where users enter the OTP code (and can resend) + +This is because a **fresh captcha token is consumed on each request**. A token used for `initOtp` cannot be reused for a subsequent `signup` or another `initOtp` call. + +### Install the Turnstile library + +We recommend using [`@marsidev/react-turnstile`](https://github.com/marsidev/react-turnstile), a lightweight React wrapper around the Cloudflare Turnstile widget: + + + +```bash npm +npm install @marsidev/react-turnstile +``` + +```bash pnpm +pnpm add @marsidev/react-turnstile +``` + +```bash yarn +yarn add @marsidev/react-turnstile +``` + + + +### Setting up the Turnstile component + +Import the component and types: + +```tsx +import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile"; +``` + +#### Appearance modes + +Cloudflare Turnstile supports several appearance modes. For the best user experience, we recommend: + +- **`"interaction-only"`** — The widget is completely hidden unless Cloudflare determines it needs user interaction. This is ideal for most cases as users won't see anything unless a challenge is required. +- **`"always"`** — The widget is always visible. Use this if you want users to always see the captcha. + +The Turnkey dashboard configures Turnstile in **Managed** mode, meaning Cloudflare decides whether an interactive challenge is needed. Using `appearance: "interaction-only"` pairs well with this — the widget stays invisible for most users, and only appears when Cloudflare needs interaction. + +### Auth screen integration + +Add a Turnstile component to your auth screen. This component will generate a token that gets consumed when the user initiates a Signup or InitOtp request. + +```tsx +import { useRef, useState } from "react"; +import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile"; +import { useTurnkey } from "@turnkey/react-wallet-kit"; + +function AuthScreen() { + const { config } = useTurnkey(); + const turnstileRef = useRef(null); + const [captchaToken, setCaptchaToken] = useState(null); + const [showPrompt, setShowPrompt] = useState(false); + + return ( +
+ {/* Your auth form inputs here */} + + {config?.turnstileSiteKey && ( + setCaptchaToken(token)} + onError={() => setCaptchaToken(null)} + onExpire={() => setCaptchaToken(null)} + onBeforeInteractive={() => setShowPrompt(true)} + options={{ + appearance: "interaction-only", + size: "flexible", + }} + /> + )} + + {showPrompt && ( +

Please complete the captcha to continue.

+ )} +
+ ); +} +``` + +### OTP screen integration + +A **second** Turnstile component is needed on the OTP verification screen. This generates fresh tokens for completing the OTP or resending it — both of which are captcha-protected requests. + +```tsx +import { useRef, useState } from "react"; +import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile"; +import { useTurnkey } from "@turnkey/react-wallet-kit"; + +function OtpScreen() { + const { config } = useTurnkey(); + const turnstileRef = useRef(null); + const [captchaToken, setCaptchaToken] = useState(null); + const [showPrompt, setShowPrompt] = useState(false); + + return ( +
+ {/* Your OTP input here */} + + {config?.turnstileSiteKey && ( + setCaptchaToken(token)} + onError={() => setCaptchaToken(null)} + onExpire={() => setCaptchaToken(null)} + onBeforeInteractive={() => setShowPrompt(true)} + options={{ + appearance: "interaction-only", + size: "flexible", + }} + /> + )} + + {showPrompt && ( +

Please complete the captcha to continue.

+ )} +
+ ); +} +``` + +### Consuming tokens + +Each captcha token can only be used **once**. After consuming a token (e.g., when calling `initOtp`), you must reset the widget to generate a new one for the next request: + +```tsx +// Consume the token and reset the widget for the next request +const consumeToken = () => { + const token = captchaToken; + setCaptchaToken(null); + turnstileRef.current?.reset(); + return token; +}; + +// Example: passing the token to initOtp +const handleSendOtp = async () => { + const token = consumeToken(); + + await initOtp({ + otpType: "email", + contact: email, + ...(token ? { captchaToken: token } : {}), + }); +}; + +// Example: passing the token to a signup request +const handleSignup = async () => { + const token = consumeToken(); + + await signUpWithPasskey({ + ...(token ? { captchaToken: token } : {}), + }); +}; +``` + + + The `captchaToken` parameter is accepted by `initOtp`, `completeOtp`, `completeOauth`, `loginOrSignupWithWallet`, and OAuth handle methods. When captcha is not enabled, omitting the token has no effect. + + +## Best Practices + +- **Deploy first, enable second**: Integrate captcha components into your app before enabling captcha in the dashboard. The Turnstile widget will silently idle when captcha is not enabled (no `turnstileSiteKey` in config). +- **Always reset after consuming**: Call `turnstileRef.current?.reset()` after using a token so the widget can generate a fresh one for the next request. +- **Handle expiration**: Turnstile tokens expire after a short period. Listen to the `onExpire` callback and clear your stored token so that users aren't submitting stale tokens. +- **Disable submit buttons while waiting**: Consider disabling auth buttons until a valid captcha token is available to prevent failed requests. diff --git a/sdks/typescript-frontend/captcha.mdx b/sdks/typescript-frontend/captcha.mdx new file mode 100644 index 00000000..df7dfa03 --- /dev/null +++ b/sdks/typescript-frontend/captcha.mdx @@ -0,0 +1,256 @@ +--- +title: "Captcha Protection" +description: "Learn how to integrate Cloudflare Turnstile captcha protection with @turnkey/core." +sidebarTitle: "Captcha Protection" +--- + + + {/* TODO: UPDATE THE VERSION */} + Captcha support requires `@turnkey/core` version **x.x.x** or later. Make sure to update your package before proceeding. + + +## Overview + +The `@turnkey/core` package supports [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) captcha protection to prevent automated abuse of authentication endpoints. When captcha is enabled in the Turnkey Dashboard, fresh captcha tokens are required for **every Signup request and every InitOtp request**. + +Unlike `@turnkey/react-wallet-kit`, the core package does not include a built-in auth component or provider that handles captcha automatically. You are responsible for: + +1. Fetching your Turnstile site key from the Auth Proxy +2. Rendering the Turnstile widget +3. Passing captcha tokens to the appropriate SDK methods + +## Enabling Captcha in the Dashboard + + + As soon as captcha is enabled in the dashboard, **all** Signup and InitOtp requests will require captcha tokens. If your application is not set up to provide tokens, authentication will fail. It is **strongly recommended** to deploy your app with captcha integrated first (it will idle silently in the background), and **then** enable captcha in the dashboard for a seamless transition. + + + + + Go to the [Turnkey Dashboard](https://app.turnkey.com), navigate to the **Wallet Kit** page. + + + + Toggle the **Captcha** option to enable it. + + + + Click **Save**. From this point on, all Signup and InitOtp requests will require valid captcha tokens. + + + +## Fetching the Turnstile Site Key + +Unlike the React Wallet Kit (which fetches this automatically), you need to manually retrieve the Turnstile site key from the Auth Proxy using `getClientParams`: + +```ts +import { getClientParams } from "@turnkey/core"; + +const clientParams = await getClientParams( + "YOUR_AUTH_PROXY_CONFIG_ID", + // Optional: custom auth proxy URL (defaults to https://authproxy.turnkey.com) +); + +const turnstileSiteKey = clientParams.turnstileSiteKey; +``` + +If `turnstileSiteKey` is present in the response, captcha is enabled for your organization. If it is `undefined`, captcha is not enabled and no tokens are required. + + + You should call `getClientParams` during your app's initialization — alongside any other setup like `client.init()`. Cache the result so you don't need to fetch it on every auth attempt. + + +## Installing the Turnstile Library + +For web applications, we recommend using the [Cloudflare Turnstile script](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/) directly, or a framework wrapper if you're using React or another UI library. + +### Vanilla JavaScript / script tag + +Add the Turnstile script to your HTML: + +```html + +``` + +### React (or other component frameworks) + +If you're using React with `@turnkey/core` directly, we recommend [`@marsidev/react-turnstile`](https://github.com/marsidev/react-turnstile): + + + +```bash npm +npm install @marsidev/react-turnstile +``` + +```bash pnpm +pnpm add @marsidev/react-turnstile +``` + +```bash yarn +yarn add @marsidev/react-turnstile +``` + + + +## Integration + +### Turnstile Appearance + +The Turnkey dashboard configures Turnstile in **Managed** mode, meaning Cloudflare decides whether a user needs to complete an interactive challenge. To keep the widget hidden unless interaction is required, use `appearance: "interaction-only"`. This way most users will never see the captcha — it only appears when Cloudflare needs verification. + +### Vanilla JavaScript + +Render the Turnstile widget and capture the token: + +```html +
+ + +``` + +### React + +```tsx +import { useRef, useState, useEffect } from "react"; +import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile"; +import { getClientParams } from "@turnkey/core"; + +function CaptchaWidget({ authProxyConfigId }: { authProxyConfigId: string }) { + const turnstileRef = useRef(null); + const [siteKey, setSiteKey] = useState(null); + const [captchaToken, setCaptchaToken] = useState(null); + const [showPrompt, setShowPrompt] = useState(false); + + useEffect(() => { + getClientParams(authProxyConfigId).then((params) => { + if (params.turnstileSiteKey) { + setSiteKey(params.turnstileSiteKey); + } + }); + }, [authProxyConfigId]); + + if (!siteKey) return null; + + return ( + setCaptchaToken(token)} + onError={() => setCaptchaToken(null)} + onExpire={() => setCaptchaToken(null)} + onBeforeInteractive={() => setShowPrompt(true)} + options={{ + appearance: "interaction-only", + size: "flexible", + }} + /> + ); +} +``` + +## Passing Captcha Tokens to SDK Methods + +The following methods accept an optional `captchaToken` parameter: + +- `initOtp` +- `completeOtp` +- `signUpWithPasskey` +- `signUpWithOtp` +- `completeOauth` +- `loginOrSignupWithWallet` + +Each token can only be used **once**. After consuming a token, reset the Turnstile widget to generate a new one for the next request. + +### Token consumption pattern + +```ts +// Consume the token and reset the widget +function consumeToken() { + const token = captchaToken; + captchaToken = null; + // Reset the widget to generate a fresh token + // Vanilla JS: + turnstile.reset("#turnstile-container"); + // React: turnstileRef.current?.reset(); + return token; +} +``` + +### Example: Email OTP flow + +A complete OTP flow requires captcha tokens in **two places** — the initial `initOtp` call and the `completeOtp` call. This means you need a fresh token for each step. + +```ts +import { TurnkeyClient, OtpType } from "@turnkey/core"; + +const client = new TurnkeyClient({ + organizationId: "YOUR_ORG_ID", + authProxyConfigId: "YOUR_AUTH_PROXY_CONFIG_ID", +}); +await client.init(); + +// Step 1: Initiate OTP — consume a captcha token +const otpId = await client.initOtp({ + otpType: OtpType.Email, + contact: "user@example.com", + captchaToken: consumeToken(), // First token consumed here +}); + +// ... user receives OTP and enters it ... +// A NEW captcha token must be generated by this point + +// Step 2: Complete OTP — consume another captcha token +const session = await client.completeOtp({ + otpId, + otpCode: "123456", + contact: "user@example.com", + otpType: OtpType.Email, + captchaToken: consumeToken(), // Second token consumed here +}); +``` + +### Example: Passkey signup + +```ts +const session = await client.signUpWithPasskey({ + captchaToken: consumeToken(), +}); +``` + +### Example: Wallet login/signup + +```ts +const session = await client.loginOrSignupWithWallet({ + walletProvider: "metamask", + captchaToken: consumeToken(), +}); +``` + + + When captcha is not enabled (no `turnstileSiteKey` from `getClientParams`), you can safely omit the `captchaToken` parameter — the requests will work as usual. + + +## Important Considerations + +- **Two tokens per OTP flow**: `initOtp` and `completeOtp` each require their own captcha token. Make sure your UI resets the widget after the first token is consumed so a fresh token is ready for the second step. +- **Deploy first, enable second**: Integrate captcha into your app before enabling it in the dashboard. The Turnstile widget will idle silently when no `turnstileSiteKey` is returned from `getClientParams`. +- **Handle expiration**: Turnstile tokens expire after a short period. Listen for expiration events and clear your stored token to avoid submitting stale tokens. +- **Reset after every use**: Always reset the Turnstile widget after consuming a token so it can generate a new one.