You've already forked Php-Template
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m55s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m55s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m58s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 3m1s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 2m40s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 3m0s
Reviewed-on: #21 Co-authored-by: Ron Rise <ron@siteworxpro.com> Co-committed-by: Ron Rise <ron@siteworxpro.com>
118 lines
4.9 KiB
PHP
118 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Siteworxpro\App\Http\Middleware;
|
|
|
|
use League\Route\Dispatcher;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use Psr\Http\Server\RequestHandlerInterface;
|
|
use Siteworxpro\App\Attributes\Guards\RequireAllScopes;
|
|
use Siteworxpro\App\Attributes\Guards\Scope;
|
|
use Siteworxpro\App\Controllers\Controller;
|
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
|
use Siteworxpro\HttpStatus\CodesEnum;
|
|
|
|
/**
|
|
* Middleware that enforces scope-based access control on controller actions.
|
|
*
|
|
* It inspects PHP 8 attributes of type \`Scope\` applied to the resolved controller method,
|
|
* compares the required scopes with the user scopes provided on the request attribute \`scopes\`,
|
|
* and returns a 403 JSON response when any required scope is missing.
|
|
*
|
|
* If the route callable cannot be resolved, or no scope is required, the request is passed through.
|
|
*
|
|
* @see Scope
|
|
*/
|
|
class ScopeMiddleware extends Middleware
|
|
{
|
|
/**
|
|
* Resolve the route callable, read any \`Scope\` attributes, and enforce required scopes.
|
|
*
|
|
* Expected user scopes are provided on the request under the attribute name \`scopes\`
|
|
* as an array of strings.
|
|
*
|
|
* @param ServerRequestInterface $request Incoming PSR-7 request (expects \`scopes\` attribute).
|
|
* @param RequestHandlerInterface|Dispatcher $handler Next handler or League\Route dispatcher.
|
|
*
|
|
* @return ResponseInterface A 403 JSON response when scopes are insufficient; otherwise the handler response.
|
|
*
|
|
* @throws \JsonException If encoding the JSON error response fails.
|
|
* @throws \ReflectionException If reflection on the controller or method fails.
|
|
*/
|
|
public function process(
|
|
ServerRequestInterface $request,
|
|
RequestHandlerInterface|Dispatcher $handler
|
|
): ResponseInterface {
|
|
// Attempt to resolve the route's callable [Controller instance, method name].
|
|
$callable = $this->extractRouteCallable($handler);
|
|
if ($callable === null) {
|
|
// If no callable is available, delegate to the next handler.
|
|
return $handler->handle($request);
|
|
}
|
|
|
|
/** @var Controller $class Controller instance resolved from the route. */
|
|
[$class, $method] = $callable;
|
|
|
|
// Ensure the controller exists and the method is defined before reflecting.
|
|
if (class_exists($class::class)) {
|
|
$reflectionClass = new \ReflectionClass($class);
|
|
|
|
if ($reflectionClass->hasMethod($method)) {
|
|
$reflectionMethod = $reflectionClass->getMethod($method);
|
|
|
|
// Fetch all Scope attributes declared on the method.
|
|
$attributes = $reflectionMethod->getAttributes(Scope::class);
|
|
$requireAllAttributes = $reflectionMethod->getAttributes(RequireAllScopes::class);
|
|
|
|
if (empty($attributes)) {
|
|
// No scope attributes; delegate to the next handler.
|
|
return $handler->handle($request);
|
|
}
|
|
|
|
$requiredScopes = [];
|
|
$userScopes = [];
|
|
$requireAll = false;
|
|
|
|
foreach ($attributes as $attribute) {
|
|
/** @var Scope $scopeInstance Concrete Scope attribute instance. */
|
|
$scopeInstance = $attribute->newInstance();
|
|
$requiredScopes = array_merge($requiredScopes, $scopeInstance->getScopes());
|
|
|
|
// If any attribute requires all scopes, set the flag.
|
|
$requireAll = $requireAll || !empty($requireAllAttributes);
|
|
|
|
$scopes = $request->getAttribute($scopeInstance->getClaim());
|
|
if (!is_array($scopes)) {
|
|
// If user scopes are not an array, treat as no scopes provided.
|
|
$scopes = explode($scopeInstance->getSeparator(), (string) $scopes);
|
|
}
|
|
|
|
$userScopes = array_merge(
|
|
$userScopes,
|
|
$scopes
|
|
);
|
|
}
|
|
|
|
$userScopes = array_unique($userScopes);
|
|
|
|
// Deny if any required scope is missing from the user's scopes.
|
|
if (
|
|
(!$requireAll && array_intersect($userScopes, $requiredScopes) === []) ||
|
|
($requireAll && array_diff($requiredScopes, $userScopes) !== [])
|
|
) {
|
|
return JsonResponseFactory::createJsonResponse([
|
|
'error' => 'insufficient_scope',
|
|
'error_description' =>
|
|
'The request requires higher privileges than provided by the access token.'
|
|
], CodesEnum::FORBIDDEN);
|
|
}
|
|
}
|
|
}
|
|
|
|
// All checks passed; continue down the middleware pipeline.
|
|
return $handler->handle($request);
|
|
}
|
|
}
|