Skip to main content

Handle sign-up challenges in your custom flow

Warning

This guide is for users who want to build a . To use a prebuilt UI, use the Account Portal pages or prebuilt components.

Challenge suspicious sign-ups makes sign-ups that look automated, or that come through VPNs, Tor, or proxies, pass a Cloudflare Turnstile check. Most people pass it without doing anything, and some are asked to complete a CAPTCHA. In a custom sign-up flow, ClerkJS runs that challenge for you, in a dialog over your page, so you don't render anything for it. Your flow does need to meet the requirements: a flow that can't show the challenge can't complete a challenged sign-up. Check them before you turn on Challenge suspicious sign-ups, or before you update from legacy bot sign-up protection.

If your application uses legacy bot sign-up protection, see Legacy bot sign-up protection.

Requirements

Your custom sign-up flow can show the challenge when all of the following are true:

  • It's built with the useSignUp() hook from @clerk/nextjs 7 or later, @clerk/react 6 or later, @clerk/react-router 3 or later, or @clerk/tanstack-react-start 1 or later. Flows built with the legacy API, such as the hooks imported from @clerk/nextjs/legacy or clerk.client.signUp, and flows built with other SDKs, aren't supported yet.
  • It loads ClerkJS 6.36.0 or later and @clerk/ui 1.38.0 or later. By default, Clerk's SDKs load the latest versions, so you don't need to do anything. If your application bundles @clerk/ui with the ui prop, or pins a ClerkJS or @clerk/ui version, update it.
  • It loads Clerk's UI. ClerkJS uses Clerk's UI to show the challenge. If you've turned off UI prefetching with prefetchUI={false} or NEXT_PUBLIC_CLERK_PREFETCH_UI=false, turn it back on.
  • Its Content Security Policy allows the challenge, the same as for Clerk's prebuilt components. See Configure Clerk Content-Security-Policy headers.
  • If it supports OAuth or enterprise SSO, its SSO callback route handles the challenge. See OAuth and enterprise SSO sign-ups.

A flow that doesn't meet these requirements can't complete a challenged sign-up. How it fails depends on which requirement it misses:

  • The flow can't show the challenge, because it misses one of the first three requirements. The signUp method returns without an error, but the sign-up stays incomplete, with protect_check in signUp.missingFields.
  • The challenge can't load, because the Content Security Policy blocks it. The dialog closes and the signUp method returns an error with the code protect_check_script_load_failed. See If the challenge isn't completed.
  • The SSO callback route can't handle the challenge. See OAuth and enterprise SSO sign-ups.

What to do depends on which protection your application uses:

  • If your application uses legacy bot sign-up protection, don't update until your flow meets these requirements. When you update, the Clerk Dashboard asks you to confirm that you've checked them, and you can't switch back afterward. To see your flow handle a challenge before your users do, update your development instance first.
  • If Challenge suspicious sign-ups is already on and your flow doesn't meet these requirements, update your flow, or turn off the protection until you do.

Important

Before you turn on Challenge suspicious sign-ups, the Clerk Dashboard checks your application's ClerkJS version and Content Security Policy. That check can't see how your custom flow is built, so confirm the other requirements yourself.

How the challenge appears in your flow

When a sign-up is challenged, the signUp method you called doesn't return right away. ClerkJS shows the challenge in a dialog over your page, and the method returns once the challenge passes. By then, the signUp object is already updated, so your flow continues as it would without the challenge. You don't need to change your code.

Most signUp methods that send a request to Clerk can show the challenge, such as signUp.password(), signUp.create(), or signUp.verifications.sendEmailCode(). A user who passes the challenge usually isn't challenged again during the same sign-up.

While the dialog is open, fetchStatus is 'fetching', so any loading state you show based on it stays visible. Users can't close the dialog.

The following example shows where the challenge can appear in an email and password sign-up. It's the same code as in the email and password custom flow, with no changes for Challenge suspicious sign-ups.

const handleSubmit = async (formData: FormData) => {
  const emailAddress = formData.get('email') as string
  const password = formData.get('password') as string

  // If this sign-up is challenged, ClerkJS shows the challenge in a dialog,
  // and `signUp.password()` returns once the challenge passes
  const { error } = await signUp.password({
    emailAddress,
    password,
  })
  if (error) {
    // See https://clerk.com/docs/guides/development/custom-flows/error-handling
    // for more info on error handling
    console.error(JSON.stringify(error, null, 2))
    return
  }

  await signUp.verifications.sendEmailCode()
}

If the challenge isn't completed

If the challenge can't load or expires before it's completed, or Clerk blocks the sign-up, the dialog closes and the method returns an error. The error is also available in errors.global. Show the user a message, as described in Error handling.

If the error's code is protect_check_script_load_failed, the challenge couldn't load. Check that your Content Security Policy allows it.

OAuth and enterprise SSO sign-ups

A sign-up through OAuth or enterprise SSO can be challenged after the user returns from the identity provider, on your SSO callback route. To show the challenge there, handle the callback with the <HandleSSOCallback /> component, which requires @clerk/react 6.17.7 or later (@clerk/nextjs 7.9.13 or later).

app/sso-callback/page.tsx
'use client'

import { HandleSSOCallback } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()

  return (
    <HandleSSOCallback
      navigateToApp={({ session, decorateUrl }) => {
        // Handle session tasks
        // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
        if (session?.currentTask) {
          console.log(session?.currentTask)
          return
        }

        // If no session tasks, navigate the signed-in user to the home page
        const url = decorateUrl('/')
        if (url.startsWith('http')) {
          window.location.href = url
        } else {
          router.push(url)
        }
      }}
      navigateToSignIn={() => router.push('/sign-in')}
      navigateToSignUp={() => router.push('/sign-up')}
    />
  )
}

If the challenge isn't completed, <HandleSSOCallback /> calls navigateToSignUp, and the error is available in errors.global from useSignUp(). Show the user a message on your sign-up page, as described in If the challenge isn't completed. If your application pins a ClerkJS version, the error is only reported with ClerkJS 6.38.1 or later.

If your callback route uses <AuthenticateWithRedirectCallback /> instead, a challenged sign-up is sent to your sign-up URL to complete the challenge. That works only if the page at your sign-up URL renders the <SignUp /> component.

The OAuth connections and enterprise connections custom flow guides build their callback route with <HandleSSOCallback />.

Remove the legacy CAPTCHA element

Challenge suspicious sign-ups doesn't use the <div id="clerk-captcha" /> element that legacy bot sign-up protection needs. After you update, you can remove it from your sign-up form.

Legacy bot sign-up protection

Legacy bot sign-up protection renders a CAPTCHA widget in your sign-up form, so your custom flow needs to provide an element for the widget.

Enable legacy bot sign-up protection

  1. In the Clerk Dashboard, navigate to the Protections page under Protect.
  2. In the Bot sign-up protection row, check the Status. The row is tagged Legacy when the update is available for your application.
    • If Disabled, select Enable. In the dialog, toggle on Enable, then select Save.
    • If Enabled, legacy bot sign-up protection is already active and no action is required. To disable it, select Manage, toggle off Enable, then select Save.

Note

When enabled, users suspected of being a bot will be shown an interactive challenge (like clicking a checkbox) to verify they are human. The CAPTCHA widget will only be shown if the client is suspected to be a bot.

Deprecated

If your application previously had the Invisible CAPTCHA type selected, it's highly recommended to switch to the Smart option, as the Invisible option is deprecated. If the Dashboard doesn't show CAPTCHA type options for your application, legacy bot sign-up protection uses the Smart option and is enabled from the Bot sign-up protection row on the Protections page.

To render the CAPTCHA widget in your custom sign-up form, you need to include the <div id="clerk-captcha" /> element by the time you call signUp.create(). This element acts as a placeholder onto which the widget will be rendered.

If this element is not found, the SDK will transparently fall back to an invisible widget in order to avoid breaking your sign-up flow. If this happens, you should see a relevant error in your browser's console.

Tip

The invisible widget fallback automatically blocks suspected bot traffic without offering users falsely detected as bots with an opportunity to prove otherwise. Therefore, it's strongly recommended that you ensure the <div id="clerk-captcha" /> element exists in your DOM.

The following example shows how to support the CAPTCHA widget:

<>
  <h1>Sign up</h1>
  <form onSubmit={handleSubmit}>
    <div>
      <label htmlFor="email">Enter email address</label>
      <input
        id="email"
        type="email"
        name="email"
        value={emailAddress}
        onChange={(e) => setEmailAddress(e.target.value)}
      />
    </div>
    <div>
      <label htmlFor="password">Enter password</label>
      <input
        id="password"
        type="password"
        name="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
    </div>

    {/* Clerk's CAPTCHA widget */}
    <div id="clerk-captcha" />

    <button type="submit">Continue</button>
  </form>
</>

You can customize the appearance of the CAPTCHA widget by passing data attributes to the <div id="clerk-captcha" /> element. The following attributes are supported:

  • data-cl-theme: The CAPTCHA widget theme. Can take the following values: 'light', 'dark', 'auto'. Defaults to 'auto'.
  • data-cl-size: The CAPTCHA widget size. Can take the following values: 'normal', 'flexible', 'compact'. Defaults to 'normal'.
  • data-cl-language: The CAPTCHA widget language. Must be either 'auto' (default) to use the language that the visitor has chosen, or language and country code (e.g., 'en-US'). Some languages are supported by Clerk but not by Cloudflare Turnstile, which is used for the CAPTCHA widget. See Cloudflare Turnstile's supported languages.

For example, to set the theme to 'dark', the size to 'flexible', and the language to 'es-ES', you would add the following attributes to the <div id="clerk-captcha" /> element:

<div id="clerk-captcha" data-cl-theme="dark" data-cl-size="flexible" data-cl-language="es-ES" />

Feedback

What did you think of this content?

Last updated on