From 2f447305b9c042bc746d6f2b71c76a7de58920f3 Mon Sep 17 00:00:00 2001 From: Ron Rise Date: Wed, 19 Nov 2025 14:08:54 -0500 Subject: [PATCH 1/2] fix: make Scope attribute repeatable and improve scope handling in middleware --- src/Attributes/Guards/Scope.php | 2 +- src/Http/Middleware/ScopeMiddleware.php | 51 +++++++++++++++---------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/Attributes/Guards/Scope.php b/src/Attributes/Guards/Scope.php index f86ac36..76fb6db 100644 --- a/src/Attributes/Guards/Scope.php +++ b/src/Attributes/Guards/Scope.php @@ -6,7 +6,7 @@ namespace Siteworxpro\App\Attributes\Guards; use Attribute; -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] readonly class Scope { public function __construct( diff --git a/src/Http/Middleware/ScopeMiddleware.php b/src/Http/Middleware/ScopeMiddleware.php index 19a144e..f20dcd8 100644 --- a/src/Http/Middleware/ScopeMiddleware.php +++ b/src/Http/Middleware/ScopeMiddleware.php @@ -32,7 +32,7 @@ class ScopeMiddleware extends Middleware * 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 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. @@ -42,7 +42,7 @@ class ScopeMiddleware extends Middleware */ public function process( ServerRequestInterface $request, - RequestHandlerInterface | Dispatcher $handler + RequestHandlerInterface|Dispatcher $handler ): ResponseInterface { // Attempt to resolve the route's callable [Controller instance, method name]. $callable = $this->extractRouteCallable($handler); @@ -57,38 +57,47 @@ class ScopeMiddleware extends Middleware // 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); + if (empty($attributes)) { + // No scope attributes; delegate to the next handler. + return $handler->handle($request); + } + + $requiredScopes = []; + $userScopes = []; + foreach ($attributes as $attribute) { /** @var Scope $scopeInstance Concrete Scope attribute instance. */ $scopeInstance = $attribute->newInstance(); - $requiredScopes = $scopeInstance->getScopes(); + $requiredScopes = array_merge($requiredScopes, $scopeInstance->getScopes()); - // Retrieve user scopes from the request (defaults to an empty array). - $userScopes = $request->getAttribute($scopeInstance->getClaim(), []); - - if (!is_array($userScopes)) { + $scopes = $request->getAttribute($scopeInstance->getClaim()); + if (!is_array($scopes)) { // If user scopes are not an array, treat as no scopes provided. - $userScopes = explode($scopeInstance->getSeparator(), (string) $userScopes); + $scopes = explode($scopeInstance->getSeparator(), (string) $scopes); } - // Deny if any required scope is missing from the user's scopes. - if ( - array_any( - $requiredScopes, - fn($requiredScope) => !in_array($requiredScope, $userScopes, true) - ) - ) { - return JsonResponseFactory::createJsonResponse([ - 'error' => 'insufficient_scope', - 'error_description' => - 'The request requires higher privileges than provided by the access token.' - ], CodesEnum::FORBIDDEN); - } + $userScopes = array_merge( + $userScopes, + $scopes + ); + } + + $userScopes = array_unique($userScopes); + + // Deny if any required scope is missing from the user's scopes. + if (array_intersect($userScopes, $requiredScopes) === []) { + return JsonResponseFactory::createJsonResponse([ + 'error' => 'insufficient_scope', + 'error_description' => + 'The request requires higher privileges than provided by the access token.' + ], CodesEnum::FORBIDDEN); } } } -- 2.49.1 From 52393814d931672987fad00950fa08aff32de54e Mon Sep 17 00:00:00 2001 From: Ron Rise Date: Wed, 19 Nov 2025 14:25:08 -0500 Subject: [PATCH 2/2] fix: enhance scope handling by adding RequireAllScopes attribute and updating Scope usage --- src/Attributes/Guards/RequireAllScopes.php | 12 ++++++++++++ src/Attributes/Guards/Scope.php | 7 ++++++- src/Controllers/IndexController.php | 3 ++- src/Http/Middleware/ScopeMiddleware.php | 11 ++++++++++- 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 src/Attributes/Guards/RequireAllScopes.php diff --git a/src/Attributes/Guards/RequireAllScopes.php b/src/Attributes/Guards/RequireAllScopes.php new file mode 100644 index 0000000..95bd0c8 --- /dev/null +++ b/src/Attributes/Guards/RequireAllScopes.php @@ -0,0 +1,12 @@ + $scopes the required scopes + * @param string $claim the claim to check for scopes + * @param string $separator the separator used to split scopes in the claim + */ public function __construct( private array $scopes = [], private string $claim = 'scope', - private string $separator = ' ', + private string $separator = ' ' ) { } diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php index c0a8e57..1547df5 100644 --- a/src/Controllers/IndexController.php +++ b/src/Controllers/IndexController.php @@ -22,7 +22,8 @@ class IndexController extends Controller * @throws \JsonException */ #[Guards\Jwt] - #[Guards\Scope(['get.index'])] + #[Guards\Scope(['get.index', 'status.check'])] + #[Guards\RequireAllScopes] public function get(ServerRequest $request): ResponseInterface { return JsonResponseFactory::createJsonResponse(['status_code' => 200, 'message' => 'Server is running']); diff --git a/src/Http/Middleware/ScopeMiddleware.php b/src/Http/Middleware/ScopeMiddleware.php index f20dcd8..334afde 100644 --- a/src/Http/Middleware/ScopeMiddleware.php +++ b/src/Http/Middleware/ScopeMiddleware.php @@ -8,6 +8,7 @@ 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; @@ -63,6 +64,7 @@ class ScopeMiddleware extends Middleware // 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. @@ -71,12 +73,16 @@ class ScopeMiddleware extends Middleware $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. @@ -92,7 +98,10 @@ class ScopeMiddleware extends Middleware $userScopes = array_unique($userScopes); // Deny if any required scope is missing from the user's scopes. - if (array_intersect($userScopes, $requiredScopes) === []) { + if ( + (!$requireAll && array_intersect($userScopes, $requiredScopes) === []) || + ($requireAll && array_diff($requiredScopes, $userScopes) !== []) + ) { return JsonResponseFactory::createJsonResponse([ 'error' => 'insufficient_scope', 'error_description' => -- 2.49.1