├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── recaptcha-enterprise.php ├── resources └── lang │ └── en │ └── validation.php └── src ├── Contracts └── RecaptchaContract.php ├── Exceptions ├── InvalidTokenException.php └── MissingPropertiesException.php ├── Facades └── RecaptchaEnterprise.php ├── Mocks └── FakeRecaptchaEnterprise.php ├── RecaptchaEnterpriseServiceProvider.php ├── Rules └── Recaptcha.php └── Services └── RecaptchaService.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-recaptcha-enterprise` will be documented in this file. 4 | 5 | ## v0.5.0 - 2025-04-11 6 | 7 | ### What's Changed 8 | 9 | * build(deps): bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/29 10 | * build(deps): bump aglipanci/laravel-pint-action from 2.4 to 2.5 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/30 11 | 12 | **Full Changelog**: https://github.com/oneduo/laravel-recaptcha-enterprise/compare/v0.4.3...v0.5.0 13 | 14 | ## v0.4.3 - 2025-01-08 15 | 16 | ### What's Changed 17 | 18 | * build(deps): bump codecov/codecov-action from 4 to 5 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/28 19 | 20 | **Full Changelog**: https://github.com/oneduo/laravel-recaptcha-enterprise/compare/v0.4.2...v0.4.3 21 | 22 | ## v0.4.2 - 2024-08-29 23 | 24 | ### What's Changed 25 | 26 | * test: fixed wrong localization test for validation by @Rezrazi in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/25 27 | * fix: move mocks to src directory by @rdesorme in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/21 28 | * build(deps): bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/26 29 | 30 | ### New Contributors 31 | 32 | * @Rezrazi made their first contribution in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/25 33 | * @rdesorme made their first contribution in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/21 34 | 35 | **Full Changelog**: https://github.com/oneduo/laravel-recaptcha-enterprise/compare/v0.4.1...v0.4.2 36 | 37 | ## v0.4.1 - 2024-05-11 38 | 39 | ### What's Changed 40 | 41 | * build(deps): bump dependabot/fetch-metadata from 1.6.0 to 2.1.0 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/22 42 | 43 | **Full Changelog**: https://github.com/oneduo/laravel-recaptcha-enterprise/compare/v0.4...v0.4.1 44 | 45 | ## v0.4 - 2024-05-11 46 | 47 | ### What's Changed 48 | 49 | * build(deps): bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/20 50 | 51 | **Full Changelog**: https://github.com/oneduo/laravel-recaptcha-enterprise/compare/v0.3...v0.4 52 | 53 | ## v0.3 - 2024-03-18 54 | 55 | **Full Changelog**: https://github.com/oneduo/laravel-recaptcha-enterprise/compare/v0.2.2...v0.3 56 | 57 | ## v0.2.2 - 2024-03-08 58 | 59 | ### What's Changed 60 | 61 | * build(deps): bump aglipanci/laravel-pint-action from 2.3.0 to 2.3.1 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/14 62 | * build(deps): bump codecov/codecov-action from 3 to 4 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/15 63 | * build(deps): bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/16 64 | 65 | **Full Changelog**: https://github.com/oneduo/laravel-recaptcha-enterprise/compare/v0.2.1...v0.2.2 66 | 67 | ## v0.2.1 - 2023-10-14 68 | 69 | ### What's Changed 70 | 71 | - build(deps): bump dependabot/fetch-metadata from 1.4.0 to 1.5.1 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/8 72 | - build(deps): bump aglipanci/laravel-pint-action from 2.2.0 to 2.3.0 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/9 73 | - build(deps): bump dependabot/fetch-metadata from 1.5.1 to 1.6.0 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/11 74 | - build(deps): bump actions/checkout from 3 to 4 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/12 75 | - build(deps): bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/13 76 | 77 | **Full Changelog**: https://github.com/oneduo/laravel-recaptcha-enterprise/compare/v0.2...v0.2.1 78 | 79 | ## v0.2 - 2023-05-17 80 | 81 | ### What's Changed 82 | 83 | - build(deps): bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/2 84 | - build(deps): bump aglipanci/laravel-pint-action from 2.1.0 to 2.2.0 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/4 85 | - build(deps): bump dependabot/fetch-metadata from 1.3.6 to 1.4.0 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/5 86 | - Update composer.json - Laravel 10 support by @syntaxlexx in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/6 87 | 88 | ### New Contributors 89 | 90 | - @syntaxlexx made their first contribution in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/6 91 | 92 | **Full Changelog**: https://github.com/oneduo/laravel-recaptcha-enterprise/compare/v0.1.1...v0.2 93 | 94 | ## v0.1.1 - 2023-01-21 95 | 96 | **Full Changelog**: https://github.com/oneduo/laravel-recaptcha-enterprise/compare/v0.1.0...v0.1.1 97 | 98 | ## v0.1.0 - 2023-01-17 99 | 100 | ### What's Changed 101 | 102 | - build(deps): bump aglipanci/laravel-pint-action from 1.0.0 to 2.1.0 by @dependabot in https://github.com/oneduo/laravel-recaptcha-enterprise/pull/1 103 | 104 | **Full Changelog**: https://github.com/oneduo/laravel-recaptcha-enterprise/commits/v0.1.0 105 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) oneduo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google reCAPTCHA Enterprise for Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/oneduo/laravel-recaptcha-enterprise.svg?style=flat-square)](https://packagist.org/packages/oneduo/laravel-recaptcha-enterprise) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/oneduo/laravel-recaptcha-enterprise/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/oneduo/laravel-recaptcha-enterprise/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/oneduo/laravel-recaptcha-enterprise/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/oneduo/laravel-recaptcha-enterprise/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/oneduo/laravel-recaptcha-enterprise.svg?style=flat-square)](https://packagist.org/packages/oneduo/laravel-recaptcha-enterprise) 7 | [![codecov](https://codecov.io/github/oneduo/laravel-recaptcha-enterprise/branch/main/graph/badge.svg)](https://codecov.io/github/oneduo/laravel-recaptcha-enterprise) 8 | 9 | Wrapper to use Google reCAPTCHA Enterprise with Laravel. Provides a handy validation rule to verify your token's score. 10 | 11 | ## Table of Contents 12 | 13 | - [Prerequisites](#prerequisites) 14 | - [Installation](#installation) 15 | - [Configuration](#configuration) 16 | - [Usage](#usage) 17 | 18 | 19 | ## Prerequisites 20 | 21 | **TLDR;** You may want to follow 22 | the [official documentation](https://cloud.google.com/recaptcha-enterprise/docs/set-up-google-cloud) to get started. 23 | 24 | ### 1. Enable the reCAPTCHA Enterprise API 25 | 26 | On your Google Cloud console, go ahead and enable the reCAPTCHA Enterprise API. 27 | 28 | ### 2. Create a service account 29 | 30 | Create a service account with the following roles: 31 | 32 | - reCAPTCHA Enterprise Agent 33 | 34 | ### 3. Create a key 35 | 36 | Create a key for your service account and download it as a JSON file. 37 | 38 | ### 4. Use your credentials 39 | 40 | Use your credentials by setting the appropriate values in `config/recaptcha-enterprise.php` or by setting the 41 | environment variables. 42 | 43 | ## Installation 44 | 45 | You can install the package via composer: 46 | 47 | ```bash 48 | composer require oneduo/laravel-recaptcha-enterprise 49 | ``` 50 | 51 | ## Configuration 52 | 53 | You can publish the config file with: 54 | 55 | ```bash 56 | php artisan vendor:publish --tag="recaptcha-enterprise-config" 57 | ``` 58 | 59 | This is the contents of the published config file, you are required to set the variables accordingly: 60 | 61 | ```php 62 | return [ 63 | 'site_key' => env('RECAPTCHA_ENTERPRISE_SITE_KEY'), 64 | 65 | 'use_credentials' => env('RECAPTCHA_ENTERPRISE_USE_CREDENTIALS', 'default'), 66 | 67 | 'credentials' => [ 68 | 'default' => [ 69 | 'type' => 'service_account', 70 | 'project_id' => env('RECAPTCHA_ENTERPRISE_PROJECT_ID'), 71 | 'private_key_id' => env('RECAPTCHA_ENTERPRISE_PRIVATE_KEY_ID'), 72 | 'private_key' => env('RECAPTCHA_ENTERPRISE_PRIVATE_KEY'), 73 | 'client_email' => $email = env('RECAPTCHA_ENTERPRISE_CLIENT_EMAIL'), 74 | 'client_id' => env('RECAPTCHA_ENTERPRISE_CLIENT_ID'), 75 | 'auth_uri' => 'https://accounts.google.com/o/oauth2/auth', 76 | 'token_uri' => 'https://accounts.google.com/o/oauth2/token', 77 | 'auth_provider_x509_cert_url' => 'https://www.googleapis.com/oauth2/v1/certs', 78 | 'client_x509_cert_url' => 'https://www.googleapis.com/robot/v1/metadata/x509/' . $email, 79 | ], 80 | ], 81 | ]; 82 | ``` 83 | 84 | ## Usage 85 | 86 | You may start using the reCAPTCHA validation rule by implementing the 87 | available `Oneduo\RecaptchaEnterprise\Rules\Recaptcha` rule in your business logic, here's an example of a `FormRequest` 88 | implementation: 89 | 90 | ```php 91 | ['required', new Recaptcha()], 106 | ]; 107 | } 108 | 109 | public function authorize(): bool 110 | { 111 | return true; 112 | } 113 | } 114 | 115 | ``` 116 | 117 | ### Configuring the threshold 118 | 119 | When validating a token, you may want to set a threshold for the score. You can do so setting the `score_threshold` 120 | config value: 121 | 122 | ```php 123 | 'score_threshold' => 0.7, 124 | ``` 125 | 126 | Default threshold is `0.5` 127 | 128 | ## Testing 129 | 130 | ```bash 131 | composer test 132 | ``` 133 | 134 | ## Changelog 135 | 136 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 137 | 138 | ## Contributing 139 | 140 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 141 | 142 | ## Security Vulnerabilities 143 | 144 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 145 | 146 | ## Credits 147 | 148 | - [Charaf Rezrazi](https://github.com/oneduo) 149 | - [All Contributors](../../contributors) 150 | 151 | ## License 152 | 153 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 154 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oneduo/laravel-recaptcha-enterprise", 3 | "description": "This is my package laravel-recaptcha-enterprise", 4 | "keywords": [ 5 | "oneduo", 6 | "laravel", 7 | "laravel-recaptcha-enterprise" 8 | ], 9 | "homepage": "https://github.com/oneduo/laravel-recaptcha-enterprise", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Charaf Rezrazi", 14 | "email": "charaf@rezrazi.fr", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "google/cloud-recaptcha-enterprise": "^1.2", 21 | "illuminate/contracts": ">=9.0", 22 | "spatie/laravel-package-tools": "^1.13.0" 23 | }, 24 | "require-dev": { 25 | "laravel/pint": "^1.0", 26 | "nunomaduro/collision": "^6.0", 27 | "nunomaduro/larastan": "^2.0.1", 28 | "orchestra/testbench": "^7.0", 29 | "pestphp/pest": "^1.21|^2.0", 30 | "pestphp/pest-plugin-laravel": "^1.1|^2.0", 31 | "phpstan/extension-installer": "^1.1", 32 | "phpstan/phpstan-deprecation-rules": "^1.0", 33 | "phpstan/phpstan-phpunit": "^1.0", 34 | "phpunit/phpunit": "^9.5", 35 | "spatie/laravel-ray": "^1.26" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Oneduo\\RecaptchaEnterprise\\": "src" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Oneduo\\RecaptchaEnterprise\\Tests\\": "tests" 45 | } 46 | }, 47 | "scripts": { 48 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 49 | "analyse": "vendor/bin/phpstan analyse", 50 | "test": "vendor/bin/pest", 51 | "test-coverage": "vendor/bin/pest --coverage", 52 | "format": "vendor/bin/pint" 53 | }, 54 | "config": { 55 | "sort-packages": true, 56 | "allow-plugins": { 57 | "pestphp/pest-plugin": true, 58 | "phpstan/extension-installer": true 59 | } 60 | }, 61 | "extra": { 62 | "laravel": { 63 | "providers": [ 64 | "Oneduo\\RecaptchaEnterprise\\RecaptchaEnterpriseServiceProvider" 65 | ], 66 | "aliases": { 67 | "RecaptchaEnterprise": "Oneduo\\RecaptchaEnterprise\\Facades\\RecaptchaEnterprise" 68 | } 69 | } 70 | }, 71 | "minimum-stability": "dev", 72 | "prefer-stable": true 73 | } 74 | -------------------------------------------------------------------------------- /config/recaptcha-enterprise.php: -------------------------------------------------------------------------------- 1 | env('RECAPTCHA_ENTERPRISE_SITE_KEY'), 7 | 8 | 'use_credentials' => env('RECAPTCHA_ENTERPRISE_USE_CREDENTIALS', 'default'), 9 | 10 | 'score_threshold' => env('RECAPTCHA_ENTERPRISE_SCORE_THRESHOLD', 0.5), 11 | 12 | 'credentials' => [ 13 | 'default' => [ 14 | 'type' => 'service_account', 15 | 'project_id' => env('RECAPTCHA_ENTERPRISE_PROJECT_ID'), 16 | 'private_key_id' => env('RECAPTCHA_ENTERPRISE_PRIVATE_KEY_ID'), 17 | 'private_key' => env('RECAPTCHA_ENTERPRISE_PRIVATE_KEY'), 18 | 'client_email' => $email = env('RECAPTCHA_ENTERPRISE_CLIENT_EMAIL'), 19 | 'client_id' => env('RECAPTCHA_ENTERPRISE_CLIENT_ID'), 20 | 'auth_uri' => 'https://accounts.google.com/o/oauth2/auth', 21 | 'token_uri' => 'https://accounts.google.com/o/oauth2/token', 22 | 'auth_provider_x509_cert_url' => 'https://www.googleapis.com/oauth2/v1/certs', 23 | 'client_x509_cert_url' => 'https://www.googleapis.com/robot/v1/metadata/x509/'.$email, 24 | ], 25 | ], 26 | ]; 27 | -------------------------------------------------------------------------------- /resources/lang/en/validation.php: -------------------------------------------------------------------------------- 1 | 'reCAPTCHA verification failed, reason: :reason', 5 | ]; 6 | -------------------------------------------------------------------------------- /src/Contracts/RecaptchaContract.php: -------------------------------------------------------------------------------- 1 | reason ? InvalidReason::name($reason) : 'Unspecified reason' 17 | )); 18 | } 19 | 20 | public static function forReason(?int $reason = null): self 21 | { 22 | return new self(reason: $reason); 23 | } 24 | 25 | /** 26 | * @codeCoverageIgnore 27 | */ 28 | public function context(): array 29 | { 30 | return [ 31 | 'reason' => $this->reason ? InvalidReason::name($this->reason) : null, 32 | 'code' => $this->reason, 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exceptions/MissingPropertiesException.php: -------------------------------------------------------------------------------- 1 | $this->assessment?->serializeToJsonString(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Facades/RecaptchaEnterprise.php: -------------------------------------------------------------------------------- 1 | assess($token); 36 | } 37 | 38 | public static function fake(?bool $alwaysValid = null, ?Closure $callback = null): RecaptchaContract 39 | { 40 | return tap(static::getFacadeRoot(), function (RecaptchaService $fake) use ($alwaysValid, $callback) { 41 | static::swap(is_callable($callback) ? $callback($fake) : new FakeRecaptchaEnterprise($alwaysValid)); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Mocks/FakeRecaptchaEnterprise.php: -------------------------------------------------------------------------------- 1 | threshold = config('recaptcha-enterprise.score_threshold'); 26 | } 27 | 28 | public function assess(string $token): static 29 | { 30 | return $this; 31 | } 32 | 33 | public function validateScore(): bool 34 | { 35 | // if no threshold is set, we assume it passes 36 | if ($this->threshold === null) { 37 | return true; 38 | } 39 | 40 | // if no score is set, we assume it fails 41 | if ($this->score === null) { 42 | return false; 43 | } 44 | 45 | // check if the score is higher than the threshold 46 | return $this->score >= $this->threshold; 47 | } 48 | 49 | public function validateAction(string $action): bool 50 | { 51 | return $this->properties->getAction() === $action; 52 | } 53 | 54 | public function validateCreationTime(CarbonInterval $interval): bool 55 | { 56 | $timestamp = $this->properties->getCreateTime()?->getSeconds(); 57 | 58 | return Carbon::parse($timestamp)->lessThanOrEqualTo(now()->sub($interval)); 59 | } 60 | 61 | public function isValid(?string $action = null, ?CarbonInterval $interval = null): bool 62 | { 63 | if (is_bool($this->alwaysValid)) { 64 | return $this->alwaysValid; 65 | } 66 | 67 | $valid = $this->validateScore(); 68 | 69 | if ($action) { 70 | $valid = $valid && $this->validateAction($action); 71 | } 72 | 73 | if ($interval) { 74 | $valid = $valid && $this->validateCreationTime($interval); 75 | } 76 | 77 | return $valid; 78 | } 79 | 80 | public function setScore(float $score): static 81 | { 82 | if (! app()->runningUnitTests()) { 83 | throw new RuntimeException('This method is only available in tests'); 84 | } 85 | 86 | $this->score = $score; 87 | 88 | return $this; 89 | } 90 | 91 | public function setThreshold(float $threshold): static 92 | { 93 | if (! app()->runningUnitTests()) { 94 | throw new RuntimeException('This method is only available in tests'); 95 | } 96 | 97 | $this->threshold = $threshold; 98 | 99 | return $this; 100 | } 101 | 102 | public function setProperties(TokenProperties $properties): static 103 | { 104 | if (! app()->runningUnitTests()) { 105 | throw new RuntimeException('This method is only available in tests'); 106 | } 107 | 108 | $this->properties = $properties; 109 | 110 | return $this; 111 | } 112 | 113 | public function forceValid(bool $value = true): static 114 | { 115 | if (! app()->runningUnitTests()) { 116 | throw new RuntimeException('This method is only available in tests'); 117 | } 118 | 119 | $this->alwaysValid = $value; 120 | 121 | return $this; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/RecaptchaEnterpriseServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-recaptcha-enterprise') 18 | ->hasTranslations() 19 | ->hasConfigFile(); 20 | } 21 | 22 | public function packageBooted(): void 23 | { 24 | $this->app->singleton(RecaptchaContract::class, fn () => new RecaptchaService()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Rules/Recaptcha.php: -------------------------------------------------------------------------------- 1 | reason = $exception->reason; 34 | 35 | return false; 36 | } 37 | 38 | $validAction = true; 39 | $validInterval = true; 40 | 41 | if ($this->action) { 42 | $validAction = $recaptcha->validateAction($this->action); 43 | } 44 | 45 | if ($this->interval) { 46 | $validInterval = $recaptcha->validateCreationTime($this->interval); 47 | } 48 | 49 | return $recaptcha->validateScore() && $validAction && $validInterval; 50 | } 51 | 52 | public function action(?string $action = null): static 53 | { 54 | $this->action = $action; 55 | 56 | return $this; 57 | } 58 | 59 | public function validity(?CarbonInterval $interval = null): static 60 | { 61 | $this->interval = $interval; 62 | 63 | return $this; 64 | } 65 | 66 | public function message(): string 67 | { 68 | return __('recaptcha-enterprise::validation.recaptcha', [ 69 | 'reason' => $this->reason ? InvalidReason::name($this->reason) : 'Unknown reason', 70 | ]); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Services/RecaptchaService.php: -------------------------------------------------------------------------------- 1 | client = app(RecaptchaClient::class, [ 33 | 'options' => [ 34 | 'credentials' => static::credentials(), 35 | ], 36 | ]); 37 | } 38 | 39 | protected static function credentials(): array 40 | { 41 | $useCredentials = config('recaptcha-enterprise.use_credentials'); 42 | 43 | return data_get(config('recaptcha-enterprise.credentials'), $useCredentials, []); 44 | } 45 | 46 | protected function projectName(): string 47 | { 48 | return RecaptchaClient::projectName(data_get(static::credentials(), 'project_id')); 49 | } 50 | 51 | protected function siteKey(): string 52 | { 53 | return config('recaptcha-enterprise.site_key'); 54 | } 55 | 56 | /** 57 | * Assess the token against Google reCAPTCHA Enterprise 58 | * 59 | * @throws MissingPropertiesException 60 | * @throws InvalidTokenException 61 | * @throws \Google\ApiCore\ApiException 62 | */ 63 | public function assess(string $token): static 64 | { 65 | $this->initAssessmentForEvent($this->event($token)); 66 | 67 | // we run the assessment through the reCAPTCHA Enterprise API client 68 | $this->assessment = $this->client->createAssessment($this->projectName(), $this->assessment); 69 | 70 | // The SDK documentation recommends closing the connection after each request. 71 | $this->close(); 72 | 73 | $this->properties = $this->assessment->getTokenProperties(); 74 | 75 | // throw an error if no properties are returned 76 | if ($this->properties === null) { 77 | throw MissingPropertiesException::forAssessment($this->assessment); 78 | } 79 | 80 | // throw an error if the token is invalid 81 | if (! $this->properties->getValid()) { 82 | throw InvalidTokenException::forReason($this->properties->getInvalidReason()); 83 | } 84 | 85 | // set the score 86 | $this->score = $this->assessment->getRiskAnalysis()?->getScore(); 87 | 88 | return $this; 89 | } 90 | 91 | protected function event(string $token): Event 92 | { 93 | return app(Event::class) 94 | ->setSiteKey($this->siteKey()) 95 | ->setToken($token); 96 | } 97 | 98 | protected function initAssessmentForEvent(Event $event): void 99 | { 100 | $this->assessment = app(Assessment::class)->setEvent($event); 101 | } 102 | 103 | public function validateScore(): bool 104 | { 105 | $threshold = config('recaptcha-enterprise.score_threshold'); 106 | 107 | // if no threshold is set, we assume it passes 108 | if ($threshold === null) { 109 | return true; 110 | } 111 | 112 | // if no score is set, we assume it fails 113 | if ($this->score === null) { 114 | return false; 115 | } 116 | 117 | // check if the score is higher than the threshold 118 | return $this->score >= $threshold; 119 | } 120 | 121 | public function validateAction(string $action): bool 122 | { 123 | return $this->properties->getAction() === $action; 124 | } 125 | 126 | public function validateCreationTime(CarbonInterval $interval): bool 127 | { 128 | $timestamp = $this->properties->getCreateTime()?->getSeconds(); 129 | 130 | return Carbon::parse($timestamp)->lessThanOrEqualTo(now()->sub($interval)); 131 | } 132 | 133 | public function isValid(?string $action = null, ?CarbonInterval $interval = null): bool 134 | { 135 | $valid = $this->validateScore(); 136 | 137 | if ($action) { 138 | $valid = $valid && $this->validateAction($action); 139 | } 140 | 141 | if ($interval) { 142 | $valid = $valid && $this->validateCreationTime($interval); 143 | } 144 | 145 | return $valid; 146 | } 147 | 148 | protected function close(): void 149 | { 150 | $this->client->close(); 151 | } 152 | } 153 | --------------------------------------------------------------------------------