DEV Community

Zach Koch
Zach Koch Subscriber

Posted on • Edited on

Using reCAPTCHA v3 with Next.js 14

Using useFormState and useActionState

Next.js documentation currently encourages the use of useFormState and useActionState to submit form data to your server actions.

I will show you how I got Google's reCaptcha v3 working with useFormState. This may also apply to useActionState, but I have not tested it.

Setting up reCAPTCHA

Go to the reCAPTCHA v3 admin console and create a new site if you don't have one yet

Your configuration should have

  • A label for your project
  • Score Based (v3) selected
  • localhost and your site url both added as domains

reCAPTCHA config

After submitting the next page will let you copy the keys that you will need to add to your project in the next section

reCAPTCHA keys

Setting up environment variables

In your project create a file called .env.local if you don't have one

In this file add your keys

.env.local


 bash
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=yoursitekeygoeshere
RECAPTCHA_SECRET_KEY=yoursecretkeygoeshere


Enter fullscreen mode Exit fullscreen mode

Make sure you have this file added to .gitignore

adding NEXT_PUBLIC_ to our variable allows it to be accessed in the browser

react-google-recaptcha-v3

install react-google-recaptcha-v3


 bash
npm i -S react-google-recaptcha-v3


Enter fullscreen mode Exit fullscreen mode

This package comes with GoogleReCaptchaProvider, a component that runs the reCaptcha script that detects bots and provides the result to any child components.

The documentation states that it should places as high as possible in the tree, but I found that I would get errors if it was outside the bounds of a client component.

In my case I wrapped it in the same file as my form component

Contact.tsx


 ts
'use client';
import {
    GoogleReCaptchaProvider, useGoogleReCaptcha
} from 'react-google-recaptcha-v3';

export default function Contact() {
    return (
        <GoogleReCaptchaProvider
            reCaptchaKey={
                process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ?? ''
            }>
            <ContactForm />
        </GoogleReCaptchaProvider>
    )
}

function ContactForm() {
    // my form component
}


Enter fullscreen mode Exit fullscreen mode

Integrate with useFormState

Normally we would set up useFormState by providing it with a server action that handles our form data.

In this case we want to add the reCAPTCHA data just before submitting to the server.

To do this we will

  • make a function with that accepts prevState and formData
  • get our captcha response from the reCAPTCHA script using executeRecaptcha
  • attach the captcha response to our form as a form field
  • send it off to our server action

Contact.tsx just inside your form component


 ts
const initialState: MessageState = { message: null, errors: {}};
const [state, formAction] = useFormState(addRecaptcha, initialState);
const { executeRecaptcha } = useGoogleReCaptcha();

async function addRecaptcha(prevState: MessageState, formData: FormData) {
    let gRecaptchaToken = ''
    if (executeRecaptcha) {
        gRecaptchaToken = await executeRecaptcha('contactMessage');
    }
    formData.set('captcha', gRecaptchaToken);

    return createMessage(prevState, formData)
}


Enter fullscreen mode Exit fullscreen mode

The form action will go at the top of your form

Contact.tsx



<form action={formAction}>
  // your form
</form>


Enter fullscreen mode Exit fullscreen mode

When you submit, the token that is created by the captcha provider will be sent to your server action as just another field

Validating captcha data

In your server action you will recieve your captcha token as part of your form data.

You can send the token to Google to verify

If the verification fails you can treat it the same way you would treat validation for any failed field in your form.

Contact.tsx



async function validateCaptcha(captchaToken: string): Promise<boolean> {
    const minimumCaptchaScore = 0.7;
    const secretKey = process.env.RECAPTCHA_SECRET_KEY || '';
    const data = new FormData();
    data.append('secret', secretKey);
    data.append('response', captchaToken);
    const captchaResponse = await fetch('https://www.google.com/recaptcha/api/siteverify', {
        method: "POST",
        body: data,
    });
    const res = await captchaResponse.json();
    console.log(`captcha score: ${res.score}`);
    return res.score && res.score >= minimumCaptchaScore;
}
const valid = await validateCaptcha(formData.get('captcha') ?? '')


Enter fullscreen mode Exit fullscreen mode

The captcha score I'm still playing with, there doesn't seem to be a clear answer, but in my testing I usually get a 0.9

A friend of mine pointed out that using FormData would be more secure than building the url with a template string.

Hopefully that is enough to help you integrate captcha. I'll update this for useActionState in the future. I am currently using this on my own site. Let me know in the comments if you spot any issues or if anything is unclear.

Top comments (1)

Collapse
 
tomcatbuzz profile image
Tony • Edited

Did you test this on a live deployment? I have been struggling to get recaptchav3 to work, using vanilla recaptcha and the react-recapatchaV3. It seems ad blockers are blocking the recaptcha-api script and not showing the captcha banner and failing any form submission.