Using reCAPTCHA on a Astro form
By Flavio Copes
Learn how to add Google reCAPTCHA to an Astro form to stop spam, wiring the widget with your site key and verifying the response on the server with siteverify.
To use reCAPTCHA on an Astro form you do three things: load the reCAPTCHA script on the page, wire the widget to your submit button with your site key, and verify the token on the server with the siteverify endpoint.
Here’s how I used Recaptcha on a Astro form to prevent spam and form submission abuse.
Get the keys
Set Recaptcha first, and grab the RECAPTCHA_SITE_KEY and RECAPTCHA_SECRET_KEY variables, put them in .env or anywhere you manage env vars.
The two keys have very different roles. The site key is public, it ends up in your HTML. The secret key must never leave the server. If it ends up in client-side code, anyone can call siteverify pretending to be you.
The form
In the Astro component, the first script loads the reCAPTCHA library. The submit button gets the g-recaptcha class and the data-sitekey attribute, which binds the widget to it. When the user passes the check, reCAPTCHA calls the function named in data-callback, and there we submit the form:
<script
is:inline
src='https://www.google.com/recaptcha/api.js'></script>
<script is:inline>
function recaptcha() {
document.querySelector('form').submit()
}
</script>
<form method='post'>
...
<input
type='submit'
class='block w-full px-3 py-2 mt-8 text-sm font-semibold text-center text-white cursor-pointer leading-6 rounded-md g-recaptcha focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 ring-1 ring-inset ring-blue-200 hover:ring-blue-300 hover:bg-blue-600'
data-sitekey={import.meta.env
.RECAPTCHA_SITE_KEY ||
process.env.RECAPTCHA_SITE_KEY}
data-callback='recaptcha'
data-action='submit'
value='Login'
/>
</form>
One thing that’s easy to miss: reCAPTCHA adds a hidden field named g-recaptcha-response to the form. That field carries the token, and it’s what we read on the server.
Verifying on the server
The widget alone protects nothing. A bot can skip your page entirely and POST straight to your endpoint. The real protection is the server-side check: we take the token from the form data and send it to Google’s siteverify endpoint, along with the secret key.
Server-side form POST request handler:
export async function processCaptcha(g_recaptcha_response: string) {
const url =
'https://www.google.com/recaptcha/api/siteverify'
const requestBody = new URLSearchParams({
secret:
import.meta.env.RECAPTCHA_SECRET_KEY ||
process.env.RECAPTCHA_SECRET_KEY,
response: g_recaptcha_response
})
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: requestBody.toString()
})
const data = await response.json()
console.log(data)
/*
{
success: true,
challenge_ts: '2024-01-20T18:18:12Z',
hostname: 'localhost',
score: 0.9,
action: 'submit'
}
*/
return data.success
}
if (Astro.request.method === 'POST') {
const formData = await Astro.request.formData()
const email = formData.get('email')?.toString() || ''
const password =
formData.get('password')?.toString() || ''
const g_recaptcha_response =
formData.get('g-recaptcha-response')?.toString() || ''
const is_valid_captcha = await processCaptcha(g_recaptcha_response)
if (!is_valid_captcha) {
error = 'Invalid captcha'
} else {
//valid
}
}
When verification fails, success is false and the response includes an error-codes array telling you why: an expired token, a missing token, a wrong secret. Logging that array saves a lot of guessing while you set this up.
Notice the request only proceeds when data.success is true. If a bot posts without a token, or with a reused one, the check fails and we return the “Invalid captcha” error.
Be careful with one detail: tokens are single-use and they expire after a couple of minutes. Verify the token in the same request that handles the form submission, don’t store it for later. If a user leaves the form open too long before submitting, verification fails, so show a clear error and let them retry.
Related posts about astro: