From a1312a94c12b03c6420507077cb4f54ebf484a7a Mon Sep 17 00:00:00 2001 From: Italo Date: Tue, 4 Jan 2022 23:04:18 -0300 Subject: [PATCH] Adds ability to use default auth guard (#50) * Adds tests for authenticated users. * Updates code examples on the README. * Updates README.md * Fixes wrong variable name * Fixed Generic user not being instanced properly * Fixes tests for score challenges * Fixes guard not configured * Fixes Captchavel mock for auth tests. * Fixes authentication tests * Fixes route controller test * More test fixes * More tests fixes * Fixes helper not taking into account default guard --- README.md | 43 +++++-- src/Http/Middleware/VerificationHelpers.php | 4 + src/ReCaptcha.php | 19 ++- .../Middleware/ChallengeMiddlewareTest.php | 109 ++++++++++++++++++ tests/Http/Middleware/ScoreMiddlewareTest.php | 57 +++++++++ tests/ReCaptchaMiddlewareHelperTest.php | 8 ++ 6 files changed, 228 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 96a6837..7c1f795 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,9 @@ Route::post('comment', [CommentController::class, 'create']) #### Bypassing on authenticated users -Sometimes you may want to bypass reCAPTCHA checks when there is an authenticated user, or automatically receive it as a "human" on score-driven challenges. While in your frontend you can programmatically disable reCAPTCHA when the user is authenticated, on the routes you can specify the guards to bypass using `except()`. +Sometimes you may want to bypass reCAPTCHA checks when there is an authenticated user, or automatically receive it as a "human" on score-driven challenges, specially on recurrent actions or when the user already completes a challenge (like on logins). + +To exclude authenticated user you can use `forGuests()`, and specify the guards if necessary. ```php use App\Http\Controllers\CommentController; @@ -187,11 +189,29 @@ use App\Http\Controllers\MessageController; use DarkGhostHunter\Captchavel\ReCaptcha; use Illuminate\Support\Facades\Route +// Don't challenge users authenticated on the default (web) guard. Route::post('message/send', [MessageController::class, 'send']) - ->middleware(ReCaptcha::invisible()->except('user')); + ->middleware(ReCaptcha::invisible()->forGuests()); +// Don't challenge users authenticated on the "admin" and "moderator" guards. Route::post('comment/store', [CommentController::class, 'store']) - ->middleware(ReCaptcha::score(0.7)->action('comment.store')->except('admin', 'moderator')); + ->middleware(ReCaptcha::score(0.7)->action('comment.store')->forGuests('admin', 'moderator')); +``` + +Then, in your blade files, you can easily skip the challenge with the `@guest` or `@auth` directives. + +```blade +
+ + + @auth + + @else + + @endauth +
``` #### Faking reCAPTCHA scores @@ -209,10 +229,14 @@ From there, you can fake a robot response by filling the `is_robot` input in you ```blade
+ @env('local', 'testing') @endenv - + +
``` @@ -224,11 +248,12 @@ You can use the `captchavel()` helper to output the site key depending on the ch ```blade
- - - - -
+ + + +
``` diff --git a/src/Http/Middleware/VerificationHelpers.php b/src/Http/Middleware/VerificationHelpers.php index 2210509..b8040db 100644 --- a/src/Http/Middleware/VerificationHelpers.php +++ b/src/Http/Middleware/VerificationHelpers.php @@ -23,6 +23,10 @@ protected function isGuest(array $guards): bool { $auth = auth(); + if ($guards === ['null']) { + $guards = [null]; + } + foreach ($guards as $guard) { if ($auth->guard($guard)->check()) { return false; diff --git a/src/ReCaptcha.php b/src/ReCaptcha.php index 116bb25..72dbe2c 100644 --- a/src/ReCaptcha.php +++ b/src/ReCaptcha.php @@ -101,7 +101,18 @@ public function input(string $name): static */ public function except(string ...$guards): static { - $this->guards = $guards; + return $this->forGuests(...$guards); + } + + /** + * Show the challenge on non-authenticated users. + * + * @param string ...$guards + * @return $this + */ + public function forGuests(string ...$guards): static + { + $this->guards = $guards ?: ['null']; return $this; } @@ -209,8 +220,10 @@ public function __toString(): string { $declaration = $this->getBaseParameters() ->reverse() - ->skipUntil(static function (string $parameter): bool { - return $parameter !== 'null'; + ->unless($this->guards, static function (Collection $parameters): Collection { + return $parameters->skipUntil(static function (string $parameter): bool { + return $parameter !== 'null'; + }); }) ->reverse() ->implode(','); diff --git a/tests/Http/Middleware/ChallengeMiddlewareTest.php b/tests/Http/Middleware/ChallengeMiddlewareTest.php index bf36a56..d08a3bb 100644 --- a/tests/Http/Middleware/ChallengeMiddlewareTest.php +++ b/tests/Http/Middleware/ChallengeMiddlewareTest.php @@ -4,6 +4,7 @@ use DarkGhostHunter\Captchavel\Captchavel; use DarkGhostHunter\Captchavel\Http\ReCaptchaResponse; +use Illuminate\Auth\GenericUser; use LogicException; use Orchestra\Testbench\TestCase; use Tests\CreatesFulfilledResponse; @@ -69,6 +70,114 @@ public function test_bypass_if_not_enabled(): void $this->post('v2/android')->assertOk(); } + public function test_bypasses_if_authenticated_on_default_guard(): void + { + $mock = $this->mock(Captchavel::class); + + $mock->expects('isDisabled')->times(3)->andReturnFalse(); + $mock->expects('shouldFake')->times(3)->andReturnFalse(); + $mock->allows('getChallenge')->never(); + + $this->actingAs(new GenericUser([])); + + $this->app['router']->post('checkbox/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:checkbox,null,null,null'); + $this->app['router']->post('invisible/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:invisible,null,null,null'); + $this->app['router']->post('android/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:android,null,null,null'); + + $this->post('/checkbox/auth')->assertOk(); + $this->post('/invisible/auth')->assertOk(); + $this->post('/android/auth')->assertOk(); + } + + public function test_bypasses_if_authenticated_on_one_of_given_guard(): void + { + config()->set('auth.guards.api', [ + 'driver' => 'session', + 'provider' => 'users', + ]); + + $mock = $this->mock(Captchavel::class); + + $mock->expects('isDisabled')->times(3)->andReturnFalse(); + $mock->expects('shouldFake')->times(3)->andReturnFalse(); + $mock->allows('getChallenge')->never(); + + $this->actingAs(new GenericUser([]), 'api'); + + $this->app['router']->post('checkbox/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:checkbox,null,null,web,api'); + $this->app['router']->post('invisible/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:invisible,null,null,web,api'); + $this->app['router']->post('android/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:android,null,null,web,api'); + + $this->post('/checkbox/auth')->assertOk(); + $this->post('/invisible/auth')->assertOk(); + $this->post('/android/auth')->assertOk(); + } + + public function test_error_if_guest(): void + { + $mock = $this->mock(Captchavel::class); + + $mock->expects('isDisabled')->times(3)->andReturnFalse(); + $mock->expects('shouldFake')->times(3)->andReturnFalse(); + $mock->allows('getChallenge')->never(); + + $this->app['router']->post('checkbox/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:checkbox,null,null,null'); + $this->app['router']->post('invisible/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:invisible,null,null,null'); + $this->app['router']->post('android/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:android,null,null,null'); + + $this->post('/checkbox/auth') + ->assertSessionHasErrors(Captchavel::INPUT, trans('captchavel::validation.missing')) + ->assertRedirect('/'); + $this->post('/invisible/auth') + ->assertSessionHasErrors(Captchavel::INPUT, trans('captchavel::validation.missing')) + ->assertRedirect('/'); + $this->post('/android/auth') + ->assertSessionHasErrors(Captchavel::INPUT, trans('captchavel::validation.missing')) + ->assertRedirect('/'); + } + + public function test_error_if_guest_on_given_guard(): void + { + config()->set('auth.guards.api', [ + 'driver' => 'session', + 'provider' => 'users', + ]); + + $mock = $this->mock(Captchavel::class); + + $mock->expects('isDisabled')->times(3)->andReturnFalse(); + $mock->expects('shouldFake')->times(3)->andReturnFalse(); + $mock->allows('getChallenge')->never(); + + $this->actingAs(new GenericUser([])); + + $this->app['router']->post('checkbox/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:checkbox,null,null,api'); + $this->app['router']->post('invisible/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:invisible,null,null,api'); + $this->app['router']->post('android/auth', [__CLASS__, 'returnResponseIfExists']) + ->middleware('recaptcha:android,null,null,api'); + + $this->post('/checkbox/auth') + ->assertSessionHasErrors(Captchavel::INPUT, trans('captchavel::validation.missing')) + ->assertRedirect('/'); + $this->post('/invisible/auth') + ->assertSessionHasErrors(Captchavel::INPUT, trans('captchavel::validation.missing')) + ->assertRedirect('/'); + $this->post('/android/auth') + ->assertSessionHasErrors(Captchavel::INPUT, trans('captchavel::validation.missing')) + ->assertRedirect('/'); + } + public function test_success_when_enabled_and_fake(): void { config(['captchavel.enable' => true]); diff --git a/tests/Http/Middleware/ScoreMiddlewareTest.php b/tests/Http/Middleware/ScoreMiddlewareTest.php index 0152d85..4e68124 100644 --- a/tests/Http/Middleware/ScoreMiddlewareTest.php +++ b/tests/Http/Middleware/ScoreMiddlewareTest.php @@ -7,6 +7,7 @@ use DarkGhostHunter\Captchavel\CaptchavelFake; use DarkGhostHunter\Captchavel\Http\ReCaptchaResponse; use DarkGhostHunter\Captchavel\ReCaptcha; +use Illuminate\Auth\GenericUser; use Illuminate\Foundation\Auth\User; use Illuminate\Http\Client\Factory; use Illuminate\Http\Request; @@ -169,6 +170,62 @@ public function test_uses_custom_input(): void ->assertExactJson(['success' => true, 'score' => 0.7, 'foo' => 'bar']); } + public function test_fakes_human_score_if_authenticated_in_default_guard(): void + { + $mock = $this->mock(Captchavel::class); + + $mock->allows('getChallenge')->never(); + + $this->actingAs(new GenericUser([])); + + $this->app['router']->post('score/auth', [__CLASS__, 'returnSameResponse']) + ->middleware('recaptcha.score:0.5,null,null,null'); + + $this->post('/score/auth')->assertOk(); + } + + public function test_fakes_human_score_if_authenticated_in_one_of_given_guards(): void + { + config()->set('auth.guards.api', [ + 'driver' => 'session', + 'provider' => 'users', + ]); + + $mock = $this->mock(Captchavel::class); + + $mock->allows('getChallenge')->never(); + + $this->actingAs(new GenericUser([]), 'api'); + + $this->app['router']->post('score/auth', [__CLASS__, 'returnSameResponse']) + ->middleware('recaptcha.score:0.5,null,null,web,api'); + + $this->post('/score/auth')->assertOk(); + } + + public function test_error_if_is_guest(): void + { + config()->set('auth.guards.api', [ + 'driver' => 'session', + 'provider' => 'users', + ]); + + $mock = $this->mock(Captchavel::class); + + $mock->expects('isDisabled')->once()->andReturnFalse(); + $mock->expects('shouldFake')->once()->andReturnFalse(); + $mock->allows('getChallenge')->never(); + + $this->actingAs(new GenericUser([])); + + $this->app['router']->post('score/auth', [__CLASS__, 'returnSameResponse']) + ->middleware('recaptcha.score:0.5,null,null,api'); + + $this->post('/score/auth') + ->assertSessionHasErrors(Captchavel::INPUT, trans('captchavel::validation.missing')) + ->assertRedirect('/'); + } + public function test_exception_when_token_absent(): void { $this->post('v3/default', ['foo' => 'bar']) diff --git a/tests/ReCaptchaMiddlewareHelperTest.php b/tests/ReCaptchaMiddlewareHelperTest.php index c753a44..026c0b4 100644 --- a/tests/ReCaptchaMiddlewareHelperTest.php +++ b/tests/ReCaptchaMiddlewareHelperTest.php @@ -144,4 +144,12 @@ public function test_cast_to_string(): void static::assertEquals('recaptcha.score:0.7', ReCaptcha::score(0.7)->toString()); static::assertEquals('recaptcha.score:0.7', ReCaptcha::score(0.7)->__toString()); } + + public function tests_uses_all_guards_as_exception(): void + { + static::assertEquals('recaptcha:checkbox,null,null,null', ReCaptcha::checkbox()->forGuests()->toString()); + static::assertEquals('recaptcha:invisible,null,null,null', ReCaptcha::invisible()->forGuests()->toString()); + static::assertEquals('recaptcha:android,null,null,null', ReCaptcha::android()->forGuests()->toString()); + static::assertEquals('recaptcha.score:0.5,null,null,null', ReCaptcha::score()->forGuests()->toString()); + } }