Change login handling to automatically redirect a user if their session will need renewal.

This commit is contained in:
Dane Everitt 2018-06-16 14:05:39 -07:00
parent 24bb8da43d
commit e7faf979a1
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
8 changed files with 126 additions and 23 deletions

View file

@ -155,13 +155,15 @@ abstract class AbstractLoginController extends Controller
*/ */
protected function createJsonWebToken(User $user): string protected function createJsonWebToken(User $user): string
{ {
$now = Chronos::now('utc');
$token = $this->builder $token = $this->builder
->setIssuer('Pterodactyl Panel') ->setIssuer('Pterodactyl Panel')
->setAudience(config('app.url')) ->setAudience(config('app.url'))
->setId(str_random(16), true) ->setId(str_random(16), true)
->setIssuedAt(Chronos::now()->getTimestamp()) ->setIssuedAt($now->getTimestamp())
->setNotBefore(Chronos::now()->getTimestamp()) ->setNotBefore($now->getTimestamp())
->setExpiration(Chronos::now()->addSeconds(config('session.lifetime'))->getTimestamp()) ->setExpiration($now->addSeconds(config('jwt.lifetime'))->getTimestamp())
->set('user', (new AccountTransformer())->transform($user)) ->set('user', (new AccountTransformer())->transform($user))
->sign($this->getJWTSigner(), $this->getJWTSigningKey()) ->sign($this->getJWTSigner(), $this->getJWTSigningKey())
->getToken(); ->getToken();

View file

@ -98,13 +98,17 @@ class AuthenticateKey
} }
// Run through the token validation and throw an exception if the token is not valid. // 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 ( if (
$token->getClaim('nbf') > Chronos::now()->getTimestamp() Chronos::createFromTimestampUTC($token->getClaim('nbf'))->gt($now)
|| $token->getClaim('iss') !== 'Pterodactyl Panel' || $token->getClaim('iss') !== 'Pterodactyl Panel'
|| $token->getClaim('aud') !== config('app.url') || $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([ return (new ApiKey)->forceFill([

View file

@ -12,6 +12,7 @@ return [
| |
*/ */
'key' => env('APP_JWT_KEY'), 'key' => env('APP_JWT_KEY'),
'lifetime' => env('APP_JWT_LIFETIME', 1440),
'signer' => \Lcobucci\JWT\Signer\Hmac\Sha256::class, 'signer' => \Lcobucci\JWT\Signer\Hmac\Sha256::class,
]; ];

View file

@ -10,22 +10,21 @@ require('./bootstrap');
import { Ziggy } from './helpers/ziggy'; import { Ziggy } from './helpers/ziggy';
import Locales from './../../../resources/lang/locales'; import Locales from './../../../resources/lang/locales';
import { flash } from './mixins/flash'; import { flash } from './mixins/flash';
import { routes } from './routes';
import store from './store/index.js'; import store from './store/index.js';
import router from './router';
window.events = new Vue; window.events = new Vue;
window.Ziggy = Ziggy; window.Ziggy = Ziggy;
Vue.use(Vuex); Vue.use(Vuex);
Vue.use(VueRouter);
Vue.use(vuexI18n.plugin, store);
const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default; const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default;
Vue.mixin({ methods: { route } }); Vue.mixin({ methods: { route } });
Vue.mixin(flash); Vue.mixin(flash);
Vue.use(VueRouter);
Vue.use(vuexI18n.plugin, store);
Vue.i18n.add('en', Locales.en); Vue.i18n.add('en', Locales.en);
Vue.i18n.set('en'); Vue.i18n.set('en');
@ -33,8 +32,4 @@ if (module.hot) {
module.hot.accept(); module.hot.accept();
} }
const router = new VueRouter({
mode: 'history', routes
});
const app = new Vue({ store, router }).$mount('#pterodactyl'); const app = new Vue({ store, router }).$mount('#pterodactyl');

View file

@ -9,17 +9,19 @@ export default class User {
*/ */
static fromToken(token) { static fromToken(token) {
if (!isString(token)) { if (!isString(token)) {
token = localStorage.getItem('token'); token = this.getToken();
} }
if (!isString(token) || token.length < 1) { if (!isString(token) || token.length < 1) {
return null; return null;
} }
try {
const data = jwtDecode(token); const data = jwtDecode(token);
if (data.user) { if (data.user) {
return new User(data.user); return new User(data.user);
} }
} catch (ex) {}
return null; return null;
} }
@ -29,8 +31,7 @@ export default class User {
* *
* @returns {string | null} * @returns {string | null}
*/ */
static getToken() static getToken() {
{
return localStorage.getItem('token'); return localStorage.getItem('token');
} }
@ -60,4 +61,11 @@ export default class User {
this.last_name = last_name; this.last_name = last_name;
this.language = language; this.language = language;
} }
/**
* Returns the JWT belonging to the current user.
*/
getJWT() {
return jwtDecode(User.getToken());
}
} }

View file

@ -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;

View file

@ -4,7 +4,19 @@ import auth from './modules/auth';
Vue.use(Vuex); Vue.use(Vuex);
export default new Vuex.Store({ const store = new Vuex.Store({
strict: process.env.NODE_ENV !== 'production', strict: process.env.NODE_ENV !== 'production',
modules: { auth }, modules: { auth },
}); });
if (module.hot) {
module.hot.accept(['./modules/auth'], () => {
const newAuthModule = require('./modules/auth').default;
store.hotUpdate({
modules: { newAuthModule },
});
});
}
export default store;

View file

@ -8,7 +8,7 @@
| Endpoint: /auth | Endpoint: /auth
| |
*/ */
Route::group(['middleware' => 'guest'], function () { Route::group([], function () {
// These routes are defined so that we can continue to reference them programatically. // 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. // They all route to the same controller function which passes off to Vuejs.
Route::get('/login', 'LoginController@index')->name('auth.login'); Route::get('/login', 'LoginController@index')->name('auth.login');