diff --git a/.env.example b/.env.example index 5278a54f9..e282046f9 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ APP_ENV=local APP_DEBUG=true APP_KEY=SomeRandomString +JWT_SECRET=ChangeMe DB_HOST=localhost DB_PORT=3306 @@ -22,3 +23,7 @@ MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null + +API_PREFIX=api +API_VERSION=v1 +API_DEBUG=false diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 1ef91c60c..d3a4599a7 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -55,7 +55,7 @@ class Handler extends ExceptionHandler $e = new NotFoundHttpException($e->getMessage(), $e); } - if ($request->isXmlHttpRequest() || $request->ajax() || $request->is('api/*') || $request->is('remote/*')) { + if ($request->isXmlHttpRequest() || $request->ajax() || $request->is('remote/*')) { $exception = 'An exception occured while attempting to perform this action, please try again.'; diff --git a/app/Http/Controllers/API/AuthController.php b/app/Http/Controllers/API/AuthController.php new file mode 100644 index 000000000..6f95aa6c8 --- /dev/null +++ b/app/Http/Controllers/API/AuthController.php @@ -0,0 +1,64 @@ +"}) + */ + public function postLogin(Request $request) { + $credentials = $request->only('email', 'password'); + + try { + $token = JWTAuth::attempt($credentials, [ + 'permissions' => [ + 'view_users' => true, + 'edit_users' => true, + 'delete_users' => false, + ] + ]); + if (!$token) { + throw new UnauthorizedHttpException(''); + } + } catch (JWTException $ex) { + throw new ServiceUnavailableHttpException(''); + } + + return compact('token'); + } + + /** + * Check if Authenticated + * + * @Post("/validate") + * @Versions({"v1"}) + * @Request(headers={"Authorization": "Bearer "}) + * @Response(204); + */ + public function postValidate(Request $request) { + return $this->response->noContent(); + } + +} diff --git a/app/Http/Controllers/API/BaseController.php b/app/Http/Controllers/API/BaseController.php new file mode 100644 index 000000000..b0ade2a64 --- /dev/null +++ b/app/Http/Controllers/API/BaseController.php @@ -0,0 +1,11 @@ +header('X-Authorization'), 'get-users')) { - return API::noPermissionError(); - } - - return response()->json([ - 'users' => User::all() - ]); - } - - /** - * Returns JSON response about a user given their ID. - * If fields are provided only those fields are returned. + * List All Users * - * Does not return protected fields (i.e. password & totp_secret) + * Lists all users currently on the system. * - * @param Request $request - * @param int $id - * @param string $fields - * @return Response + * @Get("/{?page}") + * @Versions({"v1"}) + * @Parameters({ + * @Parameter("page", type="integer", description="The page of results to view.", default=1) + * }) + * @Response(200) */ - public function getUser(Request $request, $id, $fields = null) - { - - // Policies don't work if the user isn't logged in for whatever reason in Laravel... - if(!API::checkPermission($request->header('X-Authorization'), 'get-users')) { - return API::noPermissionError(); - } - - if (is_null($fields)) { - return response()->json(User::find($id)); - } - - $query = User::where('id', $id); - $explode = explode(',', $fields); - - foreach($explode as &$exploded) { - if(!empty($exploded)) { - $query->addSelect($exploded); - } - } - - try { - return response()->json($query->get()); - } catch (\Exception $e) { - if ($e instanceof \Illuminate\Database\QueryException) { - return response()->json([ - 'error' => 'One of the fields provided in your argument list is invalid.' - ], 500); - } - throw $e; - } - + public function getUsers(Request $request) { + $users = Models\User::paginate(15); + return $this->response->paginator($users, new UserTransformer); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 5ae0c37ad..513fa4d9d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -17,7 +17,6 @@ class Kernel extends HttpKernel \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \Pterodactyl\Http\Middleware\VerifyCsrfToken::class, ]; /** @@ -30,7 +29,7 @@ class Kernel extends HttpKernel 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'guest' => \Pterodactyl\Http\Middleware\RedirectIfAuthenticated::class, 'server' => \Pterodactyl\Http\Middleware\CheckServer::class, - 'api' => \Pterodactyl\Http\Middleware\APIAuthenticate::class, 'admin' => \Pterodactyl\Http\Middleware\AdminAuthenticate::class, + 'csrf' => \Pterodactyl\Http\Middleware\VerifyCsrfToken::class, ]; } diff --git a/app/Http/Middleware/APIAuthenticate.php b/app/Http/Middleware/APIAuthenticate.php deleted file mode 100644 index 8b59f7e27..000000000 --- a/app/Http/Middleware/APIAuthenticate.php +++ /dev/null @@ -1,46 +0,0 @@ -header('X-Authorization')) { - return response()->json([ - 'error' => 'Authorization header was missing with this request. Please pass the \'X-Authorization\' header with your request.' - ], 403); - } - - $api = API::where('key', $request->header('X-Authorization'))->first(); - if (!$api) { - return response()->json([ - 'error' => 'Invalid API key was provided in the request.' - ], 403); - } - - if (!is_null($api->allowed_ips)) { - if (!in_array($request->ip(), json_decode($api->allowed_ips, true))) { - return response()->json([ - 'error' => 'This IP (' . $request->ip() . ') is not permitted to access the API with that token.' - ], 403); - } - } - - return $next($request); - - } -} diff --git a/app/Http/Routes/APIRoutes.php b/app/Http/Routes/APIRoutes.php new file mode 100644 index 000000000..355a1a3b7 --- /dev/null +++ b/app/Http/Routes/APIRoutes.php @@ -0,0 +1,47 @@ +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) { + + $api->get('users', [ + 'as' => 'api.auth.validate', + 'uses' => 'Pterodactyl\Http\Controllers\API\UserController@getUsers' + ]); + + $api->get('users/{id}', function($id) { + return Models\User::findOrFail($id); + }); + + + }); + } + +} diff --git a/app/Transformers/UserTransformer.php b/app/Transformers/UserTransformer.php new file mode 100644 index 000000000..22e4dffe6 --- /dev/null +++ b/app/Transformers/UserTransformer.php @@ -0,0 +1,21 @@ +=5.5.9", "laravel/framework": "5.2.*", "barryvdh/laravel-debugbar": "^2.0", + "dingo/api": "1.0.*@dev", "doctrine/dbal": "^2.5", "guzzlehttp/guzzle": "^6.1", "pragmarx/google2fa": "^0.7.1", "webpatser/laravel-uuid": "^2.0", "prologue/alerts": "^0.4.0", - "s1lentium/iptools": "^1.0" + "s1lentium/iptools": "^1.0", + "tymon/jwt-auth": "^0.5.6" }, "require-dev": { "fzaninotto/faker": "~1.4", diff --git a/config/api.php b/config/api.php new file mode 100644 index 000000000..872723046 --- /dev/null +++ b/config/api.php @@ -0,0 +1,209 @@ + env('API_STANDARDS_TREE', 'x'), + + /* + |-------------------------------------------------------------------------- + | API Subtype + |-------------------------------------------------------------------------- + | + | Your subtype will follow the standards tree you use when used in the + | "Accept" header to negotiate the content type and version. + | + | For example: Accept: application/x.SUBTYPE.v1+json + | + */ + + 'subtype' => env('API_SUBTYPE', 'pterodactyl'), + + /* + |-------------------------------------------------------------------------- + | Default API Version + |-------------------------------------------------------------------------- + | + | This is the default version when strict mode is disabled and your API + | is accessed via a web browser. It's also used as the default version + | when generating your APIs documentation. + | + */ + + 'version' => env('API_VERSION', 'v1'), + + /* + |-------------------------------------------------------------------------- + | Default API Prefix + |-------------------------------------------------------------------------- + | + | A default prefix to use for your API routes so you don't have to + | specify it for each group. + | + */ + + 'prefix' => env('API_PREFIX', null), + + /* + |-------------------------------------------------------------------------- + | Default API Domain + |-------------------------------------------------------------------------- + | + | A default domain to use for your API routes so you don't have to + | specify it for each group. + | + */ + + 'domain' => env('API_DOMAIN', null), + + /* + |-------------------------------------------------------------------------- + | Name + |-------------------------------------------------------------------------- + | + | When documenting your API using the API Blueprint syntax you can + | configure a default name to avoid having to manually specify + | one when using the command. + | + */ + + 'name' => env('API_NAME', 'Pterodactyl Panel API'), + + /* + |-------------------------------------------------------------------------- + | Conditional Requests + |-------------------------------------------------------------------------- + | + | Globally enable conditional requests so that an ETag header is added to + | any successful response. Subsequent requests will perform a check and + | will return a 304 Not Modified. This can also be enabled or disabled + | on certain groups or routes. + | + */ + + 'conditionalRequest' => env('API_CONDITIONAL_REQUEST', true), + + /* + |-------------------------------------------------------------------------- + | Strict Mode + |-------------------------------------------------------------------------- + | + | Enabling strict mode will require clients to send a valid Accept header + | with every request. This also voids the default API version, meaning + | your API will not be browsable via a web browser. + | + */ + + 'strict' => env('API_STRICT', false), + + /* + |-------------------------------------------------------------------------- + | Debug Mode + |-------------------------------------------------------------------------- + | + | Enabling debug mode will result in error responses caused by thrown + | exceptions to have a "debug" key that will be populated with + | more detailed information on the exception. + | + */ + + 'debug' => env('API_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Generic Error Format + |-------------------------------------------------------------------------- + | + | When some HTTP exceptions are not caught and dealt with the API will + | generate a generic error response in the format provided. Any + | keys that aren't replaced with corresponding values will be + | removed from the final response. + | + */ + + 'errorFormat' => [ + 'message' => ':message', + 'errors' => ':errors', + 'code' => ':code', + 'status_code' => ':status_code', + 'debug' => ':debug', + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Providers + |-------------------------------------------------------------------------- + | + | The authentication providers that should be used when attempting to + | authenticate an incoming API request. + | + */ + + 'auth' => [ + 'jwt' => 'Dingo\Api\Auth\Provider\JWT' + ], + + /* + |-------------------------------------------------------------------------- + | Throttling / Rate Limiting + |-------------------------------------------------------------------------- + | + | Consumers of your API can be limited to the amount of requests they can + | make. You can create your own throttles or simply change the default + | throttles. + | + */ + + 'throttling' => [ + + ], + + /* + |-------------------------------------------------------------------------- + | Response Transformer + |-------------------------------------------------------------------------- + | + | Responses can be transformed so that they are easier to format. By + | default a Fractal transformer will be used to transform any + | responses prior to formatting. You can easily replace + | this with your own transformer. + | + */ + + 'transformer' => env('API_TRANSFORMER', Dingo\Api\Transformer\Adapter\Fractal::class), + + /* + |-------------------------------------------------------------------------- + | Response Formats + |-------------------------------------------------------------------------- + | + | Responses can be returned in multiple formats by registering different + | response formatters. You can also customize an existing response + | formatter. + | + */ + + 'defaultFormat' => env('API_DEFAULT_FORMAT', 'json'), + + 'formats' => [ + + 'json' => Dingo\Api\Http\Response\Format\Json::class, + + ], + +]; diff --git a/config/app.php b/config/app.php index 5fb65fa0b..aa29a7be3 100644 --- a/config/app.php +++ b/config/app.php @@ -112,6 +112,9 @@ return [ 'providers' => [ + Dingo\Api\Provider\LaravelServiceProvider::class, + Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class, + /* * Laravel Framework Service Providers... */ @@ -179,6 +182,8 @@ return [ 'Crypt' => Illuminate\Support\Facades\Crypt::class, 'DB' => Illuminate\Support\Facades\DB::class, 'Debugbar' => Barryvdh\Debugbar\Facade::class, + 'DingoAPI' => Dingo\Api\Facade\API::class, + 'DingoRoute' => Dingo\Api\Facade\Route::class, 'Eloquent' => Illuminate\Database\Eloquent\Model::class, 'Event' => Illuminate\Support\Facades\Event::class, 'File' => Illuminate\Support\Facades\File::class, @@ -187,6 +192,8 @@ return [ 'Hash' => Illuminate\Support\Facades\Hash::class, 'Input' => Illuminate\Support\Facades\Input::class, 'Inspiring' => Illuminate\Foundation\Inspiring::class, + 'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class, + 'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class, 'Lang' => Illuminate\Support\Facades\Lang::class, 'Log' => Illuminate\Support\Facades\Log::class, 'Mail' => Illuminate\Support\Facades\Mail::class, diff --git a/config/jwt.php b/config/jwt.php new file mode 100644 index 000000000..7c1101320 --- /dev/null +++ b/config/jwt.php @@ -0,0 +1,168 @@ + env('JWT_SECRET', 'changeme'), + + /* + |-------------------------------------------------------------------------- + | JWT time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token will be valid for. + | Defaults to 1 hour + | + */ + + 'ttl' => 60, + + /* + |-------------------------------------------------------------------------- + | Refresh time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token can be refreshed + | within. I.E. The user can refresh their token within a 2 week window of + | the original token being created until they must re-authenticate. + | Defaults to 2 weeks + | + */ + + 'refresh_ttl' => 20160, + + /* + |-------------------------------------------------------------------------- + | JWT hashing algorithm + |-------------------------------------------------------------------------- + | + | Specify the hashing algorithm that will be used to sign the token. + | + | See here: https://github.com/namshi/jose/tree/2.2.0/src/Namshi/JOSE/Signer + | for possible values + | + */ + + 'algo' => 'HS256', + + /* + |-------------------------------------------------------------------------- + | User Model namespace + |-------------------------------------------------------------------------- + | + | Specify the full namespace to your User model. + | e.g. 'Acme\Entities\User' + | + */ + + 'user' => 'Pterodactyl\Models\User', + + /* + |-------------------------------------------------------------------------- + | User identifier + |-------------------------------------------------------------------------- + | + | Specify a unique property of the user that will be added as the 'sub' + | claim of the token payload. + | + */ + + 'identifier' => 'id', + + /* + |-------------------------------------------------------------------------- + | Required Claims + |-------------------------------------------------------------------------- + | + | Specify the required claims that must exist in any token. + | A TokenInvalidException will be thrown if any of these claims are not + | present in the payload. + | + */ + + 'required_claims' => ['iss', 'iat', 'exp', 'nbf', 'sub', 'jti'], + + /* + |-------------------------------------------------------------------------- + | Blacklist Enabled + |-------------------------------------------------------------------------- + | + | In order to invalidate tokens, you must have the the blacklist enabled. + | If you do not want or need this functionality, then set this to false. + | + */ + + 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Providers + |-------------------------------------------------------------------------- + | + | Specify the various providers used throughout the package. + | + */ + + 'providers' => [ + + /* + |-------------------------------------------------------------------------- + | User Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to find the user based + | on the subject claim + | + */ + + 'user' => 'Tymon\JWTAuth\Providers\User\EloquentUserAdapter', + + /* + |-------------------------------------------------------------------------- + | JWT Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to create and decode the tokens. + | + */ + + 'jwt' => 'Tymon\JWTAuth\Providers\JWT\NamshiAdapter', + + /* + |-------------------------------------------------------------------------- + | Authentication Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to authenticate users. + | + */ + + 'auth' => function ($app) { + return new Tymon\JWTAuth\Providers\Auth\IlluminateAuthAdapter($app['auth']); + }, + + /* + |-------------------------------------------------------------------------- + | Storage Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to store tokens in the blacklist + | + */ + + 'storage' => function ($app) { + return new Tymon\JWTAuth\Providers\Storage\IlluminateCacheAdapter($app['cache']); + } + + ] + +];