diff --git a/app/Http/Controllers/Auth/AbstractLoginController.php b/app/Http/Controllers/Auth/AbstractLoginController.php index 0db4bd752..150841fab 100644 --- a/app/Http/Controllers/Auth/AbstractLoginController.php +++ b/app/Http/Controllers/Auth/AbstractLoginController.php @@ -2,7 +2,10 @@ namespace Pterodactyl\Http\Controllers\Auth; +use Cake\Chronos\Chronos; +use Lcobucci\JWT\Builder; use Illuminate\Http\Request; +use Pterodactyl\Models\User; use Illuminate\Auth\AuthManager; use Illuminate\Http\JsonResponse; use PragmaRX\Google2FA\Google2FA; @@ -12,18 +15,24 @@ use Pterodactyl\Http\Controllers\Controller; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Foundation\Auth\AuthenticatesUsers; +use Pterodactyl\Traits\Helpers\ProvidesJWTServices; use Illuminate\Contracts\Cache\Repository as CacheRepository; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; abstract class AbstractLoginController extends Controller { - use AuthenticatesUsers; + use AuthenticatesUsers, ProvidesJWTServices; /** * @var \Illuminate\Auth\AuthManager */ protected $auth; + /** + * @var \Lcobucci\JWT\Builder + */ + protected $builder; + /** * @var \Illuminate\Contracts\Cache\Repository */ @@ -69,6 +78,7 @@ abstract class AbstractLoginController extends Controller * LoginController constructor. * * @param \Illuminate\Auth\AuthManager $auth + * @param \Lcobucci\JWT\Builder $builder * @param \Illuminate\Contracts\Cache\Repository $cache * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \PragmaRX\Google2FA\Google2FA $google2FA @@ -76,12 +86,14 @@ abstract class AbstractLoginController extends Controller */ public function __construct( AuthManager $auth, + Builder $builder, CacheRepository $cache, Encrypter $encrypter, Google2FA $google2FA, UserRepositoryInterface $repository ) { $this->auth = $auth; + $this->builder = $builder; $this->cache = $cache; $this->encrypter = $encrypter; $this->google2FA = $google2FA; @@ -116,19 +128,34 @@ abstract class AbstractLoginController extends Controller /** * Send the response after the user was authenticated. * + * @param \Pterodactyl\Models\User $user * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse */ - protected function sendLoginResponse(Request $request): JsonResponse + protected function sendLoginResponse(User $user, Request $request): JsonResponse { $request->session()->regenerate(); - $this->clearLoginAttempts($request); - return $this->authenticated($request, $this->guard()->user()) - ?: response()->json([ - 'intended' => $this->redirectPath(), - ]); + $token = $this->builder->setIssuer(config('app.url')) + ->setAudience(config('app.url')) + ->setId(str_random(12), true) + ->setIssuedAt(Chronos::now()->getTimestamp()) + ->setNotBefore(Chronos::now()->getTimestamp()) + ->setExpiration(Chronos::now()->addSeconds(config('session.lifetime'))->getTimestamp()) + ->set('user', $user->only([ + 'id', 'uuid', 'username', 'email', 'name_first', 'name_last', 'language', 'root_admin', + ])) + ->sign($this->getJWTSigner(), $this->getJWTSigningKey()) + ->getToken(); + + $this->auth->guard()->login($user, true); + + return response()->json([ + 'complete' => true, + 'intended' => $this->redirectPath(), + 'token' => $token->__toString(), + ]); } /** diff --git a/app/Http/Controllers/Auth/LoginCheckpointController.php b/app/Http/Controllers/Auth/LoginCheckpointController.php index 2f5d0a28f..33aa3e4a1 100644 --- a/app/Http/Controllers/Auth/LoginCheckpointController.php +++ b/app/Http/Controllers/Auth/LoginCheckpointController.php @@ -39,8 +39,6 @@ class LoginCheckpointController extends AbstractLoginController return $this->sendFailedLoginResponse($request, $user); } - $this->auth->guard()->login($user, true); - - return $this->sendLoginResponse($request); + return $this->sendLoginResponse($user, $request); } } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 96549a628..9707519b7 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -2,11 +2,9 @@ namespace Pterodactyl\Http\Controllers\Auth; -use Lcobucci\JWT\Builder; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use Illuminate\Contracts\View\View; -use Lcobucci\JWT\Signer\Hmac\Sha256; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; class LoginController extends AbstractLoginController @@ -65,26 +63,12 @@ class LoginController extends AbstractLoginController 'request_ip' => $request->ip(), ], 5); - return response()->json(['complete' => false, 'login_token' => $token]); + return response()->json([ + 'complete' => false, + 'login_token' => $token, + ]); } - $signer = new Sha256(); - $token = (new Builder)->setIssuer('http://pterodactyl.local') - ->setAudience('http://pterodactyl.local') - ->setId(str_random(12), true) - ->setIssuedAt(time()) - ->setNotBefore(time()) - ->setExpiration(time() + 3600) - ->set('uid', $user->id) - ->sign($signer, env('APP_JWT_KEY')) - ->getToken(); - - $this->auth->guard()->login($user, true); - - return response()->json([ - 'complete' => true, - 'intended' => $this->redirectPath(), - 'token' => $token->__toString(), - ]); + return $this->sendLoginResponse($user, $request); } } diff --git a/app/Http/Middleware/Api/AuthenticateKey.php b/app/Http/Middleware/Api/AuthenticateKey.php index 3ae04f6fe..998eb378c 100644 --- a/app/Http/Middleware/Api/AuthenticateKey.php +++ b/app/Http/Middleware/Api/AuthenticateKey.php @@ -9,6 +9,7 @@ use Illuminate\Http\Request; use Pterodactyl\Models\ApiKey; use Illuminate\Auth\AuthManager; use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Traits\Helpers\ProvidesJWTServices; use Symfony\Component\HttpKernel\Exception\HttpException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; @@ -16,6 +17,8 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class AuthenticateKey { + use ProvidesJWTServices; + /** * @var \Illuminate\Auth\AuthManager */ @@ -65,24 +68,55 @@ class AuthenticateKey $raw = $request->bearerToken(); - // This is an internal JWT, treat it differently to get the correct user - // before passing it along. + // 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) { - $token = (new Parser)->parse($raw); - - $model = (new ApiKey)->fill([ - 'user_id' => $token->getClaim('uid'), - 'key_type' => ApiKey::TYPE_ACCOUNT, - ]); - - $this->auth->guard()->loginUsingId($token->getClaim('uid')); - $request->attributes->set('api_key', $model); - - return $next($request); + $model = $this->authenticateJWT($raw); + } else { + $model = $this->authenticateApiKey($raw, $keyType); } - $identifier = substr($raw, 0, ApiKey::IDENTIFIER_LENGTH); - $token = substr($raw, ApiKey::IDENTIFIER_LENGTH); + $this->auth->guard()->loginUsingId($model->user_id); + $request->attributes->set('api_key', $model); + + return $next($request); + } + + /** + * Authenticate an API request using a JWT rather than an API key. + * + * @param string $token + * @return \Pterodactyl\Models\ApiKey + */ + protected function authenticateJWT(string $token): ApiKey + { + $token = (new Parser)->parse($token); + + // If the key cannot be verified throw an exception to indicate that a bad + // authorization header was provided. + if (! $token->verify($this->getJWTSigner(), $this->getJWTSigningKey())) { + throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); + } + + return (new ApiKey)->forceFill([ + 'user_id' => object_get($token->getClaim('user'), 'id', 0), + 'key_type' => ApiKey::TYPE_ACCOUNT, + ]); + } + + /** + * Authenticate an API key. + * + * @param string $key + * @param int $keyType + * @return \Pterodactyl\Models\ApiKey + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + protected function authenticateApiKey(string $key, int $keyType): ApiKey + { + $identifier = substr($key, 0, ApiKey::IDENTIFIER_LENGTH); + $token = substr($key, ApiKey::IDENTIFIER_LENGTH); try { $model = $this->repository->findFirstWhere([ @@ -97,10 +131,8 @@ class AuthenticateKey throw new AccessDeniedHttpException; } - $this->auth->guard()->loginUsingId($model->user_id); - $request->attributes->set('api_key', $model); $this->repository->withoutFreshModel()->update($model->id, ['last_used_at' => Chronos::now()]); - return $next($request); + return $model; } } diff --git a/app/Traits/Helpers/ProvidesJWTServices.php b/app/Traits/Helpers/ProvidesJWTServices.php new file mode 100644 index 000000000..1c5726fbd --- /dev/null +++ b/app/Traits/Helpers/ProvidesJWTServices.php @@ -0,0 +1,36 @@ +get('jwt.key', ''); + if (Str::startsWith($key, 'base64:')) { + $key = base64_decode(substr($key, 7)); + } + + return $key; + } + + /** + * Provide the signing algo to use for JWT. + * + * @return \Lcobucci\JWT\Signer + */ + public function getJWTSigner(): Signer + { + $class = config()->get('jwt.signer'); + + return new $class; + } +} diff --git a/config/jwt.php b/config/jwt.php new file mode 100644 index 000000000..6bd04a635 --- /dev/null +++ b/config/jwt.php @@ -0,0 +1,17 @@ + env('APP_JWT_KEY'), + + 'signer' => \Lcobucci\JWT\Signer\Hmac\Sha256::class, +]; diff --git a/resources/assets/scripts/models/allocation.js b/resources/assets/scripts/models/allocation.js deleted file mode 100644 index 27f2f62bd..000000000 --- a/resources/assets/scripts/models/allocation.js +++ /dev/null @@ -1,19 +0,0 @@ -const Allocation = function () { - this.ip = null; - this.port = null; -}; - -/** - * Return a new allocation model. - * - * @param obj - * @returns {Allocation} - */ -Allocation.prototype.fill = function (obj) { - this.ip = obj.ip || null; - this.port = obj.port || null; - - return this; -}; - -export default Allocation;