Skip to content

Commit 142cbb0

Browse files
committed
Add invisible ReCAPTCHA to login and password reset
1 parent f2f834a commit 142cbb0

File tree

8 files changed

+184
-4
lines changed

8 files changed

+184
-4
lines changed

app/Events/Auth/FailedCaptcha.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
/**
3+
* Pterodactyl - Panel
4+
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
namespace Pterodactyl\Events\Auth;
26+
27+
use Illuminate\Queue\SerializesModels;
28+
29+
class FailedCaptcha
30+
{
31+
use SerializesModels;
32+
33+
/**
34+
* The IP that the request originated from.
35+
*
36+
* @var string
37+
*/
38+
public $ip;
39+
40+
/**
41+
* The domain that was used to try to verify the request with recaptcha api.
42+
*
43+
* @var string
44+
*/
45+
public $domain;
46+
47+
/**
48+
* Create a new event instance.
49+
*
50+
* @param string $ip
51+
* @param string $domain
52+
* @return void
53+
*/
54+
public function __construct($ip, $domain)
55+
{
56+
$this->ip = $ip;
57+
$this->domain = $domain;
58+
}
59+
}

app/Http/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,6 @@ class Kernel extends HttpKernel
5858
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
5959
'can' => \Illuminate\Auth\Middleware\Authorize::class,
6060
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
61+
'recaptcha' => \Pterodactyl\Http\Middleware\VerifyReCaptcha::class,
6162
];
6263
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Middleware;
4+
5+
use Closure;
6+
use Alert;
7+
use \Pterodactyl\Events\Auth\FailedCaptcha;
8+
9+
class VerifyReCaptcha
10+
{
11+
/**
12+
* Handle an incoming request.
13+
*
14+
* @param \Illuminate\Http\Request $request
15+
* @param \Closure $next
16+
* @return mixed
17+
*/
18+
public function handle($request, Closure $next)
19+
{
20+
if (!config('recaptcha.enabled')) return next($request);
21+
22+
$response_domain = null;
23+
24+
if ($request->has('g-recaptcha-response')) {
25+
$response = $request->get('g-recaptcha-response');
26+
27+
$client = new \GuzzleHttp\Client();
28+
$res = $client->post('https://www.google.com/recaptcha/api/siteverify', [
29+
'form_params' => [
30+
'secret' => config('recaptcha.secret_key'),
31+
'response' => $response,
32+
],
33+
]);
34+
35+
if ($res->getStatusCode() === 200) {
36+
$result = json_decode($res->getBody());
37+
38+
$response_domain = $result->hostname;
39+
40+
// Compare the domain received by google with the app url
41+
$domain_verified = false;
42+
if (config('recaptcha.verify_domain')) {
43+
$matches;
44+
preg_match('/^(?:https?:\/\/)?((?:www\.)?[^:\/\n]+)/', config('app.url'), $matches);
45+
$domain = $matches[1];
46+
$domain_verified = $response_domain === $domain;
47+
}
48+
49+
if ($result->success && (!config('recaptcha.verify_domain') || $domain_verified)) {
50+
return $next($request);
51+
}
52+
}
53+
}
54+
55+
// Emit an event and return to the previous view with an error (only the captcha error will be shown!)
56+
event(new FailedCaptcha($request->ip(), $response_domain));
57+
return back()->withErrors(['g-recaptcha-response' => trans('strings.captcha_invalid')])->withInput();
58+
}
59+
}

app/Http/Routes/AuthRoutes.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public function map(Router $router)
5555
// Handle Login
5656
$router->post('login', [
5757
'uses' => 'Auth\LoginController@login',
58+
'middleware' => 'recaptcha',
5859
]);
5960

6061
$router->get('login/totp', [
@@ -75,6 +76,7 @@ public function map(Router $router)
7576
// Handle Password Reset
7677
$router->post('password', [
7778
'uses' => 'Auth\ForgotPasswordController@sendResetLinkEmail',
79+
'middleware' => 'recaptcha',
7880
]);
7981

8082
// Show Verification Checkpoint
@@ -87,6 +89,7 @@ public function map(Router $router)
8789
$router->post('password/reset', [
8890
'as' => 'auth.reset.post',
8991
'uses' => 'Auth\ResetPasswordController@reset',
92+
'middleware' => 'recaptcha',
9093
]);
9194
});
9295

config/recaptcha.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
return [
4+
5+
/**
6+
* Enable or disable captchas
7+
*/
8+
'enabled' => env('RECAPTCHA_ENABLED', true),
9+
10+
/**
11+
* Use a custom secret key, we use our public one by default
12+
*/
13+
'secret_key' => env('RECAPTCHA_SECRET_KEY', '6LekAxoUAAAAAPW-PxNWaCLH76WkClMLSa2jImwD'),
14+
15+
/**
16+
* Use a custom website key, we use our public one by default
17+
*/
18+
'website_key' => env('RECAPTCHA_WEBSITE_KEY' ,'6LekAxoUAAAAADjWZJ4ufcDRZBBiH9vfHawqRbup'),
19+
20+
/**
21+
* Domain verification is enabled by default and compares the domain used when solving the captcha
22+
* as public keys can't have domain verification on google's side enabled (obviously).
23+
*/
24+
'verify_domain' => true,
25+
26+
];

resources/lang/en/strings.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,5 @@
6969
'owner' => 'Owner',
7070
'admin' => 'Admin',
7171
'subuser' => 'Subuser',
72+
'captcha_invalid' => 'The provided captcha is invalid.',
7273
];

resources/themes/pterodactyl/auth/login.blade.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
@endforeach
4646
@endforeach
4747
<p class="login-box-msg">@lang('auth.authentication_required')</p>
48-
<form action="{{ route('auth.login') }}" method="POST">
48+
<form id="loginForm" action="{{ route('auth.login') }}" method="POST">
4949
<div class="form-group has-feedback">
5050
<input name="user" class="form-control" value="{{ old('user') }}" placeholder="@lang('strings.user_identifier')">
5151
<span class="fa fa-envelope form-control-feedback"></span>
@@ -62,10 +62,20 @@
6262
</div>
6363
<div class="col-xs-4">
6464
{!! csrf_field() !!}
65-
<button type="submit" class="btn btn-primary btn-block btn-flat">@lang('auth.sign_in')</button>
65+
<button type="submit" class="btn btn-primary btn-block btn-flat g-recaptcha" data-sitekey="{{ config('recaptcha.website_key') }}" data-callback='onSubmit'>@lang('auth.sign_in')</button>
6666
</div>
6767
</div>
6868
</form>
6969
<a href="{{ route('auth.password') }}">@lang('auth.forgot_password')</a><br>
7070
</div>
7171
@endsection
72+
73+
@section('scripts')
74+
@parent
75+
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
76+
<script>
77+
function onSubmit(token) {
78+
document.getElementById("loginForm").submit();
79+
}
80+
</script>
81+
@endsection

resources/themes/pterodactyl/auth/passwords/email.blade.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,24 @@
2525

2626
@section('content')
2727
<div class="login-box-body">
28+
@if (count($errors) > 0)
29+
<div class="callout callout-danger">
30+
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
31+
@lang('auth.auth_error')<br><br>
32+
<ul>
33+
@foreach ($errors->all() as $error)
34+
<li>{{ $error }}</li>
35+
@endforeach
36+
</ul>
37+
</div>
38+
@endif
2839
@if (session('status'))
2940
<div class="callout callout-success">
3041
@lang('auth.email_sent')
3142
</div>
3243
@endif
3344
<p class="login-box-msg">@lang('auth.request_reset_text')</p>
34-
<form action="{{ route('auth.password') }}" method="POST">
45+
<form id="resetForm" action="{{ route('auth.password') }}" method="POST">
3546
<div class="form-group has-feedback">
3647
<input type="email" name="email" class="form-control" value="{{ old('email') }}" autofocus placeholder="@lang('strings.email')">
3748
<span class="fa fa-envelope form-control-feedback"></span>
@@ -47,9 +58,19 @@
4758
</div>
4859
<div class="col-xs-8">
4960
{!! csrf_field() !!}
50-
<button type="submit" class="btn btn-primary btn-block btn-flat">@lang('auth.request_reset')</button>
61+
<button type="submit" class="btn btn-primary btn-block btn-flat g-recaptcha" data-sitekey="{{ config('recaptcha.website_key') }}" data-callback='onSubmit'>@lang('auth.request_reset')</button>
5162
</div>
5263
</div>
5364
</form>
5465
</div>
5566
@endsection
67+
68+
@section('scripts')
69+
@parent
70+
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
71+
<script>
72+
function onSubmit(token) {
73+
document.getElementById("resetForm").submit();
74+
}
75+
</script>
76+
@endsection

0 commit comments

Comments
 (0)