Implementing Google reCAPTCHA v3 Without Packages in Nuxt and Laravel
I wanted to implement Google reCAPTCHA to keep bots out of my project, yotjob.com. While there are packages in the Vue community like vue-recaptcha and vue-recaptcha-v3, I decided to implement it myself. Eliminating the overhead of dependencies was important to me, especially since these libraries don't seem well-maintained—they have unattended issues in their repositories.
In this post, I'll walk you through how I integrated Google reCAPTCHA v3 into my Nuxt frontend and Laravel backend without relying on external packages.
Why Avoid External Packages?
Before diving into the implementation, I want to touch on why I chose not to use existing packages:
- Maintenance Concerns: The repositories had several unattended issues, which made me question their reliability.
- Overhead of Dependencies: Adding unnecessary dependencies can bloat your project and introduce potential security risks.
- Learning Opportunity: Implementing it myself gave me a deeper understanding of how reCAPTCHA works.
Backend Implementation (Laravel)
Creating the RecaptchaRule
First, I created a custom validation rule in Laravel to verify the reCAPTCHA token sent from the frontend.
File: App\Rules\RecaptchaRule.php
Read also : Debugging Laravel Sanctum SPA Authentication Issues.
post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('services.recaptcha.secret_key'),
'response' => $value,
'remoteip' => request()->ip(),
]);
$responseJson = $response->json();
if (!$responseJson['success'] || $response->failed()) {
$fail('Recaptcha validation failed.');
} else if ($responseJson['score'] < 0.7) {
$fail("We're having trouble processing your request at the moment. Please ensure you're not a robot and try again.");
}
}
}
Explanation:
- HTTP Request: We send a POST request to Google's reCAPTCHA verification API using the
Http
facade. - Parameters:
secret
: Your secret key from Google reCAPTCHA settings.response
: The token received from the frontend.remoteip
: The user's IP address.
- Validation Logic:
- If the response isn't successful or fails, we add a validation error.
- We also check the score returned by Google. If it's below
0.7
, we consider the interaction suspicious.
Modifying the Registration Controller
Next, I updated the registration controller to include the new validation rule.
File: App\Http\Controllers\Auth\RegisteredUserController.php
public function store(Request $request): JsonResource
{
// ... other code ...
$validationRules = [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'lowercase', 'string', 'email:rfc,dns', 'max:255', 'unique:' . User::class],
'referrer_id' => ['nullable', 'exists:users,id'],
'timezone_id' => ['required', 'exists:timezones,id'],
'country_id' => ['required', 'exists:countries,id'],
'recaptcha_token' => ['required', new RecaptchaRule],
];
// ... other code ...
$validated = Validator::validate($request->all(), $validationRules);
// ... other code ...
}
Explanation:
- Validation Rules: Added
'recaptcha_token'
with our customRecaptchaRule
. - Validation Execution: Using
Validator::validate()
to enforce our rules.
Frontend Implementation (Nuxt.js)
Setting Up the Component
In my register.vue
component, I prepared the form data and included a method to handle form submission.
Read also : Creating Nested Layouts in Nuxt 3: A Step-by-Step Guide.
File: pages/register.vue
Explanation:
- Form Data: Managed using the
ref
API. - Recaptcha Token: We obtain the token before submitting the form.
- Error Handling: Displaying validation errors if any.
Implementing the useRecaptcha
Composable
I created a composable to handle loading the reCAPTCHA script and obtaining the token.
File: composables/useRecaptcha.ts
export function useRecaptcha(action: string = 'submit') {
const config = useRuntimeConfig();
const tokenExpiration = 100; // 100 seconds
const recaptchaScript = useScript({
src: `https://www.google.com/recaptcha/api.js?render=${config.public.services.recaptcha.siteKey}`,
tagPosition: 'head',
async: true,
defer: true,
});
const recaptchaToken = useCookie('recaptchaToken', {
maxAge: tokenExpiration,
});
let isScriptLoaded = false;
let isGrecaptchaReady = false;
recaptchaScript.onLoaded(() => {
isScriptLoaded = true;
});
async function waitForScriptLoad() {
while (!isScriptLoaded) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
async function waitForGrecaptchaReady() {
return new Promise((resolve) => {
grecaptcha.ready(() => {
isGrecaptchaReady = true;
resolve();
});
});
}
async function setRecaptchaToken() {
if (recaptchaToken.value) {
return;
}
try {
await waitForScriptLoad();
await waitForGrecaptchaReady();
const token = await grecaptcha.execute(config.public.services.recaptcha.siteKey, { action: action });
recaptchaToken.value = token;
return token;
} catch (error) {
console.error('Error fetching reCAPTCHA token:', error);
setRecaptchaToken();
}
}
return {
recaptchaToken,
setRecaptchaToken,
};
}
Explanation:
Read also : Validating dynamic pages in Nuxt using API queries.
- Loading the Script: Using
useScript
to dynamically load the reCAPTCHA API. - Token Management: Storing the token in a cookie to reuse it within its expiration time.
- Async Handling: Ensuring the script is loaded and
grecaptcha
is ready before executing. - Error Handling: Logging errors and retrying token fetching if necessary.
Using the Composable in the Component
Back in register.vue
, I utilized the composable.
Explanation:
- Token Retrieval: Before form submission, we await
setRecaptchaToken()
to ensure we have a valid token. - Form Data: We include
recaptcha_token
in the form data sent to the backend.
Challenges and Solutions
Handling Async Script Loading
One of the challenges was ensuring the reCAPTCHA script is fully loaded before attempting to use grecaptcha
. I addressed this by:
- Polling for Script Load: Implemented
waitForScriptLoad()
to check if the script has loaded. - Using
grecaptcha.ready()
: Ensured thatgrecaptcha
is ready before callingexecute
.
Token Expiration
Tokens expire every two minutes, so I:
- Used Cookies: Stored the token in a cookie with a
maxAge
slightly less than two minutes. - Checked for Existing Tokens: Before fetching a new token, I checked if a valid one already exists.
Conclusion
Implementing Google reCAPTCHA v3 without external dependencies turned out to be a rewarding experience. It gave me more control over the integration and reduced the bloat in my project. If you're considering adding reCAPTCHA to your Nuxt and Laravel application, I recommend trying it yourself—you might find it's not as daunting as it seems.
Read also : Apply middleware to route groups in NuxtJs in Nuxt config.
Bonus Tip: Always keep an eye on the maintenance status of third-party packages. Sometimes, rolling up your sleeves and doing it yourself is the better option.