Failed Conditions
Push — master ( 2436f4...03b798 )
by Arnold
02:53
created

src/AuthMiddleware.php (2 issues)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Jasny\Auth;
6
7
use Improved as i;
8
use Improved\IteratorPipeline\Pipeline;
9
use Jasny\Auth\AuthzInterface as Authz;
10
use Jasny\Auth\Session\SessionInterface;
11
use Psr\Http\Message\ServerRequestInterface as ServerRequest;
12
use Psr\Http\Message\ResponseInterface as Response;
13
use Psr\Http\Message\ResponseFactoryInterface as ResponseFactory;
14
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
15
use Psr\Http\Server\MiddlewareInterface;
16
17
/**
18
 * Middleware for access control.
19
 */
20
class AuthMiddleware implements MiddlewareInterface
21
{
22
    protected Authz $auth;
23
    protected ?ResponseFactory $responseFactory = null;
24
25
    /**
26
     * @var null|\Closure&callable(ServerRequest $request):SessionInterface
27
     */
28
    protected ?\Closure $getSession;
0 ignored issues
show
PHPDoc tag @var has invalid value (null|\Closure&callable(ServerRequest $request):SessionInterface): Unexpected token "&", expected TOKEN_OTHER at offset 29
Loading history...
29
30
    /**
31
     * @var \Closure&callable(ServerRequest $request):mixed
32
     * Function to get the required role from the request.
33
     */
34
    protected \Closure $getRequiredRole;
35
36
    /**
37
     * Class constructor
38
     *
39
     * @param Authz                                  $auth
40
     * @param callable(ServerRequest $request):mixed $getRequiredRole
41
     * @param ResponseFactory|null                   $responseFactory
42
     */
43 20
    public function __construct(Authz $auth, callable $getRequiredRole, ?ResponseFactory $responseFactory = null)
44
    {
45 20
        $this->auth = $auth;
46 20
        $this->responseFactory = $responseFactory;
47 20
        $this->getRequiredRole = \Closure::fromCallable($getRequiredRole);
48 20
    }
49
50
    /**
51
     * Get a copy of this middleware with a different session service.
52
     *
53
     * @param callable(ServerRequestInterface $request):SessionInterface $getSession
54
     * @return static
55
     */
56 4
    public function withSession(callable $getSession): self
57
    {
58 4
        $copy = clone $this;
59 4
        $copy->getSession = \Closure::fromCallable($getSession);
60
61 4
        return $copy;
62
    }
63
64
    /**
65
     * Process an incoming server request (PSR-15).
66
     *
67
     * @param ServerRequest  $request
68
     * @param RequestHandler $handler
69
     * @return Response
70
     */
71 13
    public function process(ServerRequest $request, RequestHandler $handler): Response
72
    {
73 13
        $this->initialize($request);
74
75 10
        if (!$this->isAllowed($request)) {
76 3
            return $this->forbidden($request);
77
        }
78
79 7
        return $handler->handle($request);
80
    }
81
82
    /**
83
     * Get a callback that can be used as double pass middleware.
84
     *
85
     * @return callable
86
     */
87 7
    public function asDoublePass(): callable
88
    {
89
        return function (ServerRequest $request, Response $response, callable $next): Response {
90 7
            $this->initialize($request);
91
92 7
            if (!$this->isAllowed($request)) {
93 2
                return $this->forbidden($request, $response);
94
            }
95
96 5
            return $next($request, $response);
97 7
        };
98
    }
99
100
    /**
101
     * Initialize the auth service.
102
     */
103 20
    protected function initialize(ServerRequest $request): void
104
    {
105 20
        if (!($this->auth instanceof Auth) || $this->auth->isInitialized()) {
106 16
            if (isset($this->getSession)) {
107 2
                throw new \LogicException("Session couldn't be used; auth already initialized");
108
            }
109 14
            return;
110
        }
111
112 4
        $session = $this->getSession($request);
113 3
        $this->auth->initialize($session);
114 3
    }
115
116
    /**
117
     * Return a session service for the server request.
118
     */
119 4
    protected function getSession(ServerRequest $request): ?SessionInterface
120
    {
121 4
        if (!isset($this->getSession)) {
122 2
            return null;
123
        }
124
125 2
        return i\type_check(
1 ignored issue
show
The function type_check was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

125
        return /** @scrutinizer ignore-call */ i\type_check(
Loading history...
126 2
            ($this->getSession)($request),
127 2
            SessionInterface::class,
128 2
            new \UnexpectedValueException()
129
        );
130
    }
131
132
    /**
133
     * Check if the request is allowed by the current user.
134
     */
135 17
    protected function isAllowed(ServerRequest $request): bool
136
    {
137 17
        $requiredRole = ($this->getRequiredRole)($request);
138
139 17
        if ($requiredRole === null) {
140 6
            return true;
141
        }
142
143 11
        if (is_bool($requiredRole)) {
144 7
            return $this->auth->isLoggedIn() === $requiredRole;
145
        }
146
147 4
        return Pipeline::with(is_array($requiredRole) ? $requiredRole : [$requiredRole])
148 4
            ->hasAny(fn($role) => $this->auth->is($role));
149
    }
150
151
    /**
152
     * Respond with forbidden (or unauthorized).
153
     */
154 5
    protected function forbidden(ServerRequest $request, ?Response $response = null): Response
155
    {
156 5
        $forbiddenResponse = $this->createResponse($this->auth->isLoggedIn() ? 403 : 401, $response)
157 4
            ->withProtocolVersion($request->getProtocolVersion());
158 4
        $forbiddenResponse->getBody()->write('Access denied');
159
160 4
        return $forbiddenResponse;
161
    }
162
163
    /**
164
     * Create a response using the response factory.
165
     *
166
     * @param int           $status            Response status
167
     * @param Response|null $originalResponse
168
     * @return Response
169
     */
170 5
    protected function createResponse(int $status, ?Response $originalResponse = null): Response
171
    {
172 5
        if ($this->responseFactory !== null) {
173 2
            return $this->responseFactory->createResponse($status);
174
        }
175
176 3
        if ($originalResponse !== null) {
177
            // There is no standard way to get an empty body without a factory. One of these methods may work.
178 2
            $body = clone $originalResponse->getBody();
179 2
            $body->rewind();
180
181 2
            return $originalResponse->withStatus($status)->withBody($body);
182
        }
183
184 1
        throw new \LogicException('Response factory not set');
185
    }
186
}
187