Skip to content

Commit

Permalink
Replace reCAPTCHA with Turnstile (#589)
Browse files Browse the repository at this point in the history
* add laravel turnstile

* add config & settings for turnstile

* publish view to center captcha

* completely replace reCAPTCHA

* update FailedCaptcha event

* add back config for domain verification

* don't set language so browser lang is used
  • Loading branch information
Boy132 authored Nov 1, 2024
1 parent cf57c28 commit 9d02aeb
Show file tree
Hide file tree
Showing 17 changed files with 372 additions and 136 deletions.
2 changes: 1 addition & 1 deletion app/Events/Auth/FailedCaptcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class FailedCaptcha extends Event
/**
* Create a new event instance.
*/
public function __construct(public string $ip, public string $domain)
public function __construct(public string $ip, public ?string $message)
{
}
}
36 changes: 36 additions & 0 deletions app/Filament/Pages/Auth/Login.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace App\Filament\Pages\Auth;

use Coderflex\FilamentTurnstile\Forms\Components\Turnstile;
use Filament\Pages\Auth\Login as BaseLogin;

class Login extends BaseLogin
{
protected function getForms(): array
{
return [
'form' => $this->form(
$this->makeForm()
->schema([
$this->getEmailFormComponent(),
$this->getPasswordFormComponent(),
$this->getRememberFormComponent(),
Turnstile::make('captcha')
->hidden(!config('turnstile.turnstile_enabled'))
->validationMessages([
'required' => config('turnstile.error_messages.turnstile_check_message'),
]),
])
->statePath('data'),
),
];
}

protected function throwFailureValidationException(): never
{
$this->dispatch('reset-captcha');

parent::throwFailureValidationException();
}
}
55 changes: 35 additions & 20 deletions app/Filament/Pages/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
Expand All @@ -26,6 +27,7 @@
use Filament\Pages\Page;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Notification as MailNotification;
use Illuminate\Support\HtmlString;

/**
* @property Form $form
Expand Down Expand Up @@ -67,10 +69,11 @@ protected function getFormSchema(): array
->label('General')
->icon('tabler-home')
->schema($this->generalSettings()),
Tab::make('recaptcha')
->label('reCAPTCHA')
Tab::make('captcha')
->label('Captcha')
->icon('tabler-shield')
->schema($this->recaptchaSettings()),
->schema($this->captchaSettings())
->columns(3),
Tab::make('mail')
->label('Mail')
->icon('tabler-mail')
Expand Down Expand Up @@ -180,35 +183,47 @@ private function generalSettings(): array
];
}

private function recaptchaSettings(): array
private function captchaSettings(): array
{
return [
Toggle::make('RECAPTCHA_ENABLED')
->label('Enable reCAPTCHA?')
Toggle::make('TURNSTILE_ENABLED')
->label('Enable Turnstile Captcha?')
->inline(false)
->columnSpan(1)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state))
->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))),
TextInput::make('RECAPTCHA_DOMAIN')
->label('Domain')
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_ENABLED', (bool) $state))
->default(env('TURNSTILE_ENABLED', config('turnstile.turnstile_enabled'))),
Placeholder::make('info')
->columnSpan(2)
->content(new HtmlString('<p>You can generate the keys on your <u><a href="https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key" target="_blank">Cloudflare Dashboard</a></u>. A Cloudflare account is required.</p>')),
TextInput::make('TURNSTILE_SITE_KEY')
->label('Site Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))),
TextInput::make('RECAPTCHA_WEBSITE_KEY')
->label('Website Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))),
TextInput::make('RECAPTCHA_SECRET_KEY')
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->default(env('TURNSTILE_SITE_KEY', config('turnstile.turnstile_site_key')))
->placeholder('1x00000000000000000000AA'),
TextInput::make('TURNSTILE_SECRET_KEY')
->label('Secret Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->default(env('TURNSTILE_SECRET_KEY', config('turnstile.secret_key')))
->placeholder('1x0000000000000000000000000000000AA'),
Toggle::make('TURNSTILE_VERIFY_DOMAIN')
->label('Verify domain?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_VERIFY_DOMAIN', (bool) $state))
->default(env('TURNSTILE_VERIFY_DOMAIN', config('turnstile.turnstile_verify_domain'))),
];
}

Expand Down
34 changes: 12 additions & 22 deletions app/Http/Middleware/VerifyReCaptcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

namespace App\Http\Middleware;

use GuzzleHttp\Client;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Events\Auth\FailedCaptcha;
use Coderflex\LaravelTurnstile\Facades\LaravelTurnstile;
use Symfony\Component\HttpKernel\Exception\HttpException;

readonly class VerifyReCaptcha
Expand All @@ -18,48 +18,38 @@ public function __construct(private Application $app)

public function handle(Request $request, \Closure $next): mixed
{
if (!config('recaptcha.enabled')) {
if (!config('turnstile.turnstile_enabled')) {
return $next($request);
}

if ($this->app->isLocal()) {
return $next($request);
}

if ($request->filled('g-recaptcha-response')) {
$client = new Client();
$res = $client->post(config('recaptcha.domain'), [
'form_params' => [
'secret' => config('recaptcha.secret_key'),
'response' => $request->input('g-recaptcha-response'),
],
]);
if ($request->filled('cf-turnstile-response')) {
$response = LaravelTurnstile::validate($request->get('cf-turnstile-response'));

if ($res->getStatusCode() === 200) {
$result = json_decode($res->getBody());

if ($result->success && (!config('recaptcha.verify_domain') || $this->isResponseVerified($result, $request))) {
return $next($request);
}
if ($response['success'] && $this->isResponseVerified($response['hostname'] ?? '', $request)) {
return $next($request);
}
}

event(new FailedCaptcha($request->ip(), $result->hostname ?? null));
event(new FailedCaptcha($request->ip(), $response['message'] ?? null));

throw new HttpException(Response::HTTP_BAD_REQUEST, 'Failed to validate reCAPTCHA data.');
throw new HttpException(Response::HTTP_BAD_REQUEST, 'Failed to validate turnstile captcha data.');
}

/**
* Determine if the response from the recaptcha servers was valid.
*/
private function isResponseVerified(\stdClass $result, Request $request): bool
private function isResponseVerified(string $hostname, Request $request): bool
{
if (!config('recaptcha.verify_domain')) {
return false;
if (!config('turnstile.turnstile_verify_domain')) {
return true;
}

$url = parse_url($request->url());

return $result->hostname === array_get($url, 'host');
return $hostname === array_get($url, 'host');
}
}
4 changes: 2 additions & 2 deletions app/Http/ViewComposers/AssetComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public function compose(View $view): void
'name' => config('app.name', 'Panel'),
'locale' => config('app.locale') ?? 'en',
'recaptcha' => [
'enabled' => config('recaptcha.enabled', false),
'siteKey' => config('recaptcha.website_key') ?? '',
'enabled' => config('turnstile.turnstile_enabled', false),
'siteKey' => config('turnstile.turnstile_site_key') ?? '',
],
'usesSyncDriver' => config('queue.default') === 'sync',
'serverDescriptionsEditable' => config('panel.editable_server_descriptions'),
Expand Down
3 changes: 2 additions & 1 deletion app/Providers/Filament/AdminPanelProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Providers\Filament;

use App\Filament\Pages\Auth\Login;
use App\Filament\Resources\UserResource\Pages\EditProfile;
use App\Http\Middleware\LanguageMiddleware;
use Filament\Http\Middleware\Authenticate;
Expand Down Expand Up @@ -36,7 +37,7 @@ public function panel(Panel $panel): Panel
->id('admin')
->path('admin')
->topNavigation(config('panel.filament.top-navigation', true))
->login()
->login(Login::class)
->breadcrumbs(false)
->homeUrl('/')
->favicon(config('app.favicon', '/pelican.ico'))
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"abdelhamiderrahmouni/filament-monaco-editor": "0.2.1",
"aws/aws-sdk-php": "~3.288.1",
"chillerlan/php-qrcode": "^5.0.2",
"coderflex/filament-turnstile": "^2.2",
"dedoc/scramble": "^0.10.0",
"doctrine/dbal": "~3.6.0",
"filament/filament": "^3.2",
Expand Down
Loading

0 comments on commit 9d02aeb

Please sign in to comment.