diff --git a/app/Http/Controllers/Auth/AbstractLoginController.php b/app/Http/Controllers/Auth/AbstractLoginController.php index de8e275d3..02f425216 100644 --- a/app/Http/Controllers/Auth/AbstractLoginController.php +++ b/app/Http/Controllers/Auth/AbstractLoginController.php @@ -155,13 +155,15 @@ abstract class AbstractLoginController extends Controller */ protected function createJsonWebToken(User $user): string { + $now = Chronos::now('utc'); + $token = $this->builder ->setIssuer('Pterodactyl Panel') ->setAudience(config('app.url')) ->setId(str_random(16), true) - ->setIssuedAt(Chronos::now()->getTimestamp()) - ->setNotBefore(Chronos::now()->getTimestamp()) - ->setExpiration(Chronos::now()->addSeconds(config('session.lifetime'))->getTimestamp()) + ->setIssuedAt($now->getTimestamp()) + ->setNotBefore($now->getTimestamp()) + ->setExpiration($now->addSeconds(config('jwt.lifetime'))->getTimestamp()) ->set('user', (new AccountTransformer())->transform($user)) ->sign($this->getJWTSigner(), $this->getJWTSigningKey()) ->getToken(); diff --git a/app/Http/Middleware/Api/AuthenticateKey.php b/app/Http/Middleware/Api/AuthenticateKey.php index 0b6b23f7f..4b8156dd1 100644 --- a/app/Http/Middleware/Api/AuthenticateKey.php +++ b/app/Http/Middleware/Api/AuthenticateKey.php @@ -98,13 +98,17 @@ class AuthenticateKey } // Run through the token validation and throw an exception if the token is not valid. + // + // The issued_at time is used for verification in order to allow rapid changing of session + // length on the Panel without having to wait on existing tokens to first expire. + $now = Chronos::now('utc'); if ( - $token->getClaim('nbf') > Chronos::now()->getTimestamp() + Chronos::createFromTimestampUTC($token->getClaim('nbf'))->gt($now) || $token->getClaim('iss') !== 'Pterodactyl Panel' || $token->getClaim('aud') !== config('app.url') - || $token->getClaim('exp') <= Chronos::now()->getTimestamp() + || Chronos::createFromTimestampUTC($token->getClaim('iat'))->addMinutes(config('jwt.lifetime'))->lte($now) ) { - throw new AccessDeniedHttpException; + throw new AccessDeniedHttpException('The authentication parameters provided are not valid for accessing this resource.'); } return (new ApiKey)->forceFill([ diff --git a/config/jwt.php b/config/jwt.php index 6bd04a635..d51a7fae9 100644 --- a/config/jwt.php +++ b/config/jwt.php @@ -12,6 +12,7 @@ return [ | */ 'key' => env('APP_JWT_KEY'), + 'lifetime' => env('APP_JWT_LIFETIME', 1440), 'signer' => \Lcobucci\JWT\Signer\Hmac\Sha256::class, ]; diff --git a/resources/assets/scripts/app.js b/resources/assets/scripts/app.js index fe58baf9f..6048cc5aa 100644 --- a/resources/assets/scripts/app.js +++ b/resources/assets/scripts/app.js @@ -10,22 +10,21 @@ require('./bootstrap'); import { Ziggy } from './helpers/ziggy'; import Locales from './../../../resources/lang/locales'; import { flash } from './mixins/flash'; -import { routes } from './routes'; import store from './store/index.js'; +import router from './router'; window.events = new Vue; window.Ziggy = Ziggy; Vue.use(Vuex); +Vue.use(VueRouter); +Vue.use(vuexI18n.plugin, store); const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default; Vue.mixin({ methods: { route } }); Vue.mixin(flash); -Vue.use(VueRouter); -Vue.use(vuexI18n.plugin, store); - Vue.i18n.add('en', Locales.en); Vue.i18n.set('en'); @@ -33,8 +32,4 @@ if (module.hot) { module.hot.accept(); } -const router = new VueRouter({ - mode: 'history', routes -}); - const app = new Vue({ store, router }).$mount('#pterodactyl'); diff --git a/resources/assets/scripts/models/user.js b/resources/assets/scripts/models/user.js index 149afd818..d4b338d95 100644 --- a/resources/assets/scripts/models/user.js +++ b/resources/assets/scripts/models/user.js @@ -9,17 +9,19 @@ export default class User { */ static fromToken(token) { if (!isString(token)) { - token = localStorage.getItem('token'); + token = this.getToken(); } if (!isString(token) || token.length < 1) { return null; } - const data = jwtDecode(token); - if (data.user) { - return new User(data.user); - } + try { + const data = jwtDecode(token); + if (data.user) { + return new User(data.user); + } + } catch (ex) {} return null; } @@ -29,8 +31,7 @@ export default class User { * * @returns {string | null} */ - static getToken() - { + static getToken() { return localStorage.getItem('token'); } @@ -60,4 +61,11 @@ export default class User { this.last_name = last_name; this.language = language; } + + /** + * Returns the JWT belonging to the current user. + */ + getJWT() { + return jwtDecode(User.getToken()); + } } diff --git a/resources/assets/scripts/router.js b/resources/assets/scripts/router.js new file mode 100644 index 000000000..2eaff883c --- /dev/null +++ b/resources/assets/scripts/router.js @@ -0,0 +1,81 @@ +import VueRouter from 'vue-router'; +import store from './store/index'; +import compareDate from 'date-fns/compare_asc' +import addHours from 'date-fns/add_hours' +import dateParse from 'date-fns/parse' +const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default; + +// Base Vuejs Templates +import Login from './components/auth/Login'; +import Dashboard from './components/dashboard/Dashboard'; +import Account from './components/dashboard/Account'; +import ResetPassword from './components/auth/ResetPassword'; + +const routes = [ + { name: 'login', path: '/auth/login', component: Login }, + { name: 'forgot-password', path: '/auth/password', component: Login }, + { name: 'checkpoint', path: '/auth/checkpoint', component: Login }, + { + name: 'reset-password', + path: '/auth/password/reset/:token', + component: ResetPassword, + props: function (route) { + return { token: route.params.token, email: route.query.email || '' }; + } + }, + + { name : 'dashboard', path: '/', component: Dashboard }, + { name : 'account', path: '/account', component: Account }, + { name : 'account.api', path: '/account/api', component: Account }, + { name : 'account.security', path: '/account/security', component: Account }, + + { + name: 'server', + path: '/server/:id', + // component: Server, + // children: [ + // { path: 'files', component: ServerFileManager } + // ], + } +]; + +const router = new VueRouter({ + mode: 'history', routes +}); + +// Redirect the user to the login page if they try to access a protected route and +// have no JWT or the JWT is expired and wouldn't be accepted by the Panel. +router.beforeEach((to, from, next) => { + if (to.path === route('auth.logout')) { + return window.location = route('auth.logout'); + } + + const user = store.getters['auth/getUser']; + + // If user is trying to access the authentication endpoints but is already authenticated + // don't try to load them, just send the user to the dashboard. + if (to.path.startsWith('/auth')) { + if (user !== null && compareDate(addHours(dateParse(user.getJWT().iat * 1000), 12), new Date()) >= 0) { + return window.location = '/'; + } + + return next(); + } + + // If user is trying to access any of the non-authentication endpoints ensure that they have + // a valid, non-expired JWT. + if (!to.path.startsWith('/auth')) { + // Check if the JWT has expired. Don't use the exp field, but rather that issued at time + // so that we can adjust how long we want to wait for expiration on both server-side and + // client side without having to wait for older tokens to pass their expiration time if + // we lower it. + if (user === null || compareDate(addHours(dateParse(user.getJWT().iat * 1000), 12), new Date()) < 0) { + return window.location = route('auth.login'); + } + } + + // Continue on through the pipeline. + return next(); +}); + +export default router; diff --git a/resources/assets/scripts/store/index.js b/resources/assets/scripts/store/index.js index 7c7d87cbb..09c707244 100644 --- a/resources/assets/scripts/store/index.js +++ b/resources/assets/scripts/store/index.js @@ -4,7 +4,19 @@ import auth from './modules/auth'; Vue.use(Vuex); -export default new Vuex.Store({ +const store = new Vuex.Store({ strict: process.env.NODE_ENV !== 'production', modules: { auth }, }); + +if (module.hot) { + module.hot.accept(['./modules/auth'], () => { + const newAuthModule = require('./modules/auth').default; + + store.hotUpdate({ + modules: { newAuthModule }, + }); + }); +} + +export default store; diff --git a/routes/auth.php b/routes/auth.php index a6038447b..3ae4967be 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -8,7 +8,7 @@ | Endpoint: /auth | */ -Route::group(['middleware' => 'guest'], function () { +Route::group([], function () { // These routes are defined so that we can continue to reference them programatically. // They all route to the same controller function which passes off to Vuejs. Route::get('/login', 'LoginController@index')->name('auth.login');