tkhamez/slim-role-auth

View on GitHub
RoleAuth/SecureRouteMiddleware.php

Summary

Maintainability
A
2 hrs
Test Coverage
A
100%
<?php

namespace Tkhamez\Slim\RoleAuth;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Interfaces\RouteInterface;
use Slim\Routing\RouteContext;

/**
 * Denies access to a route if the required role is missing.
 *
 * Sends a HTTP status 403 (default) or optionally a "Location"
 * header for a redirect.
 *
 * It loads the roles from an request attribute
 * (an array with string values, e. g. ['role.one', 'role.two']).
 *
 * The role attribute is provided by the RoleMiddleware class.
 *
 * The configured routes are compared against the route pattern from a
 * RouteInterface class from the request attribute named "route"
 * (provided by Slim).
 *
 * All routes are *allowed* if the "route" attributes is missing in the request object!
 */
class SecureRouteMiddleware implements MiddlewareInterface
{
    /**
     * @var ResponseFactoryInterface
     */
    private $responseFactory;

    /**
     * @var string[][]
     */
    private $secured;

    /**
     * @var string[]
     */
    private $options;

    /**
     * Constructor.
     *
     * Parameter secured:
     * - Example: ['/secured/public' => ['anonymous', 'user'], '/secured' => ['user']]
     * - First match will be used.
     * - Keys are route pattern, matched by "starts-with".
     * - Values are roles, only one must match to allow the route.
     *
     * Parameter options:
     * - Example: ['redirect_url' => '/login']
     * - redirect_url: send a Location header instead of a 403 status code.
     *
     * @param ResponseFactoryInterface $responseFactory
     * @param string[][] $secured
     * @param string[] $options
     */
    public function __construct(ResponseFactoryInterface $responseFactory, array $secured, array $options = [])
    {
        $this->responseFactory = $responseFactory;
        $this->secured = $secured;
        $this->options = $options;
    }

    /**
     * @param ServerRequestInterface $request
     * @param RequestHandlerInterface $handler
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $route = RouteContext::fromRequest($request)->getRoute();
        if (! $route instanceof RouteInterface) {
            return $handler->handle($request);
        }

        $roles = $request->getAttribute(RoleMiddleware::ROLES);
        $routePattern = $route->getPattern();

        $allowed = true;
        foreach ($this->secured as $securedRoute => $requiredRoles) {
            if (strpos($routePattern, $securedRoute) !== 0) {
                continue;
            }
            if (! is_array($roles) || count(array_intersect($requiredRoles, $roles)) === 0) {
                $allowed = false;
            }
            break;
        }

        if ($allowed === false) {
            $response = $this->responseFactory->createResponse();
            if (isset($this->options['redirect_url'])) {
                return $response
                    ->withHeader('Location', (string) $this->options['redirect_url'])
                    ->withStatus(302);
            } else {
                return $response->withStatus(403);
            }
        }

        return $handler->handle($request);
    }
}