Improve turnstile error handling (+ cleanup) (#1501)

This commit is contained in:
Boy132 2025-07-09 13:51:43 +02:00 committed by GitHub
parent 5e8cccef19
commit 5a7c6ac6e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 28 additions and 64 deletions

View File

@ -56,9 +56,4 @@ abstract class BaseSchema
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
];
}
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
{
return true;
}
}

View File

@ -26,10 +26,5 @@ interface CaptchaSchemaInterface
public function getIcon(): ?string;
/**
* @return array<string, string|bool>
*/
public function validateResponse(?string $captchaResponse = null): array;
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool;
public function validateResponse(?string $captchaResponse = null): void;
}

View File

@ -4,6 +4,7 @@ namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Extensions\Captcha\CaptchaService;
use Closure;
use Exception;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\App;
@ -11,10 +12,12 @@ class Rule implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$response = App::call(fn (CaptchaService $service) => $service->getActiveSchema()->validateResponse($value));
try {
App::call(fn (CaptchaService $service) => $service->get('turnstile')->validateResponse($value));
} catch (Exception $exception) {
report($exception);
if (!$response['success']) {
$fail($response['message'] ?? 'Unknown error occurred, please try again');
$fail('Captcha validation failed: ' . $exception->getMessage());
}
}
}

View File

@ -66,9 +66,9 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
}
/**
* @return array<string, string|bool>
* @throws Exception
*/
public function validateResponse(?string $captchaResponse = null): array
public function validateResponse(?string $captchaResponse = null): void
{
$captchaResponse ??= request()->get('cf-turnstile-response');
@ -83,22 +83,33 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => $secret,
'response' => $captchaResponse,
]);
])
->json();
return count($response->json()) ? $response->json() : [
'success' => false,
'message' => 'Unknown error occurred, please try again',
];
if (!$response['success']) {
match ($response['error-codes'][0] ?? null) {
'missing-input-secret' => throw new Exception('The secret parameter was not passed.'),
'invalid-input-secret' => throw new Exception('The secret parameter was invalid, did not exist, or is a testing secret key with a non-testing response.'),
'missing-input-response' => throw new Exception('The response parameter (token) was not passed.'),
'invalid-input-response' => throw new Exception('The response parameter (token) is invalid or has expired.'),
'bad-request' => throw new Exception('The request was rejected because it was malformed.'),
'timeout-or-duplicate' => throw new Exception('The response parameter (token) has already been validated before.'),
default => throw new Exception('An internal error happened while validating the response.'),
};
}
if (!$this->verifyDomain($response['hostname'] ?? '')) {
throw new Exception('Domain verification failed.');
}
}
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
private function verifyDomain(string $hostname): bool
{
if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) {
return true;
}
$requestUrl ??= request()->url;
$requestUrl = parse_url($requestUrl);
$requestUrl = parse_url(request()->url());
return $hostname === array_get($requestUrl, 'host');
}

View File

@ -1,39 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Extensions\Captcha\CaptchaService;
use Closure;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Events\Auth\FailedCaptcha;
use Symfony\Component\HttpKernel\Exception\HttpException;
readonly class VerifyCaptcha
{
public function __construct(private Application $app) {}
public function handle(Request $request, Closure $next, CaptchaService $captchaService): mixed
{
if ($this->app->isLocal()) {
return $next($request);
}
$schemas = $captchaService->getActiveSchemas();
foreach ($schemas as $schema) {
$response = $schema->validateResponse();
if ($response['success'] && $schema->verifyDomain($response['hostname'] ?? '', $request->url())) {
return $next($request);
}
event(new FailedCaptcha($request->ip(), $response['message'] ?? null));
throw new HttpException(Response::HTTP_BAD_REQUEST, "Failed to validate {$schema->getId()} captcha data.");
}
// No captcha enabled
return $next($request);
}
}

View File

@ -44,7 +44,6 @@ return Application::configure(basePath: dirname(__DIR__))
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'node.maintenance' => \App\Http\Middleware\MaintenanceMiddleware::class,
'captcha' => \App\Http\Middleware\VerifyCaptcha::class,
]);
})
->withSingletons([