diff --git a/app/Exceptions/Http/TwoFactorAuthRequiredException.php b/app/Exceptions/Http/TwoFactorAuthRequiredException.php
new file mode 100644
index 000000000..c38503572
--- /dev/null
+++ b/app/Exceptions/Http/TwoFactorAuthRequiredException.php
@@ -0,0 +1,21 @@
+ [
SubstituteBindings::class,
diff --git a/app/Http/Middleware/RequireTwoFactorAuthentication.php b/app/Http/Middleware/RequireTwoFactorAuthentication.php
index 0689cc14a..d837df24d 100644
--- a/app/Http/Middleware/RequireTwoFactorAuthentication.php
+++ b/app/Http/Middleware/RequireTwoFactorAuthentication.php
@@ -1,11 +1,4 @@
.
- *
- * This software is licensed under the terms of the MIT license.
- * https://opensource.org/licenses/MIT
- */
namespace Pterodactyl\Http\Middleware;
@@ -13,6 +6,7 @@ use Closure;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Prologue\Alerts\AlertsMessageBag;
+use Pterodactyl\Exceptions\Http\TwoFactorAuthRequiredException;
class RequireTwoFactorAuthentication
{
@@ -30,7 +24,7 @@ class RequireTwoFactorAuthentication
*
* @var string
*/
- protected $redirectRoute = 'account';
+ protected $redirectRoute = '/account';
/**
* RequireTwoFactorAuthentication constructor.
@@ -43,41 +37,46 @@ class RequireTwoFactorAuthentication
}
/**
- * Handle an incoming request.
+ * Check the user state on the incoming request to determine if they should be allowed to
+ * proceed or not. This checks if the Panel is configured to require 2FA on an account in
+ * order to perform actions. If so, we check the level at which it is required (all users
+ * or just admins) and then check if the user has enabled it for their account.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
+ *
+ * @throws \Pterodactyl\Exceptions\Http\TwoFactorAuthRequiredException
*/
public function handle(Request $request, Closure $next)
{
- if (! $request->user()) {
- return $next($request);
- }
-
+ /** @var \Pterodactyl\Models\User $user */
+ $user = $request->user();
+ $uri = rtrim($request->getRequestUri(), '/') . '/';
$current = $request->route()->getName();
- if (in_array($current, ['auth', 'account']) || Str::startsWith($current, ['auth.', 'account.'])) {
+
+ if (! $user || Str::startsWith($uri, ['/auth/']) || Str::startsWith($current, ['auth.', 'account.'])) {
return $next($request);
}
- switch ((int) config('pterodactyl.auth.2fa_required')) {
- case self::LEVEL_ADMIN:
- if (! $request->user()->root_admin || $request->user()->use_totp) {
- return $next($request);
- }
- break;
- case self::LEVEL_ALL:
- if ($request->user()->use_totp) {
- return $next($request);
- }
- break;
- case self::LEVEL_NONE:
- default:
- return $next($request);
+ $level = (int)config('pterodactyl.auth.2fa_required');
+ // If this setting is not configured, or the user is already using 2FA then we can just
+ // send them right through, nothing else needs to be checked.
+ //
+ // If the level is set as admin and the user is not an admin, pass them through as well.
+ if ($level === self::LEVEL_NONE || $user->use_totp) {
+ return $next($request);
+ } else if ($level === self::LEVEL_ADMIN && ! $user->root_admin) {
+ return $next($request);
+ }
+
+ // For API calls return an exception which gets rendered nicely in the API response.
+ if ($request->isJson() || Str::startsWith($uri, '/api/')) {
+ throw new TwoFactorAuthRequiredException;
}
$this->alert->danger(trans('auth.2fa_must_be_enabled'))->flash();
- return redirect()->route($this->redirectRoute);
+ return redirect()->to($this->redirectRoute);
}
}
diff --git a/resources/scripts/api/interceptors.ts b/resources/scripts/api/interceptors.ts
new file mode 100644
index 000000000..8ff03fbf0
--- /dev/null
+++ b/resources/scripts/api/interceptors.ts
@@ -0,0 +1,16 @@
+import http from '@/api/http';
+import { AxiosError } from 'axios';
+import { History } from 'history';
+
+export const setupInterceptors = (history: History) => {
+ http.interceptors.response.use(resp => resp, (error: AxiosError) => {
+ if (error.response?.status === 400) {
+ if (error.response?.data.errors?.[0].code === 'TwoFactorAuthRequiredException') {
+ if (!window.location.pathname.startsWith('/account')) {
+ history.replace('/account', { twoFactorRedirect: true });
+ }
+ }
+ }
+ throw error;
+ });
+};
diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx
index f61e73e0b..fefa501cd 100644
--- a/resources/scripts/components/App.tsx
+++ b/resources/scripts/components/App.tsx
@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import ReactGA from 'react-ga';
import { hot } from 'react-hot-loader/root';
-import { BrowserRouter, Route, Switch, useLocation } from 'react-router-dom';
+import { Route, Router, Switch, useLocation } from 'react-router-dom';
import { StoreProvider } from 'easy-peasy';
import { store } from '@/state';
import DashboardRouter from '@/routers/DashboardRouter';
@@ -13,6 +13,8 @@ import ProgressBar from '@/components/elements/ProgressBar';
import NotFound from '@/components/screens/NotFound';
import tw from 'twin.macro';
import GlobalStylesheet from '@/assets/css/GlobalStylesheet';
+import { createBrowserHistory } from 'history';
+import { setupInterceptors } from '@/api/interceptors';
interface ExtendedWindow extends Window {
SiteConfiguration?: SiteSettings;
@@ -30,6 +32,10 @@ interface ExtendedWindow extends Window {
};
}
+const history = createBrowserHistory({ basename: '/' });
+
+setupInterceptors(history);
+
const Pageview = () => {
const { pathname } = useLocation();
@@ -72,7 +78,7 @@ const App = () => {
-
+
{SiteConfiguration?.analytics && }
@@ -80,7 +86,7 @@ const App = () => {
-
+
diff --git a/resources/scripts/components/dashboard/AccountOverviewContainer.tsx b/resources/scripts/components/dashboard/AccountOverviewContainer.tsx
index 98e7a8d53..47161f793 100644
--- a/resources/scripts/components/dashboard/AccountOverviewContainer.tsx
+++ b/resources/scripts/components/dashboard/AccountOverviewContainer.tsx
@@ -7,9 +7,11 @@ import PageContentBlock from '@/components/elements/PageContentBlock';
import tw from 'twin.macro';
import { breakpoint } from '@/theme';
import styled from 'styled-components/macro';
+import { RouteComponentProps } from 'react-router';
+import MessageBox from '@/components/MessageBox';
const Container = styled.div`
- ${tw`flex flex-wrap my-10`};
+ ${tw`flex flex-wrap`};
& > div {
${tw`w-full`};
@@ -24,10 +26,15 @@ const Container = styled.div`
}
`;
-export default () => {
+export default ({ location: { state } }: RouteComponentProps) => {
return (
-
+ {state?.twoFactorRedirect &&
+
+ Your account must have two-factor authentication enabled in order to continue.
+
+ }
+
diff --git a/routes/api-client.php b/routes/api-client.php
index 35a2938dd..bc689f80b 100644
--- a/routes/api-client.php
+++ b/routes/api-client.php
@@ -1,6 +1,7 @@
name('api:client.index');
Route::get('/permissions', 'ClientController@permissions');
Route::group(['prefix' => '/account'], function () {
- Route::get('/', 'AccountController@index')->name('api:client.account');
- Route::get('/two-factor', 'TwoFactorController@index');
- Route::post('/two-factor', 'TwoFactorController@store');
- Route::delete('/two-factor', 'TwoFactorController@delete');
+ Route::get('/', 'AccountController@index')->name('api:client.account')->withoutMiddleware(RequireTwoFactorAuthentication::class);
+ Route::get('/two-factor', 'TwoFactorController@index')->withoutMiddleware(RequireTwoFactorAuthentication::class);
+ Route::post('/two-factor', 'TwoFactorController@store')->withoutMiddleware(RequireTwoFactorAuthentication::class);
+ Route::delete('/two-factor', 'TwoFactorController@delete')->withoutMiddleware(RequireTwoFactorAuthentication::class);
Route::put('/email', 'AccountController@updateEmail')->name('api:client.account.update-email');
Route::put('/password', 'AccountController@updatePassword')->name('api:client.account.update-password');
diff --git a/routes/base.php b/routes/base.php
index 4d43235c4..3be62423e 100644
--- a/routes/base.php
+++ b/routes/base.php
@@ -1,9 +1,14 @@
name('index')->fallback();
-Route::get('/account', 'IndexController@index')->name('account');
+Route::get('/account', 'IndexController@index')
+ ->withoutMiddleware(RequireTwoFactorAuthentication::class)
+ ->name('account');
Route::get('/locales/{locale}/{namespace}.json', 'LocaleController')
+ ->withoutMiddleware(RequireTwoFactorAuthentication::class)
->where('namespace', '.*');
Route::get('/{react}', 'IndexController@index')