Skip to content

Commit 4abdee0

Browse files
committed
Better 2FA implementation on logins
1 parent a1a81ac commit 4abdee0

File tree

5 files changed

+105
-24
lines changed

5 files changed

+105
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
1818
### Changed
1919
* Admin API and base routes for user management now define the fields that should be passed to repositories rather than passing all fields.
2020
* User model now defines mass assignment fields using `$fillable` rather than `$guarded`.
21-
22-
### Deprecated
21+
* 2FA checkpoint on login is now its own page, and not an AJAX based call. Improves security on that front.
2322

2423
## v0.5.6 (Bodacious Boreopterus)
2524
### Added

app/Http/Controllers/Auth/LoginController.php

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
use Auth;
2929
use Alert;
30+
use Cache;
3031
use Illuminate\Http\Request;
3132
use Pterodactyl\Models\User;
3233
use PragmaRX\Google2FA\Google2FA;
@@ -110,33 +111,62 @@ public function login(Request $request)
110111
}
111112

112113
// Verify TOTP Token was Valid
113-
if (Auth::user()->use_totp === 1) {
114-
$G2FA = new Google2FA();
115-
if (is_null($request->input('totp_token')) || ! $G2FA->verifyKey(Auth::user()->totp_secret, $request->input('totp_token'))) {
116-
if (! $lockedOut) {
117-
$this->incrementLoginAttempts($request);
118-
}
114+
if (Auth::user()->use_totp) {
115+
$verifyKey = str_random(64);
116+
Cache::put($verifyKey, Auth::user()->id, 5);
119117

120-
Alert::danger(trans('auth.totp_failed'))->flash();
118+
return redirect()->route('auth.totp')->with('authentication_token', $verifyKey);
119+
} else {
120+
Auth::login(Auth::user(), $request->has('remember'));
121121

122-
return $this->sendFailedLoginResponse($request);
123-
}
122+
return $this->sendLoginResponse($request);
124123
}
124+
}
125+
126+
public function totp(Request $request)
127+
{
128+
$verifyKey = $request->session()->get('authentication_token');
125129

126-
// Successfully Authenticated.
127-
Auth::login(Auth::user(), $request->has('remember'));
130+
if (is_null($verifyKey) || Auth::user()) {
131+
return redirect()->route('auth.login');
132+
}
128133

129-
return $this->sendLoginResponse($request);
134+
return view('auth.totp', [
135+
'verify_key' => $verifyKey,
136+
'remember' => $request->has('remember'),
137+
]);
130138
}
131139

132-
/**
133-
* Check if the provided user has TOTP enabled.
134-
*
135-
* @param \Illuminate\Http\Request $request
136-
* @return \Illuminate\Http\Response
137-
*/
138-
public function checkTotp(Request $request)
140+
public function totpCheckpoint(Request $request)
139141
{
140-
return response()->json(User::select('id')->where('email', $request->input('email'))->where('use_totp', 1)->first());
142+
$G2FA = new Google2FA();
143+
144+
if (is_null($request->input('verify_token'))) {
145+
$this->incrementLoginAttempts($request);
146+
Alert::danger(trans('auth.totp_failed'))->flash();
147+
148+
return redirect()->route('auth.login');
149+
}
150+
151+
$user = User::where('id', Cache::pull($request->input('verify_token')))->first();
152+
if (! $user) {
153+
$this->incrementLoginAttempts($request);
154+
Alert::danger(trans('auth.totp_failed'))->flash();
155+
156+
return redirect()->route('auth.login');
157+
}
158+
159+
160+
if (! is_null($request->input('2fa_token')) && $G2FA->verifyKey($user->totp_secret, $request->input('2fa_token'))) {
161+
Auth::login($user, $request->has('remember'));
162+
163+
return redirect()->intended($this->redirectPath());
164+
} else {
165+
$this->incrementLoginAttempts($request);
166+
Alert::danger(trans('auth.2fa_failed'))->flash();
167+
168+
return redirect()->route('auth.login');
169+
}
141170
}
171+
142172
}

app/Http/Routes/AuthRoutes.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,13 @@ public function map(Router $router)
5151
'uses' => 'Auth\LoginController@login',
5252
]);
5353

54-
// Determine if we need to ask for a TOTP Token
54+
$router->get('login/totp', [
55+
'as' => 'auth.totp',
56+
'uses' => 'Auth\LoginController@totp',
57+
]);
58+
5559
$router->post('login/totp', [
56-
'uses' => 'Auth\LoginController@checkTotp',
60+
'uses' => 'Auth\LoginController@totpCheckpoint',
5761
]);
5862

5963
// Show Password Reset Form

resources/lang/en/auth.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@
1515
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
1616
'password_requirements' => 'Passwords must contain at least one uppercase, lowecase, and numeric character and must be at least 8 characters in length.',
1717
'request_reset' => 'Locate Account',
18+
'2fa_required' => '2-Factor Authentication',
19+
'2fa_failed' => 'The 2FA token provided was invalid.',
1820
];
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
2+
3+
{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}}
4+
{{-- of this software and associated documentation files (the "Software"), to deal --}}
5+
{{-- in the Software without restriction, including without limitation the rights --}}
6+
{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}}
7+
{{-- copies of the Software, and to permit persons to whom the Software is --}}
8+
{{-- furnished to do so, subject to the following conditions: --}}
9+
10+
{{-- The above copyright notice and this permission notice shall be included in all --}}
11+
{{-- copies or substantial portions of the Software. --}}
12+
13+
{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}}
14+
{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}}
15+
{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}}
16+
{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}}
17+
{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}}
18+
{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}}
19+
{{-- SOFTWARE. --}}
20+
@extends('layouts.auth')
21+
22+
@section('title')
23+
2FA Checkpoint
24+
@endsection
25+
26+
@section('content')
27+
<div class="login-box-body">
28+
<p class="login-box-msg">@lang('auth.2fa_required')</p>
29+
<form action="{{ route('auth.totp') }}" method="POST">
30+
<div class="form-group">
31+
<input type="text" name="2fa_token" class="form-control" placeholder="@lang('strings.2fa_token')">
32+
<span class="fa fa-lock form-control-feedback"></span>
33+
</div>
34+
<div class="row">
35+
<div class="col-xs-12">
36+
{!! csrf_field() !!}
37+
<input type="hidden" name="verify_token" value="{{ $verify_key }}" />
38+
@if($remember)
39+
<input type="hidden" name="remember" value="true" />
40+
@endif
41+
<button type="submit" class="btn btn-primary btn-block btn-flat">@lang('strings.submit')</button>
42+
</div>
43+
</div>
44+
</form>
45+
</div>
46+
@endsection

0 commit comments

Comments
 (0)