diff --git a/app/Http/Controllers/Auth/AbstractLoginController.php b/app/Http/Controllers/Auth/AbstractLoginController.php new file mode 100644 index 000000000..eac305315 --- /dev/null +++ b/app/Http/Controllers/Auth/AbstractLoginController.php @@ -0,0 +1,151 @@ +auth = $auth; + $this->cache = $cache; + $this->encrypter = $encrypter; + $this->google2FA = $google2FA; + $this->repository = $repository; + + $this->lockoutTime = config('auth.lockout.time'); + $this->maxLoginAttempts = config('auth.lockout.attempts'); + } + + /** + * Get the failed login response instance. + * + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null) + { + $this->incrementLoginAttempts($request); + $this->fireFailedLoginEvent($user, [ + $this->getField($request->input('user')) => $request->input('user'), + ]); + + throw new DisplayException(trans('auth.failed')); + } + + /** + * Send the response after the user was authenticated. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + protected function sendLoginResponse(Request $request): JsonResponse + { + $request->session()->regenerate(); + + $this->clearLoginAttempts($request); + + return $this->authenticated($request, $this->guard()->user()) + ?: response()->json([ + 'intended' => $this->redirectPath(), + ]); + } + + /** + * Determine if the user is logging in using an email or username,. + * + * @param string $input + * @return string + */ + protected function getField(string $input = null): string + { + return str_contains($input, '@') ? 'email' : 'username'; + } + + /** + * Fire a failed login event. + * + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * @param array $credentials + */ + protected function fireFailedLoginEvent(Authenticatable $user = null, array $credentials = []) + { + event(new Failed($user, $credentials)); + } +} diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 198da73f0..f35be6d37 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -29,4 +29,17 @@ class ForgotPasswordController extends Controller return $this->sendResetLinkResponse(Password::RESET_LINK_SENT); } + + /** + * Get the response for a successful password reset link. + * + * @param string $response + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse + */ + protected function sendResetLinkResponse($response) + { + return response()->json([ + 'status' => trans($response), + ]); + } } diff --git a/app/Http/Controllers/Auth/LoginCheckpointController.php b/app/Http/Controllers/Auth/LoginCheckpointController.php new file mode 100644 index 000000000..cacd0da1f --- /dev/null +++ b/app/Http/Controllers/Auth/LoginCheckpointController.php @@ -0,0 +1,47 @@ +cache->pull($request->input('confirmation_token'), []); + $user = $this->repository->find(array_get($cache, 'user_id', 0)); + } catch (RecordNotFoundException $exception) { + return $this->sendFailedLoginResponse($request); + } + + if (! array_get($cache, 'valid_credentials') || array_get($cache, 'request_ip') !== $request->ip()) { + return $this->sendFailedLoginResponse($request, $user); + } + + if (! $this->google2FA->verifyKey( + $this->encrypter->decrypt($user->totp_secret), + $request->input('authentication_code'), + config('pterodactyl.auth.2fa.window') + )) { + return $this->sendFailedLoginResponse($request, $user); + } + + $this->authManager->guard()->login($user, true); + + return $this->sendLoginResponse($request); + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 3d336741a..22d6f4686 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -3,106 +3,11 @@ namespace Pterodactyl\Http\Controllers\Auth; use Illuminate\Http\Request; -use Illuminate\Auth\AuthManager; use Illuminate\Http\JsonResponse; -use PragmaRX\Google2FA\Google2FA; -use Illuminate\Auth\Events\Failed; -use Pterodactyl\Exceptions\DisplayException; -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 +class LoginController extends AbstractLoginController { - 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. - * - * @var string - */ - protected $redirectTo = '/'; - - /** - * Lockout time for failed login requests. - * - * @var int - */ - protected $lockoutTime; - - /** - * After how many attempts should logins be throttled and locked. - * - * @var int - */ - protected $maxLoginAttempts; - - /** - * LoginController constructor. - * - * @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 - */ - 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; - - $this->lockoutTime = $this->config->get('auth.lockout.time'); - $this->maxLoginAttempts = $this->config->get('auth.lockout.attempts'); - } - /** * Handle a login request to the application. * @@ -114,7 +19,7 @@ class LoginController extends Controller */ public function login(Request $request): JsonResponse { - $username = $request->input(self::USER_INPUT_FIELD); + $username = $request->input('user'); $useColumn = $this->getField($username); if ($this->hasTooManyLoginAttempts($request)) { @@ -137,117 +42,15 @@ class LoginController extends Controller 'request_ip' => $request->ip(), ], 5); - return response()->json([ - 'complete' => false, - 'token' => $token, - ]); + return response()->json(['complete' => false, 'token' => $token]); } - if ($validCredentials) { - $this->auth->guard()->login($user, true); - - return response()->json(['complete' => true]); - } - - return $this->sendFailedLoginResponse($request, $user); - } - - /** - * 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|\Illuminate\Http\Response - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function loginCheckpoint(Request $request) - { - if (is_null($request->input('confirmation_token')) || is_null($request->input('authentication_code'))) { - return $this->sendFailedLoginResponse($request); - } - - try { - $cache = $this->cache->pull($request->input('confirmation_token'), []); - $user = $this->repository->find(array_get($cache, 'user_id', 0)); - } catch (RecordNotFoundException $exception) { - return $this->sendFailedLoginResponse($request); - } - - if (! array_get($cache, 'valid_credentials') || array_get($cache, 'request_ip') !== $request->ip()) { - return $this->sendFailedLoginResponse($request, $user); - } - - if (! $this->google2FA->verifyKey( - $this->encrypter->decrypt($user->totp_secret), - $request->input('authentication_code'), - $this->config->get('pterodactyl.auth.2fa.window') - )) { + if (! $validCredentials) { return $this->sendFailedLoginResponse($request, $user); } $this->auth->guard()->login($user, true); - return $this->sendLoginResponse($request); - } - - /** - * Get the failed login response instance. - * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Contracts\Auth\Authenticatable|null $user - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null) - { - $this->incrementLoginAttempts($request); - $this->fireFailedLoginEvent($user, [ - $this->getField($request->input(self::USER_INPUT_FIELD)) => $request->input(self::USER_INPUT_FIELD), - ]); - - throw new DisplayException(trans('auth.failed')); - } - - /** - * Send the response after the user was authenticated. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - */ - protected function sendLoginResponse(Request $request) - { - $request->session()->regenerate(); - - $this->clearLoginAttempts($request); - - return $this->authenticated($request, $this->guard()->user()) - ?: response()->json([ - 'intended' => $this->redirectPath(), - ]); - } - - /** - * Determine if the user is logging in using an email or username,. - * - * @param string $input - * @return string - */ - private function getField(string $input = null): string - { - 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)); + return response()->json(['complete' => true]); } } diff --git a/app/Http/Requests/Auth/LoginCheckpointRequest.php b/app/Http/Requests/Auth/LoginCheckpointRequest.php new file mode 100644 index 000000000..64fa7859e --- /dev/null +++ b/app/Http/Requests/Auth/LoginCheckpointRequest.php @@ -0,0 +1,31 @@ + 'required|string', + 'authentication_code' => 'required|int', + ]; + } +} diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 000000000..7fed20939 --- /dev/null +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,27 @@ + 'required|string|min:1', + 'password' => 'required|string', + ]; + } +} diff --git a/routes/auth.php b/routes/auth.php index 7145f02de..57bcfe98a 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -13,7 +13,7 @@ Route::group(['middleware' => 'guest'], function () { Route::get('/password/reset/{token}', 'ResetPasswordController@showResetForm')->name('auth.reset'); Route::post('/login', 'LoginController@login')->middleware('recaptcha'); - Route::post('/login/checkpoint', 'LoginController@loginCheckpoint')->name('auth.checkpoint'); + Route::post('/login/checkpoint', 'LoginCheckpointController@index')->name('auth.checkpoint'); Route::post('/password', 'ForgotPasswordController@sendResetLinkEmail')->middleware('recaptcha'); Route::post('/password/reset', 'ResetPasswordController@reset')->name('auth.reset.post')->middleware('recaptcha'); Route::post('/password/reset/{token}', 'ForgotPasswordController@sendResetLinkEmail')->middleware('recaptcha');