Change authentication method for API.

This commit is contained in:
Dane Everitt 2016-01-15 19:26:50 -05:00
parent 63f377a038
commit 77e3744b40
9 changed files with 162 additions and 219 deletions

View file

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

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

View file

@ -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) {
/**

View file

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

View file

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

View file

@ -155,7 +155,7 @@ return [
*/
'auth' => [
'jwt' => 'Dingo\Api\Auth\Provider\JWT'
'custom' => 'Pterodactyl\Http\Middleware\APISecretToken'
],
/*

View file

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

View file

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