diff --git a/app/Http/Controllers/Auth/AbstractLoginController.php b/app/Http/Controllers/Auth/AbstractLoginController.php index c99ce85f8..108a104e1 100644 --- a/app/Http/Controllers/Auth/AbstractLoginController.php +++ b/app/Http/Controllers/Auth/AbstractLoginController.php @@ -6,45 +6,17 @@ use Illuminate\Http\Request; use Pterodactyl\Models\User; use Illuminate\Auth\AuthManager; use Illuminate\Http\JsonResponse; -use PragmaRX\Google2FA\Google2FA; use Illuminate\Auth\Events\Failed; +use Illuminate\Contracts\Config\Repository; 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; abstract class AbstractLoginController extends Controller { use AuthenticatesUsers; - /** - * @var \Illuminate\Auth\AuthManager - */ - protected $auth; - - /** - * @var \Illuminate\Contracts\Cache\Repository - */ - protected $cache; - - /** - * @var \Illuminate\Contracts\Encryption\Encrypter - */ - protected $encrypter; - - /** - * @var \PragmaRX\Google2FA\Google2FA - */ - protected $google2FA; - - /** - * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface - */ - protected $repository; - /** * Lockout time for failed login requests. * @@ -66,30 +38,29 @@ abstract class AbstractLoginController extends Controller */ protected $redirectTo = '/'; + /** + * @var \Illuminate\Auth\AuthManager + */ + protected $auth; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + /** * LoginController constructor. * - * @param \Illuminate\Auth\AuthManager $auth - * @param \Illuminate\Contracts\Cache\Repository $cache - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter - * @param \PragmaRX\Google2FA\Google2FA $google2FA - * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + * @param \Illuminate\Auth\AuthManager $auth + * @param \Illuminate\Contracts\Config\Repository $config */ - public function __construct( - AuthManager $auth, - CacheRepository $cache, - Encrypter $encrypter, - Google2FA $google2FA, - UserRepositoryInterface $repository - ) { - $this->auth = $auth; - $this->cache = $cache; - $this->encrypter = $encrypter; - $this->google2FA = $google2FA; - $this->repository = $repository; + public function __construct(AuthManager $auth, Repository $config) + { + $this->lockoutTime = $config->get('auth.lockout.time'); + $this->maxLoginAttempts = $config->get('auth.lockout.attempts'); - $this->lockoutTime = config('auth.lockout.time'); - $this->maxLoginAttempts = config('auth.lockout.attempts'); + $this->auth = $auth; + $this->config = $config; } /** @@ -128,10 +99,12 @@ abstract class AbstractLoginController extends Controller $this->auth->guard()->login($user, true); - return response()->json([ - 'complete' => true, - 'intended' => $this->redirectPath(), - 'user' => $user->toVueObject(), + return JsonResponse::create([ + 'data' => [ + 'complete' => true, + 'intended' => $this->redirectPath(), + 'user' => $user->toVueObject(), + ], ]); } diff --git a/app/Http/Controllers/Auth/LoginCheckpointController.php b/app/Http/Controllers/Auth/LoginCheckpointController.php index 33aa3e4a1..8af396ede 100644 --- a/app/Http/Controllers/Auth/LoginCheckpointController.php +++ b/app/Http/Controllers/Auth/LoginCheckpointController.php @@ -2,12 +2,64 @@ namespace Pterodactyl\Http\Controllers\Auth; +use Illuminate\Auth\AuthManager; use Illuminate\Http\JsonResponse; +use PragmaRX\Google2FA\Google2FA; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest; +use Illuminate\Contracts\Cache\Repository as CacheRepository; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; class LoginCheckpointController extends AbstractLoginController { + /** + * @var \Illuminate\Contracts\Cache\Repository + */ + private $cache; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + private $repository; + + /** + * @var \PragmaRX\Google2FA\Google2FA + */ + private $google2FA; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + private $encrypter; + + /** + * LoginCheckpointController constructor. + * + * @param \Illuminate\Auth\AuthManager $auth + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \PragmaRX\Google2FA\Google2FA $google2FA + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Illuminate\Contracts\Cache\Repository $cache + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + */ + public function __construct( + AuthManager $auth, + Encrypter $encrypter, + Google2FA $google2FA, + Repository $config, + CacheRepository $cache, + UserRepositoryInterface $repository + ) { + parent::__construct($auth, $config); + + $this->google2FA = $google2FA; + $this->cache = $cache; + $this->repository = $repository; + $this->encrypter = $encrypter; + } + /** * Handle a login where the user is required to provide a TOTP authentication * token. Once a user has reached this stage it is assumed that they have already @@ -16,29 +68,28 @@ class LoginCheckpointController extends AbstractLoginController * @param \Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest $request * @return \Illuminate\Http\JsonResponse * + * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException + * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException + * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException * @throws \Pterodactyl\Exceptions\DisplayException */ public function __invoke(LoginCheckpointRequest $request): JsonResponse { try { - $cache = $this->cache->pull($request->input('confirmation_token'), []); - $user = $this->repository->find(array_get($cache, 'user_id', 0)); + $user = $this->repository->find( + $this->cache->pull($request->input('confirmation_token'), 0) + ); } catch (RecordNotFoundException $exception) { return $this->sendFailedLoginResponse($request); } - if (array_get($cache, 'request_ip') !== $request->ip()) { - return $this->sendFailedLoginResponse($request, $user); + $decrypted = $this->encrypter->decrypt($user->totp_secret); + $window = $this->config->get('pterodactyl.auth.2fa.window'); + + if ($this->google2FA->verifyKey($decrypted, $request->input('authentication_code'), $window)) { + return $this->sendLoginResponse($user, $request); } - if (! $this->google2FA->verifyKey( - $this->encrypter->decrypt($user->totp_secret), - $request->input('authentication_code'), - config('pterodactyl.auth.2fa.window') - )) { - return $this->sendFailedLoginResponse($request, $user); - } - - return $this->sendLoginResponse($user, $request); + return $this->sendFailedLoginResponse($request, $user); } } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 7a3525b61..a3419412c 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -2,13 +2,57 @@ namespace Pterodactyl\Http\Controllers\Auth; +use Illuminate\Support\Str; use Illuminate\Http\Request; +use Illuminate\Auth\AuthManager; use Illuminate\Http\JsonResponse; use Illuminate\Contracts\View\View; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\View\Factory as ViewFactory; +use Illuminate\Contracts\Cache\Repository as CacheRepository; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; class LoginController extends AbstractLoginController { + /** + * @var \Illuminate\Contracts\View\Factory + */ + private $view; + + /** + * @var \Illuminate\Contracts\Cache\Repository + */ + private $cache; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + private $repository; + + /** + * LoginController constructor. + * + * @param \Illuminate\Auth\AuthManager $auth + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Illuminate\Contracts\Cache\Repository $cache + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + * @param \Illuminate\Contracts\View\Factory $view + */ + public function __construct( + AuthManager $auth, + Repository $config, + CacheRepository $cache, + UserRepositoryInterface $repository, + ViewFactory $view + ) { + parent::__construct($auth, $config); + + $this->view = $view; + $this->cache = $cache; + $this->repository = $repository; + } + /** * Handle all incoming requests for the authentication routes and render the * base authentication view component. Vuejs will take over at this point and @@ -18,7 +62,7 @@ class LoginController extends AbstractLoginController */ public function index(): View { - return view('templates/auth.core'); + return $this->view->make('templates/auth.core'); } /** @@ -55,85 +99,19 @@ class LoginController extends AbstractLoginController } if ($user->use_totp) { - $token = str_random(64); - $this->cache->put($token, ['user_id' => $user->id, 'valid_credentials' => true], 5); + $token = Str::random(64); + $this->cache->put($token, $user->id, 5); - return redirect()->route('auth.totp')->with('authentication_token', $token); + return JsonResponse::create([ + 'data' => [ + 'complete' => false, + 'confirmation_token' => $token, + ], + ]); } $this->auth->guard()->login($user, true); return $this->sendLoginResponse($user, $request); } - - /** - * Handle a TOTP implementation page. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View - */ - public function totp(Request $request) - { - $token = $request->session()->get('authentication_token'); - if (is_null($token) || $this->auth->guard()->user()) { - return redirect()->route('auth.login'); - } - - return view('auth.totp', ['verify_key' => $token]); - } - - /** - * Handle a login where the user is required to provide a TOTP authentication - * token. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response - * - * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException - * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException - * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function loginUsingTotp(Request $request) - { - if (is_null($request->input('verify_token'))) { - return $this->sendFailedLoginResponse($request); - } - - try { - $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 (is_null($request->input('2fa_token'))) { - return $this->sendFailedLoginResponse($request, $user); - } - - 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); - } - - // If the user is using 2FA we do not actually log them in at this step, we return - // a one-time token to link the 2FA credentials to this account via the UI. - if ($user->use_totp) { - $token = str_random(128); - $this->cache->put($token, [ - 'user_id' => $user->id, - 'request_ip' => $request->ip(), - ], 5); - - return response()->json([ - 'complete' => false, - 'login_token' => $token, - ]); - } - - return $this->sendLoginResponse($user, $request); - } } diff --git a/resources/scripts/api/auth/login.ts b/resources/scripts/api/auth/login.ts index 742c0b17c..2bbdfe5a0 100644 --- a/resources/scripts/api/auth/login.ts +++ b/resources/scripts/api/auth/login.ts @@ -1,9 +1,9 @@ import http from '@/api/http'; -interface LoginResponse { +export interface LoginResponse { complete: boolean; intended?: string; - token?: string; + confirmationToken?: string; } export default (user: string, password: string): Promise => { @@ -15,9 +15,9 @@ export default (user: string, password: string): Promise => { } return resolve({ - complete: response.data.complete, - intended: response.data.intended || undefined, - token: response.data.token || undefined, + complete: response.data.data.complete, + intended: response.data.data.intended || undefined, + confirmationToken: response.data.data.confirmation_token || undefined, }); }) .catch(reject); diff --git a/resources/scripts/api/auth/loginCheckpoint.ts b/resources/scripts/api/auth/loginCheckpoint.ts new file mode 100644 index 000000000..244d27c81 --- /dev/null +++ b/resources/scripts/api/auth/loginCheckpoint.ts @@ -0,0 +1,18 @@ +import http from '@/api/http'; +import { LoginResponse } from '@/api/auth/login'; + +export default (token: string, code: string): Promise => { + return new Promise((resolve, reject) => { + http.post('/auth/login/checkpoint', { + // eslint-disable-next-line @typescript-eslint/camelcase + confirmation_token: token, + // eslint-disable-next-line @typescript-eslint/camelcase + authentication_code: code, + }) + .then(response => resolve({ + complete: response.data.data.complete, + intended: response.data.data.intended || undefined, + })) + .catch(reject); + }); +}; diff --git a/resources/scripts/components/auth/ForgotPasswordContainer.tsx b/resources/scripts/components/auth/ForgotPasswordContainer.tsx index cbc4dfcfc..aa6520c0b 100644 --- a/resources/scripts/components/auth/ForgotPasswordContainer.tsx +++ b/resources/scripts/components/auth/ForgotPasswordContainer.tsx @@ -1,9 +1,7 @@ import * as React from 'react'; -import OpenInputField from '@/components/forms/OpenInputField'; import { Link } from 'react-router-dom'; import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail'; import { connect } from 'react-redux'; -import { ReduxState } from '@/redux/types'; import { pushFlashMessage, clearAllFlashMessages } from '@/redux/actions/flash'; import { httpErrorToHuman } from '@/api/http'; diff --git a/resources/scripts/components/auth/LoginCheckpointContainer.tsx b/resources/scripts/components/auth/LoginCheckpointContainer.tsx index a59d097d8..1f2dd89d2 100644 --- a/resources/scripts/components/auth/LoginCheckpointContainer.tsx +++ b/resources/scripts/components/auth/LoginCheckpointContainer.tsx @@ -1,8 +1,12 @@ import * as React from 'react'; -import { RouteComponentProps } from 'react-router'; +import { RouteComponentProps, StaticContext } from 'react-router'; import { connect } from 'react-redux'; import { pushFlashMessage, clearAllFlashMessages } from '@/redux/actions/flash'; import NetworkErrorMessage from '@/components/NetworkErrorMessage'; +import MessageBox from '@/components/MessageBox'; +import { Link } from 'react-router-dom'; +import loginCheckpoint from '@/api/auth/loginCheckpoint'; +import { httpErrorToHuman } from '@/api/http'; type State = Readonly<{ isLoading: boolean; @@ -10,12 +14,46 @@ type State = Readonly<{ code: string; }>; -class LoginCheckpointContainer extends React.PureComponent { +class LoginCheckpointContainer extends React.PureComponent, State> { state: State = { code: '', isLoading: false, }; + componentDidMount () { + const { state } = this.props.location; + if (!state || !state.token) { + this.props.history.replace('/login'); + } + } + + onChangeHandler = (e: React.ChangeEvent) => { + if (e.target.value.length > 6) { + e.target.value = e.target.value.substring(0, 6); + return e.preventDefault(); + } + + this.setState({ code: e.target.value }); + }; + + submit = (e: React.FormEvent) => { + e.preventDefault(); + + this.setState({ isLoading: true }, () => { + loginCheckpoint(this.props.location.state.token, this.state.code) + .then(response => { + if (response.complete) { + // @ts-ignore + window.location = response.intended || '/'; + } + }) + .catch(error => { + console.error(error); + this.setState({ errorMessage: httpErrorToHuman(error), isLoading: false }); + }); + }); + }; + render () { return ( @@ -23,12 +61,20 @@ class LoginCheckpointContainer extends React.PureComponent -
null}> -

- This account is protected with two-factor authentication. Please provide an authentication - code from your device in order to continue. -

-
+ + + This account is protected with two-factor authentication. A valid authentication token must + be provided in order to continue. + +
+ +
+
+ + Return to Login + +
); diff --git a/resources/scripts/components/auth/LoginContainer.tsx b/resources/scripts/components/auth/LoginContainer.tsx index 2b0385e2a..f1600ffe0 100644 --- a/resources/scripts/components/auth/LoginContainer.tsx +++ b/resources/scripts/components/auth/LoginContainer.tsx @@ -12,8 +12,6 @@ type State = Readonly<{ }>; export default class LoginContainer extends React.PureComponent { - username = React.createRef(); - state: State = { isLoading: false, }; @@ -33,7 +31,7 @@ export default class LoginContainer extends React.PureComponent this.setState({ diff --git a/resources/scripts/components/auth/ResetPasswordContainer.tsx b/resources/scripts/components/auth/ResetPasswordContainer.tsx index 3483207ee..5e0783630 100644 --- a/resources/scripts/components/auth/ResetPasswordContainer.tsx +++ b/resources/scripts/components/auth/ResetPasswordContainer.tsx @@ -95,9 +95,9 @@ class ResetPasswordContainer extends React.PureComponent {
- + {

- +