Change authentication method for API.
This commit is contained in:
parent
63f377a038
commit
77e3744b40
9 changed files with 162 additions and 219 deletions
|
@ -1,124 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\API;
|
||||
|
||||
use JWTAuth;
|
||||
use Hash;
|
||||
use Validator;
|
||||
|
||||
use Tymon\JWTAuth\Exceptions\JWTException;
|
||||
|
||||
use Dingo\Api\Exception\StoreResourceFailedException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Foundation\Auth\ThrottlesLogins;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
|
||||
|
||||
use Pterodactyl\Transformers\UserTransformer;
|
||||
use Pterodactyl\Models;
|
||||
|
||||
/**
|
||||
* @Resource("Auth")
|
||||
*/
|
||||
class AuthController extends BaseController
|
||||
{
|
||||
|
||||
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
|
||||
|
||||
/**
|
||||
* Lockout time for failed login requests.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
protected $lockoutTime = 120;
|
||||
|
||||
/**
|
||||
* After how many attempts should logins be throttled and locked.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
protected $maxLoginAttempts = 3;
|
||||
|
||||
/**
|
||||
* Create a new authentication controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* Authenticate with the API to recieved a JSON Web Token
|
||||
*
|
||||
* @Post("/auth/login")
|
||||
* @Versions({"v1"})
|
||||
* @Request({"email": "e@mail.com", "password": "soopersecret"})
|
||||
* @Response(200, body={"token": "<jwt-token>"})
|
||||
*/
|
||||
public function postLogin(Request $request) {
|
||||
|
||||
$validator = Validator::make($request->only(['email', 'password']), [
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|min:8'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new StoreResourceFailedException('Required authentication fields were invalid.', $validator->errors());
|
||||
}
|
||||
|
||||
$throttled = $this->isUsingThrottlesLoginsTrait();
|
||||
if ($throttled && $this->hasTooManyLoginAttempts($request)) {
|
||||
throw new TooManyRequestsHttpException('You have been login throttled for 120 seconds.');
|
||||
}
|
||||
|
||||
// Is the email & password valid?
|
||||
$user = Models\User::where('email', $request->input('email'))->first();
|
||||
if (!$user || !Hash::check($request->input('password'), $user->password)) {
|
||||
if ($throttled) {
|
||||
$this->incrementLoginAttempts($request);
|
||||
}
|
||||
throw new UnauthorizedHttpException('A user by those credentials was not found.');
|
||||
}
|
||||
|
||||
// @TODO: validate TOTP if enabled on account?
|
||||
// Perhaps this could be implemented in such a way that they login to their
|
||||
// account and generate a one time password that can be used? Would be a pain in
|
||||
// the butt for multiple API requests though. Maybe just included a 'totp' field
|
||||
// that can include the token for that timestamp. Would allow for programtic
|
||||
// generation of the code and API requests.
|
||||
if ($user->root_admin !== 1) {
|
||||
throw new UnauthorizedHttpException('This account does not have permission to interface this API.');
|
||||
}
|
||||
|
||||
try {
|
||||
$token = JWTAuth::fromUser($user);
|
||||
if (!$token) {
|
||||
throw new UnauthorizedHttpException('');
|
||||
}
|
||||
} catch (JWTException $ex) {
|
||||
throw new ServiceUnavailableHttpException('');
|
||||
}
|
||||
|
||||
return compact('token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Authenticated
|
||||
*
|
||||
* @Post("/auth/validate")
|
||||
* @Versions({"v1"})
|
||||
* @Request(headers={"Authorization": "Bearer <jwt-token>"})
|
||||
* @Response(204)
|
||||
*/
|
||||
public function postValidate(Request $request) {
|
||||
return $this->response->noContent();
|
||||
}
|
||||
|
||||
}
|
80
app/Http/Middleware/APISecretToken.php
Normal file
80
app/Http/Middleware/APISecretToken.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware;
|
||||
|
||||
use Pterodactyl\Models\APIKey;
|
||||
use Pterodactyl\Models\APIPermission;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Dingo\Api\Routing\Route;
|
||||
use Dingo\Api\Auth\Provider\Authorization;
|
||||
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; // 400
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; // 401
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; // 403
|
||||
|
||||
class APISecretToken extends Authorization
|
||||
{
|
||||
|
||||
protected $algo = 'sha256';
|
||||
|
||||
protected $permissionAllowed = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function getAuthorizationMethod()
|
||||
{
|
||||
return 'Authorization';
|
||||
}
|
||||
|
||||
public function authenticate(Request $request, Route $route)
|
||||
{
|
||||
if (!$request->bearerToken() || empty($request->bearerToken())) {
|
||||
throw new UnauthorizedHttpException('The authentication header was missing or malformed');
|
||||
}
|
||||
|
||||
list($public, $hashed) = explode('.', $request->bearerToken());
|
||||
|
||||
$key = APIKey::where('public', $public)->first();
|
||||
if (!$key) {
|
||||
throw new AccessDeniedHttpException('Invalid API Key.');
|
||||
}
|
||||
|
||||
// Check for Resource Permissions
|
||||
if (!empty($request->route()->getName())) {
|
||||
if(!is_null($key->allowed_ips)) {
|
||||
if (!in_array($request->ip(), json_decode($key->allowed_ips))) {
|
||||
throw new AccessDeniedHttpException('This IP address does not have permission to use this API key.');
|
||||
}
|
||||
}
|
||||
|
||||
foreach(APIPermission::where('key_id', $key->id)->get() as &$row) {
|
||||
if ($row->permission === '*' || $row->permission === $request->route()->getName()) {
|
||||
$this->permissionAllowed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->permissionAllowed) {
|
||||
throw new AccessDeniedHttpException('You do not have permission to access this resource.');
|
||||
}
|
||||
}
|
||||
|
||||
if($this->_generateHMAC($request->fullUrl(), $request->getContent(), $key->secret) !== base64_decode($hashed)) {
|
||||
throw new BadRequestHttpException('The hashed body was not valid. Potential modification of contents in route.');
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
protected function _generateHMAC($url, $body, $key)
|
||||
{
|
||||
$data = urldecode($url) . '.' . $body;
|
||||
return hash_hmac($this->algo, $data, $key, true);
|
||||
}
|
||||
|
||||
}
|
|
@ -10,25 +10,7 @@ class APIRoutes
|
|||
|
||||
public function map(Router $router) {
|
||||
|
||||
app('Dingo\Api\Auth\Auth')->extend('jwt', function ($app) {
|
||||
return new \Dingo\Api\Auth\Provider\JWT($app['Tymon\JWTAuth\JWTAuth']);
|
||||
});
|
||||
|
||||
$api = app('Dingo\Api\Routing\Router');
|
||||
|
||||
$api->version('v1', function ($api) {
|
||||
$api->post('auth/login', [
|
||||
'as' => 'api.auth.login',
|
||||
'uses' => 'Pterodactyl\Http\Controllers\API\AuthController@postLogin'
|
||||
]);
|
||||
|
||||
$api->post('auth/validate', [
|
||||
'middleware' => 'api.auth',
|
||||
'as' => 'api.auth.validate',
|
||||
'uses' => 'Pterodactyl\Http\Controllers\API\AuthController@postValidate'
|
||||
]);
|
||||
});
|
||||
|
||||
$api->version('v1', ['middleware' => 'api.auth'], function ($api) {
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Log;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Models\APIPermission;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class API extends Model
|
||||
{
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'api';
|
||||
|
||||
/**
|
||||
* The attributes excluded from the model's JSON form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = ['daemonSecret'];
|
||||
|
||||
public function permissions()
|
||||
{
|
||||
return $this->hasMany(APIPermission::class);
|
||||
}
|
||||
|
||||
public static function findKey($key)
|
||||
{
|
||||
return self::where('key', $key)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an API key has permission to perform an action.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $permission
|
||||
* @return boolean
|
||||
*/
|
||||
public static function checkPermission($key, $permission)
|
||||
{
|
||||
$api = self::findKey($key);
|
||||
|
||||
if (!$api) {
|
||||
throw new DisplayException('The requested API key (' . $key . ') was not found in the system.');
|
||||
}
|
||||
|
||||
return APIPermission::check($api->id, $permission);
|
||||
|
||||
}
|
||||
|
||||
public static function noPermissionError($error = 'You do not have permission to perform this action with this API key.')
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'You do not have permission to perform this action with this API key.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
}
|
17
app/Models/APIKey.php
Normal file
17
app/Models/APIKey.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class APIKey extends Model
|
||||
{
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'api_keys';
|
||||
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Debugbar;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class APIPermission extends Model
|
||||
|
@ -15,16 +14,4 @@ class APIPermission extends Model
|
|||
*/
|
||||
protected $table = 'api_permissions';
|
||||
|
||||
/**
|
||||
* Checks if an API key has a specific permission.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string $permission
|
||||
* @return boolean
|
||||
*/
|
||||
public static function check($id, $permission)
|
||||
{
|
||||
return self::where('key_id', $id)->where('permission', $permission)->exists();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -155,7 +155,7 @@ return [
|
|||
*/
|
||||
|
||||
'auth' => [
|
||||
'jwt' => 'Dingo\Api\Auth\Provider\JWT'
|
||||
'custom' => 'Pterodactyl\Http\Middleware\APISecretToken'
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateTableApiKeys extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('api_keys', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->char('public', 16);
|
||||
$table->char('secret', 32);
|
||||
$table->json('allowed_ips')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::drop('api_keys');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateTableApiPermissions extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('api_permissions', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('key_id')->unsigned();
|
||||
$table->string('permission');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::drop('api_permissions');
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue