Handle sign-up challenges in your custom flow
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/nextjs7 or later,@clerk/react6 or later,@clerk/react-router3 or later, or@clerk/tanstack-react-start1 or later. Flows built with the legacy API, such as the hooks imported from@clerk/nextjs/legacyorclerk.client.signUp, and flows built with other SDKs, aren't supported yet. - It loads ClerkJS 6.36.0 or later and
@clerk/ui1.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/uiwith theuiprop, or pins a ClerkJS or@clerk/uiversion, 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}orNEXT_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
signUpmethod returns without an error, but the sign-up stays incomplete, withprotect_checkinsignUp.missingFields. - The challenge can't load, because the Content Security Policy blocks it. The dialog closes and the
signUpmethod returns anerrorwith the codeprotect_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.
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).
'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
- In the Clerk Dashboard, navigate to the Protections page under Protect.
- 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.
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.
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
Last updated on