- how to expose the site key to your Vue frontend (Vite)
- how to render and get the reCAPTCHA token in Vue
- how to verify the token in a Laravel controller using your secret key
- testing tips and common troubleshooting
Why this pattern?
reCAPTCHA v2 ("I'm not a robot")
requires a site key on the frontend and a secret key on the server. The site key can safely be bundled into the frontend (build-time env). The secret key must never be sent to the browser — verify tokens server-side with Google.
Prerequisites
- Laravel (9/10+) using Vite for frontend assets
- Vue 3 (inside Laravel or as a separate SPA that talks to the Laravel API)
- Google reCAPTCHA v2 keys (Site Key + Secret Key)
Register your site at Google reCAPTCHA admin and pick reCAPTCHA v2 → “I’m not a robot”.
1) Add keys to .env
# frontend (Vite) — exposed to browser (build time)
VITE_RECAPTCHA_SITE_KEY=your_site_key_here
# backend — keep secret, server-side only
RECAPTCHA_SECRET_KEY=your_secret_key_here
After changing
.env
, restart your dev server (npm run dev
/pnpm dev
) so Vite picks up the env variables.
Optionally add to config/services.php
:
// config/services.php
return [
// ...
'recaptcha' => [
'secret' => env('RECAPTCHA_SECRET_KEY'),
],
];
2) Frontend — render reCAPTCHA and get token (manual, no extra package)
This is robust and avoids package compatibility issues. The example is a Vue 3 Single File Component (Options API) that dynamically loads the Google script and renders an explicit widget.
<!-- resources/js/components/RecaptchaV2.vue -->
<template>
<div>
<div ref="recaptcha"></div>
<p v-if="!recaptchaToken">Please check “I’m not a robot” to continue.</p>
<p v-else>reCAPTCHA token ready ✅</p>
<button @click="submitForm">Submit</button>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
// ✅ Site key from .env
const siteKey = import.meta.env.VITE_RECAPTCHA_SITE_KEY;
// Refs (reactive data)
const recaptcha = ref(null);
const recaptchaToken = ref(null);
let widgetId = null;
// ✅ Load Google reCAPTCHA script
const loadRecaptchaScript = () => {
return new Promise((resolve) => {
if (window.grecaptcha) return resolve();
const src = "https://www.google.com/recaptcha/api.js?render=explicit";
const s = document.createElement("script");
s.src = src;
s.async = true;
s.defer = true;
s.onload = () => resolve();
document.head.appendChild(s);
});
};
// ✅ Render widget when script is ready
const renderRecaptcha = () => {
if (!window.grecaptcha) return;
widgetId = window.grecaptcha.render(recaptcha.value, {
sitekey: siteKey,
callback: onVerify,
"expired-callback": onExpired,
});
};
// ✅ Callback: success
const onVerify = (token) => {
recaptchaToken.value = token;
console.log("reCAPTCHA token:", token);
};
// ✅ Callback: expired
const onExpired = () => {
recaptchaToken.value = null;
};
// ✅ Submit form with token
const submitForm = async () => {
if (!recaptchaToken.value) {
alert("Please complete reCAPTCHA first.");
return;
}
try {
const res = await fetch("/api/verify-recaptcha", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ token: recaptchaToken.value }),
});
const data = await res.json();
if (res.ok && data.success) {
alert("Verification passed ✅ Proceed with your action.");
} else {
console.error("reCAPTCHA verify failed:", data);
alert("reCAPTCHA verification failed. Try again.");
}
} catch (err) {
console.error(err);
alert("Server error while verifying reCAPTCHA.");
} finally {
if (window.grecaptcha && widgetId !== null) {
window.grecaptcha.reset(widgetId);
recaptchaToken.value = null;
}
}
};
// ✅ Load on mount
onMounted(async () => {
await loadRecaptchaScript();
renderRecaptcha();
});
</script>
Notes:
- We use
import.meta.env.VITE_RECAPTCHA_SITE_KEY
— Vite exposes envs that start withVITE_
. -
render=explicit
lets us callgrecaptcha.render()
manually and attach callbacks.
3) Backend — Laravel controller to verify token with Google
Create a controller:
// app/Http/Controllers/RecaptchaController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class RecaptchaController extends Controller
{
public function verify(Request $request)
{
$request->validate(['token' => 'required|string']);
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('services.recaptcha.secret') ?? env('RECAPTCHA_SECRET_KEY'),
'response' => $request->input('token'),
'remoteip' => $request->ip(),
]);
$body = $response->json();
// return Google response directly (you can normalize it as you like)
if (!empty($body) && isset($body['success']) && $body['success'] === true) {
return response()->json(['success' => true, 'score' => $body['score'] ?? null]);
}
return response()->json([
'success' => false,
'error_codes' => $body['error-codes'] ?? $body['error_codes'] ?? []
], 422);
}
}
Add the route (usually routes/api.php
):
use App\Http\Controllers\RecaptchaController;
Route::post('/verify-recaptcha', [RecaptchaController::class, 'verify']);
Security tips
- Use
config('services.recaptcha.secret')
orenv()
on the server — never expose the secret to the frontend. - Optionally add rate limiting middleware to this endpoint.
Top comments (0)