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
|
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();
|
||||||
|
|
|
@ -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([
|
||||||
|
|
|
@ -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,
|
||||||
];
|
];
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
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);
|
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;
|
||||||
|
|
|
@ -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');
|
||||||
|
|
Loading…
Reference in a new issue