diff --git a/app/Http/Controllers/Auth/AbstractLoginController.php b/app/Http/Controllers/Auth/AbstractLoginController.php index 58a48dfe..de8e275d 100644 --- a/app/Http/Controllers/Auth/AbstractLoginController.php +++ b/app/Http/Controllers/Auth/AbstractLoginController.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Controllers\Auth; +use Cake\Chronos\Chronos; use Lcobucci\JWT\Builder; use Illuminate\Http\Request; use Pterodactyl\Models\User; @@ -139,19 +140,35 @@ abstract class AbstractLoginController extends Controller $this->auth->guard()->login($user, true); - debug($request->cookies->all()); - return response()->json([ 'complete' => true, 'intended' => $this->redirectPath(), - 'cookie' => [ - 'name' => config('session.cookie'), - 'value' => $this->encrypter->encrypt($request->cookie(config('session.cookie'))), - ], - 'user' => (new AccountTransformer())->transform($user), + 'jwt' => $this->createJsonWebToken($user), ]); } + /** + * Create a new JWT for the request and sign it using the signing key. + * + * @param User $user + * @return string + */ + protected function createJsonWebToken(User $user): string + { + $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()) + ->set('user', (new AccountTransformer())->transform($user)) + ->sign($this->getJWTSigner(), $this->getJWTSigningKey()) + ->getToken(); + + return $token->__toString(); + } + /** * Determine if the user is logging in using an email or username,. * diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index dc7837f9..894d0938 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -80,8 +80,6 @@ class Kernel extends HttpKernel ], 'client-api' => [ 'throttle:240,1', - EncryptCookies::class, - StartSession::class, SubstituteClientApiBindings::class, SetSessionDriver::class, 'api..key:' . ApiKey::TYPE_ACCOUNT, diff --git a/app/Http/Middleware/Api/AuthenticateKey.php b/app/Http/Middleware/Api/AuthenticateKey.php index e5dd9115..0b6b23f7 100644 --- a/app/Http/Middleware/Api/AuthenticateKey.php +++ b/app/Http/Middleware/Api/AuthenticateKey.php @@ -5,7 +5,6 @@ namespace Pterodactyl\Http\Middleware\Api; use Closure; use Lcobucci\JWT\Parser; use Cake\Chronos\Chronos; -use Illuminate\Support\Str; use Illuminate\Http\Request; use Pterodactyl\Models\ApiKey; use Illuminate\Auth\AuthManager; @@ -64,24 +63,19 @@ class AuthenticateKey public function handle(Request $request, Closure $next, int $keyType) { if (is_null($request->bearerToken())) { - if (! Str::startsWith($request->route()->getName(), ['api.client']) && ! $request->user()) { - throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); - } + throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); } - if (is_null($request->bearerToken())) { - $model = (new ApiKey)->forceFill([ - 'user_id' => $request->user()->id, - 'key_type' => ApiKey::TYPE_ACCOUNT, - ]); - } + $raw = $request->bearerToken(); - if (! isset($model)) { - $raw = $request->bearerToken(); + // This is an internal JWT, treat it differently to get the correct user before passing it along. + if (strlen($raw) > ApiKey::IDENTIFIER_LENGTH + ApiKey::KEY_LENGTH) { + $model = $this->authenticateJWT($raw); + } else { $model = $this->authenticateApiKey($raw, $keyType); - $this->auth->guard()->loginUsingId($model->user_id); } + $this->auth->guard()->loginUsingId($model->user_id); $request->attributes->set('api_key', $model); return $next($request); @@ -103,6 +97,16 @@ class AuthenticateKey throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); } + // Run through the token validation and throw an exception if the token is not valid. + if ( + $token->getClaim('nbf') > Chronos::now()->getTimestamp() + || $token->getClaim('iss') !== 'Pterodactyl Panel' + || $token->getClaim('aud') !== config('app.url') + || $token->getClaim('exp') <= Chronos::now()->getTimestamp() + ) { + throw new AccessDeniedHttpException; + } + return (new ApiKey)->forceFill([ 'user_id' => object_get($token->getClaim('user'), 'id', 0), 'key_type' => ApiKey::TYPE_ACCOUNT, diff --git a/app/Transformers/Api/Client/AccountTransformer.php b/app/Transformers/Api/Client/AccountTransformer.php index 30bed0d2..2cdc92e3 100644 --- a/app/Transformers/Api/Client/AccountTransformer.php +++ b/app/Transformers/Api/Client/AccountTransformer.php @@ -25,6 +25,7 @@ class AccountTransformer extends BaseClientTransformer public function transform(User $model) { return [ + 'id' => $model->id, 'admin' => $model->root_admin, 'username' => $model->username, 'email' => $model->email, diff --git a/resources/assets/scripts/helpers/axios.js b/resources/assets/scripts/helpers/axios.js index 37617911..9fd0cbbc 100644 --- a/resources/assets/scripts/helpers/axios.js +++ b/resources/assets/scripts/helpers/axios.js @@ -1,3 +1,5 @@ +import User from './../models/user'; + /** * We'll load the axios HTTP library which allows us to easily issue requests * to our Laravel back-end. This library automatically handles sending the @@ -7,6 +9,7 @@ let axios = require('axios'); axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.defaults.headers.common['Accept'] = 'application/json'; +axios.defaults.headers.common['Authorization'] = `Bearer ${User.getToken()}`; if (typeof phpdebugbar !== 'undefined') { axios.interceptors.response.use(function (response) { diff --git a/resources/assets/scripts/models/user.js b/resources/assets/scripts/models/user.js index 8da7353f..149afd81 100644 --- a/resources/assets/scripts/models/user.js +++ b/resources/assets/scripts/models/user.js @@ -1,21 +1,37 @@ -import axios from './../helpers/axios'; +import isString from 'lodash/isString'; +import jwtDecode from 'jwt-decode'; export default class User { /** - * Get a new user model by hitting the Panel API using the authentication token - * provided. If no user can be retrieved null will be returned. + * Get a new user model from the JWT. * - * @return {User|null} + * @return {User | null} */ - static fromCookie() { - axios.get('/api/client/account') - .then(response => { - return new User(response.data.attributes); - }) - .catch(err => { - console.error(err); - return null; - }); + static fromToken(token) { + if (!isString(token)) { + token = localStorage.getItem('token'); + } + + if (!isString(token) || token.length < 1) { + return null; + } + + const data = jwtDecode(token); + if (data.user) { + return new User(data.user); + } + + return null; + } + + /** + * Return the JWT for the authenticated user. + * + * @returns {string | null} + */ + static getToken() + { + return localStorage.getItem('token'); } /** diff --git a/resources/assets/scripts/store/modules/auth.js b/resources/assets/scripts/store/modules/auth.js index fb12aaea..7ccd4372 100644 --- a/resources/assets/scripts/store/modules/auth.js +++ b/resources/assets/scripts/store/modules/auth.js @@ -4,7 +4,7 @@ const route = require('./../../../../../vendor/tightenco/ziggy/src/js/route').de export default { namespaced: true, state: { - user: null, + user: User.fromToken(), }, getters: { /** @@ -32,7 +32,7 @@ export default { } if (response.data.complete) { - commit('login', {cookie: response.data.cookie, user: response.data.user}); + commit('login', {jwt: response.data.jwt}); return resolve({ complete: true, intended: response.data.intended, @@ -59,12 +59,9 @@ export default { }, }, mutations: { - login: function (state, {cookie, user}) { - state.user = new User(user); - localStorage.setItem('token', JSON.stringify({ - name: cookie.name, - value: cookie.value, - })); + login: function (state, {jwt}) { + localStorage.setItem('token', jwt); + state.user = User.fromToken(jwt); }, logout: function (state) { localStorage.removeItem('token'); diff --git a/webpack.config.js b/webpack.config.js index 68ce9aac..47fb82f3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -65,7 +65,7 @@ const productionPlugins = [ module.exports = { mode: process.env.NODE_ENV, - devtool: process.env.NODE_ENV === 'production' ? false : 'eval-source-map', + devtool: process.env.NODE_ENV === 'production' ? false : 'source-map', performance: { hints: false, },