Skip to content

Commit e7faf97

Browse files
committed
Change login handling to automatically redirect a user if their session will need renewal.
1 parent 24bb8da commit e7faf97

File tree

8 files changed

+126
-23
lines changed

8 files changed

+126
-23
lines changed

app/Http/Controllers/Auth/AbstractLoginController.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,15 @@ protected function sendLoginResponse(User $user, Request $request): JsonResponse
155155
*/
156156
protected function createJsonWebToken(User $user): string
157157
{
158+
$now = Chronos::now('utc');
159+
158160
$token = $this->builder
159161
->setIssuer('Pterodactyl Panel')
160162
->setAudience(config('app.url'))
161163
->setId(str_random(16), true)
162-
->setIssuedAt(Chronos::now()->getTimestamp())
163-
->setNotBefore(Chronos::now()->getTimestamp())
164-
->setExpiration(Chronos::now()->addSeconds(config('session.lifetime'))->getTimestamp())
164+
->setIssuedAt($now->getTimestamp())
165+
->setNotBefore($now->getTimestamp())
166+
->setExpiration($now->addSeconds(config('jwt.lifetime'))->getTimestamp())
165167
->set('user', (new AccountTransformer())->transform($user))
166168
->sign($this->getJWTSigner(), $this->getJWTSigningKey())
167169
->getToken();

app/Http/Middleware/Api/AuthenticateKey.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,17 @@ protected function authenticateJWT(string $token): ApiKey
9898
}
9999

100100
// Run through the token validation and throw an exception if the token is not valid.
101+
//
102+
// The issued_at time is used for verification in order to allow rapid changing of session
103+
// length on the Panel without having to wait on existing tokens to first expire.
104+
$now = Chronos::now('utc');
101105
if (
102-
$token->getClaim('nbf') > Chronos::now()->getTimestamp()
106+
Chronos::createFromTimestampUTC($token->getClaim('nbf'))->gt($now)
103107
|| $token->getClaim('iss') !== 'Pterodactyl Panel'
104108
|| $token->getClaim('aud') !== config('app.url')
105-
|| $token->getClaim('exp') <= Chronos::now()->getTimestamp()
109+
|| Chronos::createFromTimestampUTC($token->getClaim('iat'))->addMinutes(config('jwt.lifetime'))->lte($now)
106110
) {
107-
throw new AccessDeniedHttpException;
111+
throw new AccessDeniedHttpException('The authentication parameters provided are not valid for accessing this resource.');
108112
}
109113

110114
return (new ApiKey)->forceFill([

config/jwt.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
|
1313
*/
1414
'key' => env('APP_JWT_KEY'),
15+
'lifetime' => env('APP_JWT_LIFETIME', 1440),
1516

1617
'signer' => \Lcobucci\JWT\Signer\Hmac\Sha256::class,
1718
];

resources/assets/scripts/app.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,26 @@ require('./bootstrap');
1010
import { Ziggy } from './helpers/ziggy';
1111
import Locales from './../../../resources/lang/locales';
1212
import { flash } from './mixins/flash';
13-
import { routes } from './routes';
1413
import store from './store/index.js';
14+
import router from './router';
1515

1616
window.events = new Vue;
1717
window.Ziggy = Ziggy;
1818

1919
Vue.use(Vuex);
20+
Vue.use(VueRouter);
21+
Vue.use(vuexI18n.plugin, store);
2022

2123
const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default;
2224

2325
Vue.mixin({ methods: { route } });
2426
Vue.mixin(flash);
2527

26-
Vue.use(VueRouter);
27-
Vue.use(vuexI18n.plugin, store);
28-
2928
Vue.i18n.add('en', Locales.en);
3029
Vue.i18n.set('en');
3130

3231
if (module.hot) {
3332
module.hot.accept();
3433
}
3534

36-
const router = new VueRouter({
37-
mode: 'history', routes
38-
});
39-
4035
const app = new Vue({ store, router }).$mount('#pterodactyl');

resources/assets/scripts/models/user.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@ export default class User {
99
*/
1010
static fromToken(token) {
1111
if (!isString(token)) {
12-
token = localStorage.getItem('token');
12+
token = this.getToken();
1313
}
1414

1515
if (!isString(token) || token.length < 1) {
1616
return null;
1717
}
1818

19-
const data = jwtDecode(token);
20-
if (data.user) {
21-
return new User(data.user);
22-
}
19+
try {
20+
const data = jwtDecode(token);
21+
if (data.user) {
22+
return new User(data.user);
23+
}
24+
} catch (ex) {}
2325

2426
return null;
2527
}
@@ -29,8 +31,7 @@ export default class User {
2931
*
3032
* @returns {string | null}
3133
*/
32-
static getToken()
33-
{
34+
static getToken() {
3435
return localStorage.getItem('token');
3536
}
3637

@@ -60,4 +61,11 @@ export default class User {
6061
this.last_name = last_name;
6162
this.language = language;
6263
}
64+
65+
/**
66+
* Returns the JWT belonging to the current user.
67+
*/
68+
getJWT() {
69+
return jwtDecode(User.getToken());
70+
}
6371
}

resources/assets/scripts/router.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import VueRouter from 'vue-router';
2+
import store from './store/index';
3+
import compareDate from 'date-fns/compare_asc'
4+
import addHours from 'date-fns/add_hours'
5+
import dateParse from 'date-fns/parse'
6+
const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default;
7+
8+
// Base Vuejs Templates
9+
import Login from './components/auth/Login';
10+
import Dashboard from './components/dashboard/Dashboard';
11+
import Account from './components/dashboard/Account';
12+
import ResetPassword from './components/auth/ResetPassword';
13+
14+
const routes = [
15+
{ name: 'login', path: '/auth/login', component: Login },
16+
{ name: 'forgot-password', path: '/auth/password', component: Login },
17+
{ name: 'checkpoint', path: '/auth/checkpoint', component: Login },
18+
{
19+
name: 'reset-password',
20+
path: '/auth/password/reset/:token',
21+
component: ResetPassword,
22+
props: function (route) {
23+
return { token: route.params.token, email: route.query.email || '' };
24+
}
25+
},
26+
27+
{ name : 'dashboard', path: '/', component: Dashboard },
28+
{ name : 'account', path: '/account', component: Account },
29+
{ name : 'account.api', path: '/account/api', component: Account },
30+
{ name : 'account.security', path: '/account/security', component: Account },
31+
32+
{
33+
name: 'server',
34+
path: '/server/:id',
35+
// component: Server,
36+
// children: [
37+
// { path: 'files', component: ServerFileManager }
38+
// ],
39+
}
40+
];
41+
42+
const router = new VueRouter({
43+
mode: 'history', routes
44+
});
45+
46+
// Redirect the user to the login page if they try to access a protected route and
47+
// have no JWT or the JWT is expired and wouldn't be accepted by the Panel.
48+
router.beforeEach((to, from, next) => {
49+
if (to.path === route('auth.logout')) {
50+
return window.location = route('auth.logout');
51+
}
52+
53+
const user = store.getters['auth/getUser'];
54+
55+
// If user is trying to access the authentication endpoints but is already authenticated
56+
// don't try to load them, just send the user to the dashboard.
57+
if (to.path.startsWith('/auth')) {
58+
if (user !== null && compareDate(addHours(dateParse(user.getJWT().iat * 1000), 12), new Date()) >= 0) {
59+
return window.location = '/';
60+
}
61+
62+
return next();
63+
}
64+
65+
// If user is trying to access any of the non-authentication endpoints ensure that they have
66+
// a valid, non-expired JWT.
67+
if (!to.path.startsWith('/auth')) {
68+
// Check if the JWT has expired. Don't use the exp field, but rather that issued at time
69+
// so that we can adjust how long we want to wait for expiration on both server-side and
70+
// client side without having to wait for older tokens to pass their expiration time if
71+
// we lower it.
72+
if (user === null || compareDate(addHours(dateParse(user.getJWT().iat * 1000), 12), new Date()) < 0) {
73+
return window.location = route('auth.login');
74+
}
75+
}
76+
77+
// Continue on through the pipeline.
78+
return next();
79+
});
80+
81+
export default router;

resources/assets/scripts/store/index.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,19 @@ import auth from './modules/auth';
44

55
Vue.use(Vuex);
66

7-
export default new Vuex.Store({
7+
const store = new Vuex.Store({
88
strict: process.env.NODE_ENV !== 'production',
99
modules: { auth },
1010
});
11+
12+
if (module.hot) {
13+
module.hot.accept(['./modules/auth'], () => {
14+
const newAuthModule = require('./modules/auth').default;
15+
16+
store.hotUpdate({
17+
modules: { newAuthModule },
18+
});
19+
});
20+
}
21+
22+
export default store;

routes/auth.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
| Endpoint: /auth
99
|
1010
*/
11-
Route::group(['middleware' => 'guest'], function () {
11+
Route::group([], function () {
1212
// These routes are defined so that we can continue to reference them programatically.
1313
// They all route to the same controller function which passes off to Vuejs.
1414
Route::get('/login', 'LoginController@index')->name('auth.login');

0 commit comments

Comments
 (0)