Skip to content

Commit

Permalink
add support for security
Browse files Browse the repository at this point in the history
  • Loading branch information
omar.fawzy committed Jul 27, 2022
1 parent b594b4d commit 7082b03
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 34 deletions.
33 changes: 20 additions & 13 deletions app/Modules/Api/Middlewares/ApiMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,38 @@
namespace App\Modules\Api\Middlewares;

use App\Modules\Api\Errors\ApiError;
use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use App\Modules\OpenApi\Middlewares\Middleware;
use Illuminate\Http\Response;
use Laravel\Sanctum\PersonalAccessToken;
use Psr\Http\Message\ServerRequestInterface;

class ApiMiddleware
class ApiMiddleware implements Middleware
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return Closure|JsonResponse
* @param ServerRequestInterface $serverRequest
* @return bool
* @throws ApiError
*/
public function handle(Request $request, Closure $next): mixed
public function handle(ServerRequestInterface $serverRequest): bool
{
$token = $request->bearerToken();
if (false === $serverRequest->hasHeader('Authorization')) {
return false;
}

$header = $serverRequest->getHeader('Authorization')[0];

if (preg_match('/Bearer\s(\S+)/', $header, $matches)) {
$token = $matches[1];
} else {
return false;
}

if (null === PersonalAccessToken::findToken($token))
{
throw new ApiError('Access Token is missing or not found', [], Response::HTTP_UNAUTHORIZED);
if (null === PersonalAccessToken::findToken($token)) {
throw new ApiError('Bearer Token is missing or not found', [], Response::HTTP_UNAUTHORIZED);
}

return $next($request);
return true;
}
}
20 changes: 20 additions & 0 deletions app/Modules/OpenApi/Factories/AuthenticationFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace App\Modules\OpenApi\Factories;

use App\Modules\Api\Middlewares\ApiMiddleware;
use App\Modules\OpenApi\Middlewares\Middleware;
use InvalidArgumentException;

class AuthenticationFactory
{
private const BEARER_AUTH = 'bearerAuth';

public function make(string $securityMethod, array $context = []): Middleware
{
return match ($securityMethod) {
self::BEARER_AUTH => new ApiMiddleware(),
default => throw new InvalidArgumentException("Security method: $securityMethod is not supported for this operation."),
};
}
}
10 changes: 10 additions & 0 deletions app/Modules/OpenApi/Middlewares/Middleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace App\Modules\OpenApi\Middlewares;

use Psr\Http\Message\ServerRequestInterface;

interface Middleware
{
public function handle(ServerRequestInterface $serverRequest): bool;
}
61 changes: 61 additions & 0 deletions app/Modules/OpenApi/Services/AuthenticationManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace App\Modules\OpenApi\Services;

use App\Modules\OpenApi\Factories\AuthenticationFactory;
use cebe\openapi\spec\OpenApi;
use Exception;
use Illuminate\Validation\UnauthorizedException;
use League\OpenAPIValidation\PSR7\PathFinder;
use League\OpenAPIValidation\PSR7\SpecFinder;
use Psr\Http\Message\ServerRequestInterface;

class AuthenticationManager
{
public function __construct(private AuthenticationFactory $authenticationFactory)
{
}

/**
* @throws Exception
*/
public function authenticate(ServerRequestInterface $serverRequest, OpenApi $openApi): void
{
$pathFinder = new PathFinder($openApi, $serverRequest->getUri(), $serverRequest->getMethod());

$operationAddresses = $pathFinder->search();

if (empty($operationAddresses)) {
throw new Exception("Operation with uri: {$serverRequest->getUri()} doesn't exist in the open api specs.");
}

if (count($operationAddresses) > 1) {
throw new Exception(
"Duplicate operations for uri: {$serverRequest->getUri()} exist in the open api specs."
);
}

$specFinder = new SpecFinder($openApi);

$securityRequirements = $specFinder->findSecuritySpecs($operationAddresses[0]);

if (empty($securityRequirements)) {
return;
}

$successfulAuth = false;

foreach ($securityRequirements as $securityRequirement) {
foreach ((array)$securityRequirement->getSerializableData() as $securityMethod => $context) {
$successfulAuth = $this->authenticationFactory->make($securityMethod, $context)->handle($serverRequest);
if (true === $successfulAuth) {
return;
}
}
}

if (false === $successfulAuth) {
throw new UnauthorizedException("Unauthorized access to a protected uri: {$serverRequest->getUri()}");
}
}
}
14 changes: 10 additions & 4 deletions app/Modules/OpenApi/Validator/OpenApiValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
use App\Modules\OpenApi\Contexts\OpenApiContext;
use App\Modules\OpenApi\Errors\OpenApiError;
use App\Modules\OpenApi\Factories\OpenApiErrorFactory;
use App\Modules\OpenApi\Services\AuthenticationManager;
use Cache\Adapter\PHPArray\ArrayCachePool;
use Illuminate\Support\Facades\Storage;
use League\OpenAPIValidation\PSR7\Exception\ValidationFailed;
use League\OpenAPIValidation\PSR7\SchemaFactory\JsonFactory;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
Expand All @@ -20,7 +22,7 @@ class OpenApiValidator

private array $cache = [];

public function __construct(private OpenApiErrorFactory $openApiErrorFactory)
public function __construct(private OpenApiErrorFactory $openApiErrorFactory, private AuthenticationManager $authenticationManager)
{
$this->cachePool = new ArrayCachePool(null, $this->cache);
}
Expand All @@ -32,10 +34,14 @@ public function __construct(private OpenApiErrorFactory $openApiErrorFactory)
*/
public function validateRequest(ServerRequestInterface $serverRequest): OpenApiContext
{
$schema = $this->getOpenApiSchema();
$schemaFactory = new JsonFactory($this->getOpenApiFile());

$schema = $schemaFactory->createSchema();

$this->authenticationManager->authenticate($serverRequest, $schema);

$validator = (new ValidatorBuilder())
->fromJson($schema)
->fromSchema($schema)
->setCache($this->cachePool)
->overrideCacheKey(self::CACHE_KEY)
->getServerRequestValidator();
Expand Down Expand Up @@ -69,7 +75,7 @@ public function validateResponse(ResponseInterface $response, OpenApiContext $co
}
}

private function getOpenApiSchema(): string
private function getOpenApiFile(): string
{
return Storage::get(config('app.open_api_file_path'));
}
Expand Down
26 changes: 9 additions & 17 deletions app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace App\Providers;

use App\Modules\Api\Middlewares\ApiMiddleware;
use App\Modules\OpenApi\Controllers\OpenApiController;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
Expand Down Expand Up @@ -40,25 +39,10 @@ public function boot()
$this->configureRateLimiting();

$this->routes(function () {
$this->configurePublicRoutes();
$this->configureProtectedRoutes();
$this->configureRoutes();
});
}

private function configurePublicRoutes(): void
{
Route::group(['prefix' => 'api/v1'], function () {
Route::post('users', OpenApiController::class);
});
}

private function configureProtectedRoutes(): void
{
Route::group(['prefix' => 'api/v1', 'middleware' => ApiMiddleware::class], function () {
Route::any('{slug}', OpenApiController::class)
->where('slug', '.*');
});
}
/**
* Configure the rate limiters for the application.
*
Expand All @@ -70,4 +54,12 @@ protected function configureRateLimiting()
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}

private function configureRoutes(): void
{
Route::group(['prefix' => 'api/v1'], function () {
Route::any('{slug}', OpenApiController::class)
->where('slug', '.*');
});
}
}

0 comments on commit 7082b03

Please sign in to comment.