This gist will guide you through the process of integrating Google ReCaptcha v3 into a Laravel Jetstream w/ Inertia application.
First, obtain your ReCaptcha v3 API keys from the Google ReCaptcha Admin Console. Add these keys to your .env file:
RECAPTCHA_SITE_KEY=your_site_key_here
RECAPTCHA_SECRET_KEY=your_secret_key_here
Next, update your config/services.php file to include the ReCaptcha configuration. This configuration allows us to access our ReCaptcha keys throughout the application using Laravel's config helper. It keeps our sensitive keys secure by referencing environment variables.
'recaptcha' => [
'site_key' => env('RECAPTCHA_SITE_KEY'),
'secret_key' => env('RECAPTCHA_SECRET_KEY'),
],
Create a new file app/Actions/Fortify/CaptchaValidation.php. This class handles the server-side validation of the ReCaptcha response. It sends a POST request to Google's verification endpoint, logs the result, and returns a boolean indicating whether the validation was successful.
namespace App\Actions\Fortify;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class CaptchaValidation
{
public function validate($captchaResponse)
{
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('services.recaptcha.secret_key'),
'response' => $captchaResponse,
]);
$result = $response->json();
Log::info('ReCaptcha validation result', $result);
if (!$result['success']) {
Log::warning('ReCaptcha validation failed', $result);
return false;
}
Log::info('ReCaptcha validation successful', [
'score' => $result['score'] ?? 'N/A',
'action' => $result['action'] ?? 'N/A',
]);
return true;
}
}
Create app/Actions/Fortify/AuthenticateUser.php. This new class is responsible for authenticating users. It checks the user credentials and validates the ReCaptcha response. It uses caching to prevent multiple ReCaptcha validations for the same token, improving performance.
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Fortify;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class AuthenticateUser
{
public function __invoke($request)
{
$user = User::where('email', $request->email)->first();
if ($user) {
$cacheKey = 'recaptcha_' . md5($request->input('g-recaptcha-response'));
try {
$validationResult = Cache::remember($cacheKey, 60, function () use ($request) {
return app(CaptchaValidation::class)->validate($request->input('g-recaptcha-response'));
});
Log::info('ReCaptcha validation result', ['result' => $validationResult]);
if ($validationResult && Hash::check($request->password, $user->password)) {
return $user;
}
} catch (\Exception $e) {
Log::error('ReCaptcha validation error', ['error' => $e->getMessage()]);
}
}
throw ValidationException::withMessages([
Fortify::username() => [trans('auth.failed')],
]);
}
}
Modify app/Actions/Fortify/CreateNewUser.php. We've added ReCaptcha validation to the user registration process. This ensures that a valid ReCaptcha response is required before creating a new user.
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers;
use Laravel\Jetstream\Jetstream;
class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules;
public function create(array $input): User
{
// Add this
app(CaptchaValidation::class)->validate($input['g-recaptcha-response']);
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => $this->passwordRules(),
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
])->validate();
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
}
}
Modify app/Http/Middleware/HandleInertiaRequests.php. This modification shares the ReCaptcha site key with all Inertia responses, making it available in our Vue components.
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'recaptchaSiteKey' => config('services.recaptcha.site_key'),
// ... other shared data
]);
}
Create a new file resources/js/Components/ReCaptcha.vue. This component handles the client-side integration of ReCaptcha. It loads the ReCaptcha script, executes the ReCaptcha challenge, and emits the response token to be used in form submissions.
<script setup>
import { onMounted, ref } from 'vue';
const props = defineProps({
action: {
type: String,
required: true
},
siteKey: {
type: String,
required: true
}
});
const emit = defineEmits(['captcha-response']);
const captchaResponse = ref(null);
onMounted(() => {
const script = document.createElement('script');
script.src = `https://www.google.com/recaptcha/api.js?render=${props.siteKey}`;
document.head.appendChild(script);
script.onload = () => {
window.grecaptcha.ready(() => {
window.grecaptcha.execute(props.siteKey, { action: props.action })
.then(token => {
captchaResponse.value = token;
emit('captcha-response', token);
});
});
};
});
</script>
<template>
<input type="hidden" name="g-recaptcha-response" :value="captchaResponse">
</template>
Modify resources/js/Pages/Auth/Register.vue and resources/js/Pages/Auth/Login.vue to include the ReCaptcha component. The setCaptchaResponse function updates the form data with the ReCaptcha token when received from the ReCaptcha component.
<script setup>
import ReCaptcha from '@/Components/ReCaptcha.vue';
const form = useForm({
// ... other form fields
'g-recaptcha-response': '',
});
const setCaptchaResponse = (response) => {
form['g-recaptcha-response'] = response;
};
</script>
<template>
<!-- ... other form fields -->
<ReCaptcha
action="register"
:site-key="$page.props.recaptchaSiteKey"
@captcha-response="setCaptchaResponse"
/>
</template>
These changes work together to integrate ReCaptcha v3 into the authentication flow of a Laravel Jetstream application, enhancing security without significantly impacting the user experience.
Thank you so much for putting this together @geosem42. I had a similar implementation but wasn't able to figure out on how could I add this to the Login Page also, totally forgot about the Fortify Actions. One thing missing in this guide would be adding the
AuthenticateUserAction in theFortifyServiceProvider, otherwise this won't work. Maybe something you can add so others won't have to wonder why it's not working if they forget about this step. I have also enhanced theCaptchaValidationwith rate limiting and IP blocking, I can also add this here if interested. 🙂