Fix JWT handling for API access when logging in

This commit is contained in:
Dane Everitt 2018-05-28 14:59:48 -07:00
parent aa61afb58f
commit a1444b047e
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
7 changed files with 143 additions and 68 deletions

View file

@ -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(),
]);
}
/**

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Pterodactyl\Traits\Helpers;
use Lcobucci\JWT\Signer;
use Illuminate\Support\Str;
trait ProvidesJWTServices
{
/**
* Get the signing key to use when creating JWTs.
*
* @return string
*/
public function getJWTSigningKey(): string
{
$key = config()->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;
}
}

17
config/jwt.php Normal file
View file

@ -0,0 +1,17 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| JWT Signing Key
|--------------------------------------------------------------------------
|
| This key is used for the verification of JSON Web Tokens in flight and
| should be different than the application encryption key. This key should
| be kept private at all times.
|
*/
'key' => env('APP_JWT_KEY'),
'signer' => \Lcobucci\JWT\Signer\Hmac\Sha256::class,
];

View file

@ -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;