Skip to content

Commit

Permalink
Start of rework for validating API keys
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbjr committed May 27, 2017
1 parent 0f0c5fe commit 65619b0
Show file tree
Hide file tree
Showing 11 changed files with 464 additions and 0 deletions.
37 changes: 37 additions & 0 deletions config/apiguard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

return [

/*
|--------------------------------------------------------------------------
| Key name
|--------------------------------------------------------------------------
|
| This is the name of the variable that will provide us the API key in the
| header
|
*/
'header_key' => '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',

],

];
38 changes: 38 additions & 0 deletions database/migrations/2016_09_13_042808_create_api_keys_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateApiKeysTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('api_keys', function (Blueprint $table) {
$table->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');
}
}
17 changes: 17 additions & 0 deletions src/Auth/Contracts/ApiGuardAuthContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Chrisbjr\ApiGuard\Auth\Contracts;

use Illuminate\Http\Request;

interface ApiGuardAuthContract
{
/**
* A method that will be triggered to indicate that a particular user/object has logged in.
*
* @param Request $request
* @param $user
* @return void
*/
public function authenticate(Request $request, $user);
}
27 changes: 27 additions & 0 deletions src/Auth/Sentinel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Chrisbjr\ApiGuard\Auth;

use Chrisbjr\ApiGuard\Auth\Contracts\ApiGuardAuthContract;
use Illuminate\Http\Request;
use Sentinel as SentinelAuth;

/**
* A sample class that shows what you can do with this interface. Here, we use
* this class to trigger the "login" method provided by Sentinel. The "login"
* method from Sentinel logs that the user has logged in and populates the
* "last_login" field in the database.
*
* @package Chrisbjr\ApiGuard\Auth
*/
class Sentinel implements ApiGuardAuthContract
{
/**
* @param Request $request
* @param $user
*/
public function authenticate(Request $request, $user)
{
SentinelAuth::login($user);
}
}
57 changes: 57 additions & 0 deletions src/Console/Commands/GenerateApiKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace Chrisbjr\ApiGuard\Console\Commands;

use Chrisbjr\ApiGuard\Models\ApiKey;
use Illuminate\Console\Command;

class GenerateApiKey extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'api-key:generate
{--id= : ID of the model you want to bind to this API key}
{--type= : The class name of the model you want to bind to this API key}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate an API key';

/**
* Create a new command instance.
*
*/
public function __construct()
{
parent::__construct();
}

/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$apiKeyableId = $this->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;
}
}
26 changes: 26 additions & 0 deletions src/Http/Controllers/ApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Chrisbjr\ApiGuard\Http\Controllers;

use EllipseSynergie\ApiResponse\Laravel\Response;
use Illuminate\Routing\Controller;
use League\Fractal\Manager;

class ApiController extends Controller
{
/**
* @var Response
*/
protected $response;

public function __construct()
{
$fractal = new Manager();

if (isset($_GET['include'])) {
$fractal->parseIncludes($_GET['include']);
}

$this->response = new Response($fractal);
}
}
66 changes: 66 additions & 0 deletions src/Http/Middleware/AuthenticateApiKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Chrisbjr\ApiGuard\Http\Middleware;

use Carbon\Carbon;
use Chrisbjr\ApiGuard\Models\Device;
use Closure;

class AuthenticateApiKey
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
$apiKeyValue = $request->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);
}
}
34 changes: 34 additions & 0 deletions src/Http/Requests/ApiFormRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Chrisbjr\ApiGuard\Http\Requests;

use EllipseSynergie\ApiResponse\Laravel\Response;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use League\Fractal\Manager;

class ApiFormRequest extends FormRequest
{
public function expectsJson()
{
return true;
}

/**
* Format the errors from the given Validator instance.
*
* @param \Illuminate\Contracts\Validation\Validator $validator
* @return array
*/
protected function formatErrors(Validator $validator)
{
return $validator->getMessageBag()->toArray();
}

public function response(array $errors)
{
$response = new Response(new Manager());

return $response->errorUnprocessable($errors);
}
}
80 changes: 80 additions & 0 deletions src/Models/ApiKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace Chrisbjr\ApiGuard\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Request;

class ApiKey extends Model
{
use SoftDeletes;

protected $fillable = [
'key',
'apikeyable_id',
'apikeyable_type',
'last_ip_address',
'last_used_at',
];

/**
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function apikeyable()
{
return $this->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;
}
}
Loading

0 comments on commit 65619b0

Please sign in to comment.