diff --git a/app/Events/Auth/FailedCaptcha.php b/app/Events/Auth/FailedCaptcha.php new file mode 100644 index 000000000..ac1786ce4 --- /dev/null +++ b/app/Events/Auth/FailedCaptcha.php @@ -0,0 +1,59 @@ +. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +namespace Pterodactyl\Events\Auth; + +use Illuminate\Queue\SerializesModels; + +class FailedCaptcha +{ + use SerializesModels; + + /** + * The IP that the request originated from. + * + * @var string + */ + public $ip; + + /** + * The domain that was used to try to verify the request with recaptcha api. + * + * @var string + */ + public $domain; + + /** + * Create a new event instance. + * + * @param string $ip + * @param string $domain + * @return void + */ + public function __construct($ip, $domain) + { + $this->ip = $ip; + $this->domain = $domain; + } +} \ No newline at end of file diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 971835dac..93e532258 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -58,5 +58,6 @@ class Kernel extends HttpKernel 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, + 'recaptcha' => \Pterodactyl\Http\Middleware\VerifyReCaptcha::class, ]; } diff --git a/app/Http/Middleware/VerifyReCaptcha.php b/app/Http/Middleware/VerifyReCaptcha.php new file mode 100644 index 000000000..b5bd8cab0 --- /dev/null +++ b/app/Http/Middleware/VerifyReCaptcha.php @@ -0,0 +1,59 @@ +has('g-recaptcha-response')) { + $response = $request->get('g-recaptcha-response'); + + $client = new \GuzzleHttp\Client(); + $res = $client->post('https://www.google.com/recaptcha/api/siteverify', [ + 'form_params' => [ + 'secret' => config('recaptcha.secret_key'), + 'response' => $response, + ], + ]); + + if ($res->getStatusCode() === 200) { + $result = json_decode($res->getBody()); + + $response_domain = $result->hostname; + + // Compare the domain received by google with the app url + $domain_verified = false; + if (config('recaptcha.verify_domain')) { + $matches; + preg_match('/^(?:https?:\/\/)?((?:www\.)?[^:\/\n]+)/', config('app.url'), $matches); + $domain = $matches[1]; + $domain_verified = $response_domain === $domain; + } + + if ($result->success && (!config('recaptcha.verify_domain') || $domain_verified)) { + return $next($request); + } + } + } + + // Emit an event and return to the previous view with an error (only the captcha error will be shown!) + event(new FailedCaptcha($request->ip(), $response_domain)); + return back()->withErrors(['g-recaptcha-response' => trans('strings.captcha_invalid')])->withInput(); + } +} diff --git a/app/Http/Routes/AuthRoutes.php b/app/Http/Routes/AuthRoutes.php index b138fe9d0..499f6abd7 100644 --- a/app/Http/Routes/AuthRoutes.php +++ b/app/Http/Routes/AuthRoutes.php @@ -55,6 +55,7 @@ class AuthRoutes // Handle Login $router->post('login', [ 'uses' => 'Auth\LoginController@login', + 'middleware' => 'recaptcha', ]); $router->get('login/totp', [ @@ -75,6 +76,7 @@ class AuthRoutes // Handle Password Reset $router->post('password', [ 'uses' => 'Auth\ForgotPasswordController@sendResetLinkEmail', + 'middleware' => 'recaptcha', ]); // Show Verification Checkpoint @@ -87,6 +89,7 @@ class AuthRoutes $router->post('password/reset', [ 'as' => 'auth.reset.post', 'uses' => 'Auth\ResetPasswordController@reset', + 'middleware' => 'recaptcha', ]); }); diff --git a/config/recaptcha.php b/config/recaptcha.php new file mode 100644 index 000000000..6e9737493 --- /dev/null +++ b/config/recaptcha.php @@ -0,0 +1,26 @@ + env('RECAPTCHA_ENABLED', true), + + /** + * Use a custom secret key, we use our public one by default + */ + 'secret_key' => env('RECAPTCHA_SECRET_KEY', '6LekAxoUAAAAAPW-PxNWaCLH76WkClMLSa2jImwD'), + + /** + * Use a custom website key, we use our public one by default + */ + 'website_key' => env('RECAPTCHA_WEBSITE_KEY' ,'6LekAxoUAAAAADjWZJ4ufcDRZBBiH9vfHawqRbup'), + + /** + * Domain verification is enabled by default and compares the domain used when solving the captcha + * as public keys can't have domain verification on google's side enabled (obviously). + */ + 'verify_domain' => true, + +]; \ No newline at end of file diff --git a/resources/lang/en/strings.php b/resources/lang/en/strings.php index 2ceb4d2cf..5cfaf1020 100644 --- a/resources/lang/en/strings.php +++ b/resources/lang/en/strings.php @@ -69,4 +69,5 @@ return [ 'owner' => 'Owner', 'admin' => 'Admin', 'subuser' => 'Subuser', + 'captcha_invalid' => 'The provided captcha is invalid.', ]; diff --git a/resources/themes/pterodactyl/auth/login.blade.php b/resources/themes/pterodactyl/auth/login.blade.php index 014e848bb..6cd087af7 100644 --- a/resources/themes/pterodactyl/auth/login.blade.php +++ b/resources/themes/pterodactyl/auth/login.blade.php @@ -45,7 +45,7 @@ @endforeach @endforeach

@lang('auth.authentication_required')

-
+
@@ -62,10 +62,20 @@
{!! csrf_field() !!} - +
@lang('auth.forgot_password')
@endsection + +@section('scripts') + @parent + + +@endsection \ No newline at end of file diff --git a/resources/themes/pterodactyl/auth/passwords/email.blade.php b/resources/themes/pterodactyl/auth/passwords/email.blade.php index 317c6d842..080db3c84 100644 --- a/resources/themes/pterodactyl/auth/passwords/email.blade.php +++ b/resources/themes/pterodactyl/auth/passwords/email.blade.php @@ -25,13 +25,24 @@ @section('content')
+ @if (count($errors) > 0) +
+ + @lang('auth.auth_error')

+ +
+ @endif @if (session('status'))
@lang('auth.email_sent')
@endif

@lang('auth.request_reset_text')

-
+
@@ -47,9 +58,19 @@
{!! csrf_field() !!} - +
@endsection + +@section('scripts') + @parent + + +@endsection \ No newline at end of file