Skip to content

Commit 6336e51

Browse files
committed
Strip out JWT usage and use cookies to track the currently logged in user
1 parent a7fae86 commit 6336e51

File tree

9 files changed

+44
-144
lines changed

9 files changed

+44
-144
lines changed

app/Http/Controllers/Auth/AbstractLoginController.php

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
namespace Pterodactyl\Http\Controllers\Auth;
44

5-
use Cake\Chronos\Chronos;
6-
use Lcobucci\JWT\Builder;
75
use Illuminate\Http\Request;
86
use Pterodactyl\Models\User;
97
use Illuminate\Auth\AuthManager;
@@ -16,7 +14,6 @@
1614
use Illuminate\Contracts\Encryption\Encrypter;
1715
use Illuminate\Foundation\Auth\AuthenticatesUsers;
1816
use Pterodactyl\Traits\Helpers\ProvidesJWTServices;
19-
use Pterodactyl\Transformers\Api\Client\AccountTransformer;
2017
use Illuminate\Contracts\Cache\Repository as CacheRepository;
2118
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
2219

@@ -29,11 +26,6 @@ abstract class AbstractLoginController extends Controller
2926
*/
3027
protected $auth;
3128

32-
/**
33-
* @var \Lcobucci\JWT\Builder
34-
*/
35-
protected $builder;
36-
3729
/**
3830
* @var \Illuminate\Contracts\Cache\Repository
3931
*/
@@ -79,22 +71,19 @@ abstract class AbstractLoginController extends Controller
7971
* LoginController constructor.
8072
*
8173
* @param \Illuminate\Auth\AuthManager $auth
82-
* @param \Lcobucci\JWT\Builder $builder
8374
* @param \Illuminate\Contracts\Cache\Repository $cache
8475
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
8576
* @param \PragmaRX\Google2FA\Google2FA $google2FA
8677
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
8778
*/
8879
public function __construct(
8980
AuthManager $auth,
90-
Builder $builder,
9181
CacheRepository $cache,
9282
Encrypter $encrypter,
9383
Google2FA $google2FA,
9484
UserRepositoryInterface $repository
9585
) {
9686
$this->auth = $auth;
97-
$this->builder = $builder;
9887
$this->cache = $cache;
9988
$this->encrypter = $encrypter;
10089
$this->google2FA = $google2FA;
@@ -143,34 +132,10 @@ protected function sendLoginResponse(User $user, Request $request): JsonResponse
143132
return response()->json([
144133
'complete' => true,
145134
'intended' => $this->redirectPath(),
146-
'jwt' => $this->createJsonWebToken($user),
135+
'user' => $user->toVueObject(),
147136
]);
148137
}
149138

150-
/**
151-
* Create a new JWT for the request and sign it using the signing key.
152-
*
153-
* @param User $user
154-
* @return string
155-
*/
156-
protected function createJsonWebToken(User $user): string
157-
{
158-
$now = Chronos::now('utc');
159-
160-
$token = $this->builder
161-
->setIssuer('Pterodactyl Panel')
162-
->setAudience(config('app.url'))
163-
->setId(str_random(16), true)
164-
->setIssuedAt($now->getTimestamp())
165-
->setNotBefore($now->getTimestamp())
166-
->setExpiration($now->addSeconds(config('jwt.lifetime'))->getTimestamp())
167-
->set('user', (new AccountTransformer())->transform($user))
168-
->sign($this->getJWTSigner(), $this->getJWTSigningKey())
169-
->getToken();
170-
171-
return $token->__toString();
172-
}
173-
174139
/**
175140
* Determine if the user is logging in using an email or username,.
176141
*

app/Http/Kernel.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class Kernel extends HttpKernel
4949
*/
5050
protected $middleware = [
5151
CheckForMaintenanceMode::class,
52+
EncryptCookies::class,
5253
ValidatePostSize::class,
5354
TrimStrings::class,
5455
ConvertEmptyStringsToNull::class,
@@ -62,7 +63,6 @@ class Kernel extends HttpKernel
6263
*/
6364
protected $middlewareGroups = [
6465
'web' => [
65-
EncryptCookies::class,
6666
AddQueuedCookiesToResponse::class,
6767
StartSession::class,
6868
AuthenticateSession::class,
@@ -82,8 +82,10 @@ class Kernel extends HttpKernel
8282
],
8383
'client-api' => [
8484
'throttle:240,1',
85-
SubstituteClientApiBindings::class,
85+
StartSession::class,
8686
SetSessionDriver::class,
87+
AuthenticateSession::class,
88+
SubstituteClientApiBindings::class,
8789
'api..key:' . ApiKey::TYPE_ACCOUNT,
8890
AuthenticateIPAccess::class,
8991
],

app/Http/Middleware/Api/AuthenticateKey.php

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
namespace Pterodactyl\Http\Middleware\Api;
44

55
use Closure;
6-
use Lcobucci\JWT\Parser;
76
use Cake\Chronos\Chronos;
87
use Illuminate\Http\Request;
8+
use Pterodactyl\Models\User;
99
use Pterodactyl\Models\ApiKey;
1010
use Illuminate\Auth\AuthManager;
1111
use Illuminate\Contracts\Encryption\Encrypter;
@@ -62,15 +62,19 @@ public function __construct(ApiKeyRepositoryInterface $repository, AuthManager $
6262
*/
6363
public function handle(Request $request, Closure $next, int $keyType)
6464
{
65-
if (is_null($request->bearerToken())) {
65+
if (is_null($request->bearerToken()) && is_null($request->user())) {
6666
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
6767
}
6868

6969
$raw = $request->bearerToken();
7070

71-
// This is an internal JWT, treat it differently to get the correct user before passing it along.
72-
if (strlen($raw) > ApiKey::IDENTIFIER_LENGTH + ApiKey::KEY_LENGTH) {
73-
$model = $this->authenticateJWT($raw);
71+
// This is a request coming through using cookies, we have an authenticated user not using
72+
// an API key. Make some fake API key models and continue on through the process.
73+
if (empty($raw) && $request->user() instanceof User) {
74+
$model = new ApiKey([
75+
'user_id' => $request->user()->id,
76+
'key_type' => ApiKey::TYPE_ACCOUNT,
77+
]);
7478
} else {
7579
$model = $this->authenticateApiKey($raw, $keyType);
7680
}
@@ -81,42 +85,6 @@ public function handle(Request $request, Closure $next, int $keyType)
8185
return $next($request);
8286
}
8387

84-
/**
85-
* Authenticate an API request using a JWT rather than an API key.
86-
*
87-
* @param string $token
88-
* @return \Pterodactyl\Models\ApiKey
89-
*/
90-
protected function authenticateJWT(string $token): ApiKey
91-
{
92-
$token = (new Parser)->parse($token);
93-
94-
// If the key cannot be verified throw an exception to indicate that a bad
95-
// authorization header was provided.
96-
if (! $token->verify($this->getJWTSigner(), $this->getJWTSigningKey())) {
97-
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
98-
}
99-
100-
// 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');
105-
if (
106-
Chronos::createFromTimestampUTC($token->getClaim('nbf'))->gt($now)
107-
|| $token->getClaim('iss') !== 'Pterodactyl Panel'
108-
|| $token->getClaim('aud') !== config('app.url')
109-
|| Chronos::createFromTimestampUTC($token->getClaim('iat'))->addMinutes(config('jwt.lifetime'))->lte($now)
110-
) {
111-
throw new AccessDeniedHttpException('The authentication parameters provided are not valid for accessing this resource.');
112-
}
113-
114-
return (new ApiKey)->forceFill([
115-
'user_id' => object_get($token->getClaim('user'), 'id', 0),
116-
'key_type' => ApiKey::TYPE_ACCOUNT,
117-
]);
118-
}
119-
12088
/**
12189
* Authenticate an API key.
12290
*

app/Models/User.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Sofa\Eloquence\Eloquence;
66
use Sofa\Eloquence\Validable;
77
use Pterodactyl\Rules\Username;
8+
use Illuminate\Support\Collection;
89
use Illuminate\Validation\Rules\In;
910
use Illuminate\Auth\Authenticatable;
1011
use Illuminate\Database\Eloquent\Model;
@@ -177,6 +178,16 @@ protected static function gatherRules()
177178
return $rules;
178179
}
179180

181+
/**
182+
* Return the user model in a format that can be passed over to Vue templates.
183+
*
184+
* @return array
185+
*/
186+
public function toVueObject(): array
187+
{
188+
return (new Collection($this->toArray()))->except(['id', 'external_id'])->toArray();
189+
}
190+
180191
/**
181192
* Send the password reset notification.
182193
*

resources/assets/scripts/helpers/axios.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import User from './../models/user';
2-
31
/**
42
* We'll load the axios HTTP library which allows us to easily issue requests
53
* to our Laravel back-end. This library automatically handles sending the
@@ -9,7 +7,6 @@ import User from './../models/user';
97
let axios = require('axios');
108
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
119
axios.defaults.headers.common['Accept'] = 'application/json';
12-
axios.defaults.headers.common['Authorization'] = `Bearer ${User.getToken()}`;
1310

1411
if (typeof phpdebugbar !== 'undefined') {
1512
axios.interceptors.response.use(function (response) {
Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,4 @@
1-
import isString from 'lodash/isString';
2-
import jwtDecode from 'jwt-decode';
3-
41
export default class User {
5-
/**
6-
* Get a new user model from the JWT.
7-
*
8-
* @return {User | null}
9-
*/
10-
static fromToken(token) {
11-
if (!isString(token)) {
12-
token = this.getToken();
13-
}
14-
15-
if (!isString(token) || token.length < 1) {
16-
return null;
17-
}
18-
19-
try {
20-
const data = jwtDecode(token);
21-
if (data.user) {
22-
return new User(data.user);
23-
}
24-
} catch (ex) {}
25-
26-
return null;
27-
}
28-
29-
/**
30-
* Return the JWT for the authenticated user.
31-
*
32-
* @returns {string | null}
33-
*/
34-
static getToken() {
35-
return localStorage.getItem('token');
36-
}
37-
382
/**
393
* Create a new user model.
404
*
@@ -46,26 +10,19 @@ export default class User {
4610
* @param {String} language
4711
*/
4812
constructor({
49-
admin,
13+
root_admin,
5014
username,
5115
email,
5216
first_name,
5317
last_name,
5418
language,
5519
}) {
56-
this.admin = admin;
20+
this.admin = root_admin;
5721
this.username = username;
5822
this.email = email;
5923
this.name = `${first_name} ${last_name}`;
6024
this.first_name = first_name;
6125
this.last_name = last_name;
6226
this.language = language;
6327
}
64-
65-
/**
66-
* Returns the JWT belonging to the current user.
67-
*/
68-
getJWT() {
69-
return jwtDecode(User.getToken());
70-
}
7128
}

resources/assets/scripts/router.js

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Login from './components/auth/Login';
1010
import Dashboard from './components/dashboard/Dashboard';
1111
import Account from './components/dashboard/Account';
1212
import ResetPassword from './components/auth/ResetPassword';
13+
import User from './models/user';
1314

1415
const routes = [
1516
{ name: 'login', path: '/auth/login', component: Login },
@@ -52,17 +53,10 @@ router.beforeEach((to, from, next) => {
5253

5354
const user = store.getters['auth/getUser'];
5455

55-
// If user is trying to access any of the non-authentication endpoints ensure that they have
56-
// a valid, non-expired JWT.
57-
if (!to.path.startsWith('/auth')) {
58-
// Check if the JWT has expired. Don't use the exp field, but rather that issued at time
59-
// so that we can adjust how long we want to wait for expiration on both server-side and
60-
// client side without having to wait for older tokens to pass their expiration time if
61-
// we lower it.
62-
if (user === null || compareDate(addHours(dateParse(user.getJWT().iat * 1000), 12), new Date()) < 0) {
63-
store.commit('auth/logout');
64-
return window.location = route('auth.logout');
65-
}
56+
// Check that if we're accessing a non-auth route that a user exists on the page.
57+
if (!to.path.startsWith('/auth') && !(user instanceof User)) {
58+
store.commit('auth/logout');
59+
return window.location = route('auth.logout');
6660
}
6761

6862
// Continue on through the pipeline.

resources/assets/scripts/store/modules/auth.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const route = require('./../../../../../vendor/tightenco/ziggy/src/js/route').de
55
export default {
66
namespaced: true,
77
state: {
8-
user: User.fromToken(),
8+
user: typeof window.PterodactylUser === 'object' ? new User(window.PterodactylUser) : null,
99
},
1010
getters: {
1111
/**
@@ -41,7 +41,7 @@ export default {
4141
}
4242

4343
if (response.data.complete) {
44-
commit('login', {jwt: response.data.jwt});
44+
commit('login', response.data.user);
4545
return resolve({
4646
complete: true,
4747
intended: response.data.intended,
@@ -86,12 +86,10 @@ export default {
8686
setEmail: function (state, email) {
8787
state.user.email = email;
8888
},
89-
login: function (state, {jwt}) {
90-
localStorage.setItem('token', jwt);
91-
state.user = User.fromToken(jwt);
89+
login: function (state, data) {
90+
state.user = new User(data);
9291
},
9392
logout: function (state) {
94-
localStorage.removeItem('token');
9593
state.user = null;
9694
},
9795
},

resources/themes/pterodactyl/templates/wrapper.blade.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99
<meta name="csrf-token" content="{{ csrf_token() }}">
1010
@show
1111

12+
@section('user-data')
13+
@if(!is_null(Auth::user()))
14+
<script>
15+
window.PterodactylUser = {!! json_encode(Auth::user()->toVueObject()) !!}
16+
</script>
17+
@endif
18+
@show
19+
1220
@section('assets')
1321
{!! $asset->css('main.css') !!}
1422
@show

0 commit comments

Comments
 (0)