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); } } }