Skip to content

Instantly share code, notes, and snippets.

@geosem42
Last active August 29, 2025 01:46
Show Gist options
  • Save geosem42/103c39cdf4bb685b51dd10fc38e4eace to your computer and use it in GitHub Desktop.
Save geosem42/103c39cdf4bb685b51dd10fc38e4eace to your computer and use it in GitHub Desktop.
Integrating Google ReCaptcha v3 in Laravel Jetstream with Inertia

This gist will guide you through the process of integrating Google ReCaptcha v3 into a Laravel Jetstream w/ Inertia application.

API Keys and Configuration

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 CaptchaValidation Action

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 AuthenticateUser Action

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')],
        ]);
    }
}

Update CreateNewUser Action

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']),
        ]);
    }
}

Update HandleInertiaRequests Middleware

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 ReCaptcha Vue Component

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>

Update Register and Login Vue Components

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.

@captainscorch
Copy link

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 AuthenticateUser Action in the FortifyServiceProvider, 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 the CaptchaValidation with rate limiting and IP blocking, I can also add this here if interested. 🙂

use App\Actions\Fortify\AuthenticateUser;

Fortify::authenticateUsing(function (Request $request) {
    return app(AuthenticateUser::class)->__invoke($request, function ($request) {
        return true;
    });
});

@geosem42
Copy link
Author

@captainscorch I'm glade you found this gist helpful. I'm not sure if you can add the new additions yourself though, as far as I know gists cannot be edited by others.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment