diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 127802d22..3d336741a 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -4,9 +4,10 @@ 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 Illuminate\Http\RedirectResponse; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Encryption\Encrypter; @@ -106,11 +107,12 @@ class LoginController extends Controller * Handle a login request to the application. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse * + * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Illuminate\Validation\ValidationException */ - public function login(Request $request) + public function login(Request $request): JsonResponse { $username = $request->input(self::USER_INPUT_FIELD); $useColumn = $this->getField($username); @@ -128,37 +130,28 @@ class LoginController extends Controller $validCredentials = password_verify($request->input('password'), $user->password); if ($user->use_totp) { - $token = str_random(64); - $this->cache->put($token, ['user_id' => $user->id, 'valid_credentials' => $validCredentials], 5); + $token = str_random(128); + $this->cache->put($token, [ + 'user_id' => $user->id, + 'valid_credentials' => $validCredentials, + 'request_ip' => $request->ip(), + ], 5); - return redirect()->route('auth.totp')->with('authentication_token', $token); + return response()->json([ + 'complete' => false, + 'token' => $token, + ]); } if ($validCredentials) { $this->auth->guard()->login($user, true); - return $this->sendLoginResponse($request); + return response()->json(['complete' => true]); } return $this->sendFailedLoginResponse($request, $user); } - /** - * 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. In order to add additional layers of security, users are not @@ -167,27 +160,29 @@ class LoginController extends Controller * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\DisplayException */ - public function loginUsingTotp(Request $request) + public function loginCheckpoint(Request $request) { - if (is_null($request->input('verify_token'))) { + if (is_null($request->input('confirmation_token')) || is_null($request->input('authentication_code'))) { return $this->sendFailedLoginResponse($request); } try { - $cache = $this->cache->pull($request->input('verify_token'), []); + $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 (is_null($request->input('2fa_token')) || ! array_get($cache, 'valid_credentials')) { + 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('2fa_token'), + $request->input('authentication_code'), $this->config->get('pterodactyl.auth.2fa.window') )) { return $this->sendFailedLoginResponse($request, $user); @@ -203,24 +198,35 @@ class LoginController extends Controller * * @param \Illuminate\Http\Request $request * @param \Illuminate\Contracts\Auth\Authenticatable|null $user - * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException */ - protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null): RedirectResponse + 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), ]); - $errors = [self::USER_INPUT_FIELD => trans('auth.failed')]; + throw new DisplayException(trans('auth.failed')); + } - if ($request->expectsJson()) { - return response()->json($errors, 422); - } + /** + * 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(); - return redirect()->route('auth.login') - ->withInput($request->only(self::USER_INPUT_FIELD)) - ->withErrors($errors); + $this->clearLoginAttempts($request); + + return $this->authenticated($request, $this->guard()->user()) + ?: response()->json([ + 'intended' => $this->redirectPath(), + ]); } /** diff --git a/package.json b/package.json index a790e437f..45825c2ab 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "vue-template-compiler": "^2.5.16", "vueify-insert-css": "^1.0.0", "vuex": "^3.0.1", + "vuex-flash": "^1.0.0", "vuex-i18n": "^1.10.5", "webpack": "^4.4.1", "webpack-stream": "^4.0.3", diff --git a/resources/assets/pterodactyl/scripts/app.js b/resources/assets/pterodactyl/scripts/app.js index 2645b4728..9100c82c2 100644 --- a/resources/assets/pterodactyl/scripts/app.js +++ b/resources/assets/pterodactyl/scripts/app.js @@ -1,6 +1,8 @@ import Vue from 'vue'; import Vuex from 'vuex'; import vuexI18n from 'vuex-i18n'; +import VuexFlash from 'vuex-flash'; +import { createFlashStore } from 'vuex-flash'; import VueRouter from 'vue-router'; // Helpers @@ -15,7 +17,11 @@ window.Ziggy = Ziggy; Vue.use(Vuex); -const store = new Vuex.Store(); +const store = new Vuex.Store({ + plugins: [ + createFlashStore(), + ], +}); const route = require('./../../../../vendor/tightenco/ziggy/src/js/route').default; Vue.config.productionTip = false; @@ -26,6 +32,10 @@ Vue.mixin({ }); Vue.use(VueRouter); +Vue.use(VuexFlash, { + mixin: true, + template: require('./components/errors/Flash.template') +}); Vue.use(vuexI18n.plugin, store); Vue.i18n.add('en', Locales.en); @@ -34,9 +44,20 @@ Vue.i18n.set('en'); const router = new VueRouter({ routes: [ { - path: '/:action?', + name: 'login', + path: '/', component: Login, - } + }, + { + name: 'forgot-password', + path: '/forgot-password', + component: Login, + }, + { + name: 'checkpoint', + path: '/checkpoint', + component: Login, + }, ] }); diff --git a/resources/assets/pterodactyl/scripts/components/auth/ForgotPassword.vue b/resources/assets/pterodactyl/scripts/components/auth/ForgotPassword.vue index af46c1c06..a252dab94 100644 --- a/resources/assets/pterodactyl/scripts/components/auth/ForgotPassword.vue +++ b/resources/assets/pterodactyl/scripts/components/auth/ForgotPassword.vue @@ -3,7 +3,7 @@