Stop bothering people with reCaptcha; use reCaptcha

Are you using reCaptcha v2 to protect forms against bots? While it works well, it forces a lot of your visitors to keep picking traffic lights, motorcycles, and pathways before actually submitting the data. Is v3 going to help?

Ondrej Polesny

Published on Oct 3, 2023

The reCaptcha version 3 comes with the promise of mitigating that problem. It works entirely differently, and funny enough, you’ll most likely need to run both versions simultaneously.

Why?

Let me first explain how reCaptcha v3 works. On the form, you generate a unique token. When a user submits the form, your API route needs to verify that token with the reCaptcha API. Its response contains a number between 0 and 1 describing the likelihood of the request being submitted by a human. 0 means that the request was submitted by a bot, and 1 means complete confidence that it was a human.

Diagram showing how reCaptcha v3 works

ReCaptcha doesn’t just randomly guess the score. It registers the activity of each visitor on every page, evaluates the behavior, and calculates the score when they decide to submit a form.

Diagram showing how reCaptcha v3 evaluates visitor behavior

This works well, but what do you do when the check fails? When reCaptcha flags a real visitor as a bot? We’re only working with probabilities, after all. Then, it’s time for the reCaptcha v2 again.

Diagram showing reCaptcha v2 used as a fallback in case reCaptcha v3 marks the visitor as non-human

So yes, in most cases, you’ll end up using both versions of reCaptcha. The good news is that the vast majority of your visitors will have a smooth experience and end up not being bothered by reCaptcha at all. The v2 is simply just a fallback.

Implementation of reCaptcha v3 in Next.js

So how do we implement this thing in Next.js or any other similar web framework?

Add the tracking/reCaptcha JavaScript

First, we add the following script to every page of our website. As I mentioned before, Google needs to track the user activity as a pre-requisite to the scoring:

 <script src="https://www.google.com/recaptcha/api.js"></script>

Generate reCaptcha v3 token

The next step depends on your implementation of forms. If you’re using a simple form element, you can follow the official docs and bind the token generation to the submit button. However, if you’re using any additional logic or form-handling library like Formik, you’ll need to generate the token programmatically when the visitor submits the form. In that case, you need to extend the global script with the reCaptcha site key:

 <script src="https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key"></script>

Note: You can get the reCaptcha site key in the reCaptcha admin dashboard.

Then, we generate the token in the onSubmit callback right before submitting the data for the token validation:

<Formik
	...
	onSubmit={async (values, { setSubmitting }) => {
		...
		values['recaptchaV3'] = await window.grecaptcha.execute(
			process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY_V3,
			{ action: 'submit' }
		)
		const response = await onSuccessCallback(values)
		...
	}
/>

Note: The onSuccessCallback is the callback function that submits data to the respective API. The onSubmit function is only relevant to Formik. You can generate the token in any function that handles the form submission.

Validate reCaptcha v3 token in API route

When a visitor submits the form, your API handler must validate the reCaptcha token. The validation process for v2 and v3 of reCaptcha is almost the same, with one difference. As I mentioned above, v3 returns a score describing the likelihood of dealing with a human. You need to decide what is the minimum threshold for your application. The 0.8 is a good default setting:

export const validateRecaptchaV3 = async (code: string): Promise<boolean> => {
	try {
		const response = await fetch(
			`https://www.google.com/recaptcha/api/siteverify?secret=RECAPTCHA_SECRET_V3&response=${code}`
		)
		const data = await response.json()
		if (!data.success){
			console.error(data)
			return false
		}
		return data.score && data.score >= 0.8
	} catch (err) {
		console.error(err)
	}
	return false
}

With reCaptcha v2, the remote endpoint decides for you:

export const validateRecaptchaV2 = async (code: string): Promise<boolean> => {
	try {
		const response = await fetch(
			`https://www.google.com/recaptcha/api/siteverify?secret=RECAPTCHA_SECRET_V2&response=${code}`
		)
		const data = await response.json()
		return data.success && data.success === true
	} catch (err) {
		console.error(err)
	}
	return false
}

In both cases, we refuse to service the request if the validation fails. But, we need to distinguish v2 from v3; if v3 fails, we need to display v2 on the front end and let the user complete a challenge.

const formHandler = async (req: NextApiRequest, res: NextApiResponse) => {
	try {
		const fields = JSON.parse(req.body)
		// validate recaptcha v3 only if v2 not present
		if (!fields.recaptchaV3) {
			const recaptchaV3Response = await validateRecaptchaV3(fields.recaptchaV3)

			if (!recaptchaV3Response) {
				res.status(511).json({
					message: 'Recaptcha v3 validation failed.',
					fallbackChallengeRequired: true,
				})
				return
			}
		} else {
			const recaptchaV2Response = await validateRecaptchaV2(fields.recaptcha)
			if (!recaptchaV2Response) {
				res.status(511).json({
					message: 'Recaptcha v2 validation failed.',
				})
				return
			}
		}

                // remove recaptcha fields from the request for further processing
		delete fields.recaptchaV3
		delete fields.recaptcha

Note: I used the response status code 511 (network authentication required) as it’s closest to what is actually happening. The code is only used to let the front end know what happened. You can use a different status code if you prefer.

Display v2 as fallback

The last part of the implementation is handling the case of failed v3 validation:

<Formik
	...
	onSubmit={async (values, { setSubmitting }) => {
		...
		const response = await onSuccessCallback(valuesToSubmit)
                if (response.status !== 200) {
                    switch (result) {
                        case 511:
                            // recaptcha validation failed
                            if ((await response.json()).fallbackChallengeRequired){
                                // v3 failed, show v2
                                setShowRecaptchaV2(true)
                                setErrorMessage('Please complete reCAPTCHA v2.')
                            } else {
                                setErrorMessage('Recaptcha v2 validation failed.')
                            }
                            break
                        default:
                            setErrorMessage('Something went wrong. Please try again later.')
                    }
                }
		...
	}
/>

				

We’re simply looking at the status code of the response and handling reCaptcha failed validation based on the fallbackChallengeRequired field. That is only present in the case of the v3 response. The setShowRecaptchaV2 is a React hook showing the v2 field at the bottom of the form:

...
const [showRecaptchaV2, setShowRecaptchaV2] = useState(false)
...
{showRecaptchaV2 && <CaptchaV2 />}
...

In other cases, we display generic or field-specific error messages ("Recaptcha v2 validation failed.").

With this implementation, when the reCaptcha v3 validation fails, we display the reCaptcha v2 box, and the visitor needs to complete the presented challenge. Once they resubmit the form, we validate the request again and either process it or return back to the user for another challenge.

Conclusion

In this article, I showed you the basic process of validating user submissions with reCaptcha v3 and explained why v2 typically still needs to be present as a fallback. I walked you through the implementation of such validation in Next.js. However, the principles can be applied to any framework and used library. ReCaptcha v3 greatly improves the user experience, and unobtrusive submission of forms naturally leads to increased conversions. That’s a good benefit for a bit of additional logic around your forms.

To learn more about reCaptcha, handling form validations, and web development in general, join the Kontent.ai community on Discord.

Feeling like your brand’s content is getting lost in the noise?

Listen to our new podcast for practical tips, tricks, and strategies to make your content shine. From AI’s magic touch to content management mastery and customer experience secrets, we’ll cover it all.

Kontent Waves

Subscribe to the Kontent.ai newsletter

Stay in the loop. Get the hottest updates while they’re fresh!