diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b4508658..d87388ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ### Changed * Moved Docker image setting to be on the startup management page for a server rather than the details page. This value changes based on the Nest and Egg that are selected. * Two-Factor authentication tokens are now 32 bytes in length, and are stored encrypted at rest in the database. +* Login page UI has been improved to be more sleek and welcoming to users. +* Changed 2FA login process to be more secure. Previously authentication checking happened on the 2FA post page, now it happens prior and is passed along to the 2FA page to avoid storing any credentials. ## v0.7.0-beta.1 (Derelict Dermodactylus) ### Added diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 11329b891..198da73f0 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -1,15 +1,9 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Http\Controllers\Auth; use Illuminate\Http\Request; +use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Password; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Events\Auth\FailedPasswordReset; @@ -17,27 +11,8 @@ use Illuminate\Foundation\Auth\SendsPasswordResetEmails; class ForgotPasswordController extends Controller { - /* - |-------------------------------------------------------------------------- - | Password Reset Controller - |-------------------------------------------------------------------------- - | - | This controller is responsible for handling password reset emails and - | includes a trait which assists in sending these notifications from - | your application to your users. Feel free to explore this trait. - | - */ - use SendsPasswordResetEmails; - /** - * Create a new controller instance. - */ - public function __construct() - { - $this->middleware('guest'); - } - /** * Get the response for a failed password reset link. * @@ -45,12 +20,12 @@ class ForgotPasswordController extends Controller * @param string $response * @return \Illuminate\Http\RedirectResponse */ - protected function sendResetLinkFailedResponse(Request $request, $response) + protected function sendResetLinkFailedResponse(Request $request, $response): RedirectResponse { // As noted in #358 we will return success even if it failed // to avoid pointing out that an account does or does not // exist on the system. - event(new FailedPasswordReset($request->ip(), $request->only('email'))); + event(new FailedPasswordReset($request->ip(), $request->input('email'))); return $this->sendResetLinkResponse(Password::RESET_LINK_SENT); } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 9fab7b53e..8e67d9f1a 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -1,53 +1,57 @@ - * Some Modifications (c) 2015 Dylan Seidt . - * - * 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\Http\Controllers\Auth; -use Auth; -use Cache; -use Crypt; use Illuminate\Http\Request; -use Pterodactyl\Models\User; +use Illuminate\Auth\AuthManager; use PragmaRX\Google2FA\Google2FA; +use Illuminate\Auth\Events\Failed; +use Illuminate\Http\RedirectResponse; use Pterodactyl\Http\Controllers\Controller; +use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Foundation\Auth\AuthenticatesUsers; +use Illuminate\Contracts\Cache\Repository as CacheRepository; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Illuminate\Contracts\Config\Repository as ConfigRepository; class LoginController extends Controller { - /* - |-------------------------------------------------------------------------- - | Login Controller - |-------------------------------------------------------------------------- - | - | This controller handles authenticating users for the application and - | redirecting them to your home screen. The controller uses a trait - | to conveniently provide its functionality to your applications. - | - */ use AuthenticatesUsers; + const USER_INPUT_FIELD = 'user'; + + /** + * @var \Illuminate\Auth\AuthManager + */ + private $auth; + + /** + * @var \Illuminate\Contracts\Cache\Repository + */ + private $cache; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + private $config; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + private $encrypter; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + private $repository; + + /** + * @var \PragmaRX\Google2FA\Google2FA + */ + private $google2FA; + /** * Where to redirect users after login / registration. * @@ -60,54 +64,54 @@ class LoginController extends Controller * * @var int */ - protected $lockoutTime = 120; + protected $lockoutTime; /** * After how many attempts should logins be throttled and locked. * * @var int */ - protected $maxLoginAttempts = 3; + protected $maxLoginAttempts; /** - * Create a new controller instance. - */ - public function __construct() - { - $this->middleware('guest', ['except' => 'logout']); - } - - /** - * Get the failed login response instance. + * LoginController constructor. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse + * @param \Illuminate\Auth\AuthManager $auth + * @param \Illuminate\Contracts\Cache\Repository $cache + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \PragmaRX\Google2FA\Google2FA $google2FA + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ - protected function sendFailedLoginResponse(Request $request) - { - $this->incrementLoginAttempts($request); + public function __construct( + AuthManager $auth, + CacheRepository $cache, + ConfigRepository $config, + Encrypter $encrypter, + Google2FA $google2FA, + UserRepositoryInterface $repository + ) { + $this->auth = $auth; + $this->cache = $cache; + $this->config = $config; + $this->encrypter = $encrypter; + $this->google2FA = $google2FA; + $this->repository = $repository; - $errors = [$this->username() => trans('auth.failed')]; - - if ($request->expectsJson()) { - return response()->json($errors, 422); - } - - return redirect()->route('auth.login') - ->withInput($request->only($this->username(), 'remember')) - ->withErrors($errors); + $this->lockoutTime = $this->config->get('auth.lockout.time'); + $this->maxLoginAttempts = $this->config->get('auth.lockout.attempts'); } /** * Handle a login request to the application. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response|\Illuminate\Response\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response */ public function login(Request $request) { - // Check wether the user identifier is an email address or a username - $checkField = str_contains($request->input('user'), '@') ? 'email' : 'username'; + $username = $request->input(self::USER_INPUT_FIELD); + $useColumn = $this->getField($username); if ($this->hasTooManyLoginAttempts($request)) { $this->fireLockoutEvent($request); @@ -115,40 +119,27 @@ class LoginController extends Controller return $this->sendLockoutResponse($request); } - // Determine if the user even exists. - $user = User::where($checkField, $request->input($this->username()))->first(); - if (! $user) { + try { + $user = $this->repository->findFirstWhere([[$useColumn, '=', $username]]); + } catch (RecordNotFoundException $exception) { return $this->sendFailedLoginResponse($request); } - // If user uses 2FA, redirect to that page. + $validCredentials = password_verify($request->input('password'), $user->password); if ($user->use_totp) { $token = str_random(64); - Cache::put($token, [ - 'user_id' => $user->id, - 'credentials' => Crypt::encrypt(serialize([ - $checkField => $request->input($this->username()), - 'password' => $request->input('password'), - ])), - ], 5); + $this->cache->put($token, ['user_id' => $user->id, 'valid_credentials' => $validCredentials], 5); - return redirect()->route('auth.totp') - ->with('authentication_token', $token) - ->with('remember', $request->has('remember')); + return redirect()->route('auth.totp')->with('authentication_token', $token); } - $attempt = Auth::attempt([ - $checkField => $request->input($this->username()), - 'password' => $request->input('password'), - 'use_totp' => 0, - ], $request->has('remember')); + if ($validCredentials) { + $this->auth->guard()->login($user, true); - if ($attempt) { return $this->sendLoginResponse($request); } - // Login failed, send response. - return $this->sendFailedLoginResponse($request); + return $this->sendFailedLoginResponse($request, $user); } /** @@ -160,71 +151,96 @@ class LoginController extends Controller public function totp(Request $request) { $token = $request->session()->get('authentication_token'); - - if (is_null($token) || Auth::user()) { + if (is_null($token) || $this->auth->guard()->user()) { return redirect()->route('auth.login'); } - return view('auth.totp', [ - 'verify_key' => $token, - 'remember' => $request->session()->get('remember'), - ]); + return view('auth.totp', ['verify_key' => $token]); } /** - * Handle a TOTP input. + * Handle a login where the user is required to provide a TOTP authentication + * token. In order to add additional layers of security, users are not + * informed of an incorrect password until this stage, forcing them to + * provide a token on each login attempt. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response */ - public function totpCheckpoint(Request $request) + public function loginUsingTotp(Request $request) { - $G2FA = new Google2FA(); - if (is_null($request->input('verify_token'))) { return $this->sendFailedLoginResponse($request); } - $cache = Cache::pull($request->input('verify_token')); - $user = User::where('id', $cache['user_id'])->first(); - - if (! $user || ! $cache) { - $this->sendFailedLoginResponse($request); - } - - if (is_null($request->input('2fa_token'))) { - return $this->sendFailedLoginResponse($request); - } - try { - $credentials = unserialize(Crypt::decrypt($cache['credentials'])); - } catch (\Illuminate\Contracts\Encryption\DecryptException $ex) { + $cache = $this->cache->pull($request->input('verify_token'), []); + $user = $this->repository->find(array_get($cache, 'user_id', 0)); + } catch (RecordNotFoundException $exception) { return $this->sendFailedLoginResponse($request); } - if (! $G2FA->verifyKey(Crypt::decrypt($user->totp_secret), $request->input('2fa_token'), 2)) { - event(new \Illuminate\Auth\Events\Failed($user, $credentials)); - - return $this->sendFailedLoginResponse($request); + if (is_null($request->input('2fa_token')) || ! array_get($cache, 'valid_credentials')) { + return $this->sendFailedLoginResponse($request, $user); } - $attempt = Auth::attempt($credentials, $request->has('remember')); - - if ($attempt) { - return $this->sendLoginResponse($request); + if (! $this->google2FA->verifyKey( + $this->encrypter->decrypt($user->totp_secret), + $request->input('2fa_token'), + $this->config->get('pterodactyl.auth.2fa.window') + )) { + return $this->sendFailedLoginResponse($request, $user); } - // Login failed, send response. - return $this->sendFailedLoginResponse($request); + $this->auth->guard()->login($user, true); + + return $this->sendLoginResponse($request); } /** - * Get the login username to be used by the controller. + * Get the failed login response instance. * + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * @return \Illuminate\Http\RedirectResponse + */ + protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null): RedirectResponse + { + $this->incrementLoginAttempts($request); + $this->fireFailedLoginEvent($user, [ + $this->getField($request->input(self::USER_INPUT_FIELD)) => $request->input(self::USER_INPUT_FIELD), + ]); + + $errors = [self::USER_INPUT_FIELD => trans('auth.failed')]; + + if ($request->expectsJson()) { + return response()->json($errors, 422); + } + + return redirect()->route('auth.login') + ->withInput($request->only(self::USER_INPUT_FIELD)) + ->withErrors($errors); + } + + /** + * Determine if the user is logging in using an email or username,. + * + * @param string $input * @return string */ - public function username() + private function getField(string $input = null): string { - return 'user'; + return str_contains($input, '@') ? 'email' : 'username'; + } + + /** + * Fire a failed login event. + * + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * @param array $credentials + */ + private function fireFailedLoginEvent(Authenticatable $user = null, array $credentials = []) + { + event(new Failed($user, $credentials)); } } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php deleted file mode 100644 index c0621270a..000000000 --- a/app/Http/Controllers/Auth/RegisterController.php +++ /dev/null @@ -1,69 +0,0 @@ -middleware('guest'); - } - - /** - * Get a validator for an incoming registration request. - * - * @param array $data - * @return \Illuminate\Contracts\Validation\Validator - */ - protected function validator(array $data) - { - return Validator::make($data, [ - 'name' => 'required|max:255', - 'email' => 'required|email|max:255|unique:users', - 'password' => 'required|min:6|confirmed', - ]); - } - - /** - * Create a new user instance after a valid registration. - * - * @param array $data - * @return User - */ - protected function create(array $data) - { - return User::create([ - 'name' => $data['name'], - 'email' => $data['email'], - 'password' => bcrypt($data['password']), - ]); - } -} diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index 67d9b8f33..226958416 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -2,23 +2,11 @@ namespace Pterodactyl\Http\Controllers\Auth; -use Pterodactyl\Models\User; use Pterodactyl\Http\Controllers\Controller; use Illuminate\Foundation\Auth\ResetsPasswords; class ResetPasswordController extends Controller { - /* - |-------------------------------------------------------------------------- - | Password Reset Controller - |-------------------------------------------------------------------------- - | - | This controller is responsible for handling password reset requests - | and uses a simple trait to include this behavior. You're free to - | explore this trait and override any methods you wish to tweak. - | - */ - use ResetsPasswords; /** @@ -28,25 +16,17 @@ class ResetPasswordController extends Controller */ public $redirectTo = '/'; - /** - * Create a new controller instance. - */ - public function __construct() - { - $this->middleware('guest'); - } - /** * Return the rules used when validating password reset. * * @return array */ - protected function rules() + protected function rules(): array { return [ 'token' => 'required', 'email' => 'required|email', - 'password' => 'required|confirmed|' . User::PASSWORD_RULES, + 'password' => 'required|confirmed|min:8', ]; } } diff --git a/config/auth.php b/config/auth.php index b83dd9eca..e83406286 100644 --- a/config/auth.php +++ b/config/auth.php @@ -1,6 +1,21 @@ [ + 'time' => 120, + 'attempts' => 3, + ], + /* |-------------------------------------------------------------------------- | Authentication Defaults diff --git a/public/themes/pterodactyl/css/pterodactyl.css b/public/themes/pterodactyl/css/pterodactyl.css index a0b9da1e8..572b3b634 100644 --- a/public/themes/pterodactyl/css/pterodactyl.css +++ b/public/themes/pterodactyl/css/pterodactyl.css @@ -26,42 +26,74 @@ background: #10529f; } +#login-position-elements { + margin: 25% auto; +} + .login-logo { - color: white; - font-weight: bold; + color: #fff; + font-weight: 400; } .login-copyright { - color: white; + color: rgba(255, 255, 255, 0.3); } -.login-copyright a, .login-copyright a:hover { - color: white; - font-weight: bold; +.login-copyright > a { + color: rgba(255, 255, 255, 0.6); } .particles-js-canvas-el { position: absolute; + width: 100%; + height: 100%; + top: 0; + z-index: -1; } -.login-box, .register-box { - position: absolute; - margin: -180px 0 0 -180px; - left: 50%; - top: 50%; - height: 360px; - width: 360px; - z-index: 100; +.pterodactyl-login-box { + background: rgba(0, 0, 0, 0.25); + border-radius: 3px; + padding: 20px; } -@media (max-width:768px) { - .login-box { - width: 90%; - margin-top: 20px; - margin: 5%; - left: 0; - top: 0; - } +.pterodactyl-login-input > input { + background: rgba(0, 0, 0, 0.4); + border: 1px solid #000; + border-radius: 2px; + color: #fff; +} + +.pterodactyl-login-input > .form-control-feedback { + color: #fff; +} + +.pterodactyl-login-button--main { + background: rgba(0, 0, 0, 0.4); + border: 1px solid #000; + border-radius: 2px; + color: #fff; +} + +.pterodactyl-login-button--main:hover { + background: rgba(0, 0, 0, 0.7); + border: 1px solid #000; + border-radius: 2px; + color: #fff; +} + +.pterodactyl-login-button--left { + background: rgba(255, 255, 255, 0.4); + border: 1px solid rgba(255, 255, 255, 0.6); + border-radius: 2px; + color: #fff; +} + +.pterodactyl-login-button--left:hover { + background: rgba(255, 255, 255, 0.6); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 2px; + color: #fff; } .weight-100 { diff --git a/resources/themes/pterodactyl/auth/login.blade.php b/resources/themes/pterodactyl/auth/login.blade.php index 80e999f79..e81af8d31 100644 --- a/resources/themes/pterodactyl/auth/login.blade.php +++ b/resources/themes/pterodactyl/auth/login.blade.php @@ -10,49 +10,55 @@ @endsection @section('content') -
- @if (count($errors) > 0) -
- - @lang('auth.auth_error')

-
    - @foreach ($errors->all() as $error) -
  • {{ $error }}
  • - @endforeach -
-
- @endif - @foreach (Alert::getMessages() as $type => $messages) - @foreach ($messages as $message) -