From 65619b0977442c0b6578a02dc9a114f156ada384 Mon Sep 17 00:00:00 2001 From: Chris Bautista Date: Sat, 27 May 2017 14:41:31 +0000 Subject: [PATCH] Start of rework for validating API keys --- config/apiguard.php | 37 +++++++++ ...016_09_13_042808_create_api_keys_table.php | 38 +++++++++ src/Auth/Contracts/ApiGuardAuthContract.php | 17 ++++ src/Auth/Sentinel.php | 27 +++++++ src/Console/Commands/GenerateApiKey.php | 57 +++++++++++++ src/Http/Controllers/ApiController.php | 26 ++++++ src/Http/Middleware/AuthenticateApiKey.php | 66 +++++++++++++++ src/Http/Requests/ApiFormRequest.php | 34 ++++++++ src/Models/ApiKey.php | 80 +++++++++++++++++++ src/Models/Mixins/Apikeyable.php | 18 +++++ src/Providers/ApiGuardServiceProvider.php | 64 +++++++++++++++ 11 files changed, 464 insertions(+) create mode 100644 config/apiguard.php create mode 100644 database/migrations/2016_09_13_042808_create_api_keys_table.php create mode 100644 src/Auth/Contracts/ApiGuardAuthContract.php create mode 100644 src/Auth/Sentinel.php create mode 100644 src/Console/Commands/GenerateApiKey.php create mode 100644 src/Http/Controllers/ApiController.php create mode 100644 src/Http/Middleware/AuthenticateApiKey.php create mode 100644 src/Http/Requests/ApiFormRequest.php create mode 100644 src/Models/ApiKey.php create mode 100644 src/Models/Mixins/Apikeyable.php create mode 100644 src/Providers/ApiGuardServiceProvider.php diff --git a/config/apiguard.php b/config/apiguard.php new file mode 100644 index 000000000..6e12caad2 --- /dev/null +++ b/config/apiguard.php @@ -0,0 +1,37 @@ + 'X-Authorization', + + /* + |-------------------------------------------------------------------------- + | Authentication Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to authenticate users. Example: + | + | Chrisbjr\ApiGuard\Auth\Sentinel:class + | + | You can set up your own authentication provider here by creating a class + | that implements the ApiGuardAuthContract interface. + | + */ + 'auth' => null, + + 'models' => [ + + 'api_key' => 'Chrisbjr\ApiGuard\Models\ApiKey', + + ], + +]; diff --git a/database/migrations/2016_09_13_042808_create_api_keys_table.php b/database/migrations/2016_09_13_042808_create_api_keys_table.php new file mode 100644 index 000000000..3477369df --- /dev/null +++ b/database/migrations/2016_09_13_042808_create_api_keys_table.php @@ -0,0 +1,38 @@ +increments('id'); + $table->nullableMorphs('apikeyable'); + $table->string('key', 50); + $table->string('last_ip_address', 50)->nullable(); + $table->dateTime('last_used_at')->nullable(); + $table->nullableTimestamps(); + $table->softDeletes(); + + $table->index('key'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('api_keys'); + } +} diff --git a/src/Auth/Contracts/ApiGuardAuthContract.php b/src/Auth/Contracts/ApiGuardAuthContract.php new file mode 100644 index 000000000..300decac6 --- /dev/null +++ b/src/Auth/Contracts/ApiGuardAuthContract.php @@ -0,0 +1,17 @@ +option('id'); + $apiKeyableType = $this->option('type'); + + $apiKey = new ApiKey([ + 'key' => ApiKey::generateKey(), + 'apikeyable_id' => $apiKeyableId, + 'apikeyable_type' => $apiKeyableType, + ]); + + $apiKey->save(); + + $this->info('An API key was created with the following key: ' . $apiKey->key); + + return; + } +} diff --git a/src/Http/Controllers/ApiController.php b/src/Http/Controllers/ApiController.php new file mode 100644 index 000000000..a83223a71 --- /dev/null +++ b/src/Http/Controllers/ApiController.php @@ -0,0 +1,26 @@ +parseIncludes($_GET['include']); + } + + $this->response = new Response($fractal); + } +} diff --git a/src/Http/Middleware/AuthenticateApiKey.php b/src/Http/Middleware/AuthenticateApiKey.php new file mode 100644 index 000000000..7ccf40a81 --- /dev/null +++ b/src/Http/Middleware/AuthenticateApiKey.php @@ -0,0 +1,66 @@ +header(config('apiguard.header_key', 'X-Authorization')); + + $apiKey = app(config('apiguard.models.api_key', 'Chrisbjr\ApiGuard\Models\ApiKey'))->where('key', $apiKeyValue) + ->first(); + + if (empty($apiKey)) { + return $this->unauthorizedResponse(); + } + + // Update this api key's last_used_at and last_ip_address + $apiKey->update([ + 'last_used_at' => Carbon::now(), + 'last_ip_address' => $request->ip(), + ]); + + $user = $apiKey->apikeyable; + + if (! empty(config('apiguard.auth'))) { + $apiGuardAuth = app(config('apiguard.auth')); + $apiGuardAuth->authenticate($user); + } + + // Bind the user or object to the request + // By doing this, we can now get the specified user through the request object in the controller using: + // $request->user() + $request->setUserResolver(function () use ($user) { + return $user; + }); + + // Attach the apikey object to the request + $request->apiKey = $apiKey; + + return $next($request); + } + + protected function unauthorizedResponse() + { + return response([ + 'error' => [ + 'code' => '401', + 'http_code' => 'GEN-UNAUTHORIZED', + 'message' => 'Unauthorized.', + ], + ], 401); + } +} diff --git a/src/Http/Requests/ApiFormRequest.php b/src/Http/Requests/ApiFormRequest.php new file mode 100644 index 000000000..b59fd6cdc --- /dev/null +++ b/src/Http/Requests/ApiFormRequest.php @@ -0,0 +1,34 @@ +getMessageBag()->toArray(); + } + + public function response(array $errors) + { + $response = new Response(new Manager()); + + return $response->errorUnprocessable($errors); + } +} diff --git a/src/Models/ApiKey.php b/src/Models/ApiKey.php new file mode 100644 index 000000000..757c650a9 --- /dev/null +++ b/src/Models/ApiKey.php @@ -0,0 +1,80 @@ +morphTo(); + } + + /** + * @param $apikeyable + * + * @return ApiKey + */ + public static function make($apikeyable) + { + $apiKey = new ApiKey([ + 'key' => self::generateKey(), + 'apikeyable_id' => $apikeyable->id, + 'apikeyable_type' => get_class($apikeyable), + 'last_ip_address' => Request::ip(), + 'last_used_at' => Carbon::now(), + ]); + + $apiKey->save(); + + return $apiKey; + } + + /** + * A sure method to generate a unique API key + * + * @return string + */ + public static function generateKey() + { + do { + $salt = sha1(time() . mt_rand()); + $newKey = substr($salt, 0, 40); + } // Already in the DB? Fail. Try again + while (self::keyExists($newKey)); + + return $newKey; + } + + /** + * Checks whether a key exists in the database or not + * + * @param $key + * @return bool + */ + private static function keyExists($key) + { + $apiKeyCount = self::where('key', '=', $key)->limit(1)->count(); + + if ($apiKeyCount > 0) return true; + + return false; + } +} diff --git a/src/Models/Mixins/Apikeyable.php b/src/Models/Mixins/Apikeyable.php new file mode 100644 index 000000000..918568532 --- /dev/null +++ b/src/Models/Mixins/Apikeyable.php @@ -0,0 +1,18 @@ +morphMany(config('apiguard.models.api_key', ApiKey::class), 'apikeyable'); + } + + public function createApiKey() + { + return ApiKey::make($this); + } +} diff --git a/src/Providers/ApiGuardServiceProvider.php b/src/Providers/ApiGuardServiceProvider.php new file mode 100644 index 000000000..8d8555824 --- /dev/null +++ b/src/Providers/ApiGuardServiceProvider.php @@ -0,0 +1,64 @@ + AuthenticateApiKey::class, + ]; + + /** + * Bootstrap the application services. + * + * @param Router $router + * @return void + */ + public function boot(Router $router) + { + // Publish migrations + $this->publishFiles(); + + $this->defineMiddleware($router); + } + + /** / + * Register the application services. + * + * @return void + */ + public function register() + { + $this->commands([ + GenerateApiKey::class, + ]); + } + + private function defineMiddleware($router) + { + foreach ($this->middlewares as $name => $class) { + if (str_contains(App::VERSION(), "5.4.")) { + $router->aliasMiddleware($name, $class); + } else { + $router->middleware($name, $class); + } + } + } + + private function publishFiles() + { + $this->publishes([ + __DIR__ . '/../../database/migrations/' => base_path('/database/migrations'), + ], 'migrations'); + + $this->publishes([ + __DIR__ . '/../../config/apiguard.php' => config_path('apiguard.php'), + ], 'config'); + } +}