- 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)