Change login handling to automatically redirect a user if their session will need renewal.
This commit is contained in:
parent
24bb8da43d
commit
e7faf979a1
8 changed files with 126 additions and 23 deletions
|
@ -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();
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -12,6 +12,7 @@ return [
|
|||
|
|
||||
*/
|
||||
'key' => env('APP_JWT_KEY'),
|
||||
'lifetime' => env('APP_JWT_LIFETIME', 1440),
|
||||
|
||||
'signer' => \Lcobucci\JWT\Signer\Hmac\Sha256::class,
|
||||
];
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
81
resources/assets/scripts/router.js
Normal file
81
resources/assets/scripts/router.js
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in a new issue