AuthMiddleware::isAllowed()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 14
ccs 8
cts 8
cp 1
rs 10
cc 4
nc 3
nop 1
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Jasny\Auth;
6
7
use Closure;
8
use Improved as i;
9
use Improved\IteratorPipeline\Pipeline;
10
use Jasny\Auth\AuthzInterface as Authz;
11
use Jasny\Auth\Session\SessionInterface;
12
use LogicException;
13
use Psr\Http\Message\ServerRequestInterface as ServerRequest;
14
use Psr\Http\Message\ResponseInterface as Response;
15
use Psr\Http\Message\ResponseFactoryInterface as ResponseFactory;
16
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
17
use Psr\Http\Server\MiddlewareInterface;
18
use UnexpectedValueException;
19
20
/**
21
 * Middleware for access control.
22
 */
23
class AuthMiddleware implements MiddlewareInterface
24
{
25
    protected Authz $auth;
26
    protected ?ResponseFactory $responseFactory = null;
27
28
    /**
29
     * @var null|Closure(ServerRequest $request):SessionInterface
30
     */
31
    protected ?Closure $getSession;
32
33
    /**
34
     * @var Closure(ServerRequest $request):mixed
35
     * Function to get the required role from the request.
36
     */
37
    protected Closure $getRequiredRole;
38
39
    /**
40
     * Class constructor
41
     *
42
     * @param Authz                                  $auth
43
     * @param callable(ServerRequest $request):mixed $getRequiredRole
44
     * @param ResponseFactory|null                   $responseFactory
45
     */
46 18
    public function __construct(Authz $auth, callable $getRequiredRole, ?ResponseFactory $responseFactory = null)
47
    {
48 18
        $this->auth = $auth;
49 18
        $this->responseFactory = $responseFactory;
50 18
        $this->getRequiredRole = $getRequiredRole(...);
51
    }
52
53
    /**
54
     * Get a copy of this middleware with a different session service.
55
     *
56
     * @param callable(ServerRequest $request):SessionInterface $getSession
57
     * @return static
58
     */
59 3
    public function withSession(callable $getSession): self
60
    {
61 3
        $copy = clone $this;
62 3
        $copy->getSession = $getSession(...);
63
64 3
        return $copy;
65
    }
66
67
    /**
68
     * Process an incoming server request (PSR-15).
69
     *
70
     * @param ServerRequest  $request
71
     * @param RequestHandler $handler
72
     * @return Response
73
     */
74 11
    public function process(ServerRequest $request, RequestHandler $handler): Response
75
    {
76 11
        $this->initialize($request);
77
78 9
        if (!$this->isAllowed($request)) {
79 3
            return $this->forbidden($request);
80
        }
81
82 6
        return $handler->handle($request);
83
    }
84
85
    /**
86
     * Get a callback that can be used as double pass middleware.
87
     *
88
     * @return callable
89
     */
90 7
    public function asDoublePass(): callable
91
    {
92 7
        return function (ServerRequest $request, Response $response, callable $next): Response {
93 7
            $this->initialize($request);
94
95 7
            if (!$this->isAllowed($request)) {
96 2
                return $this->forbidden($request, $response);
97
            }
98
99 5
            return $next($request, $response);
100 7
        };
101
    }
102
103
    /**
104
     * Initialize the auth service.
105
     */
106 18
    protected function initialize(ServerRequest $request): void
107
    {
108 18
        if (!$this->auth instanceof Auth) {
109 14
            if (isset($this->getSession)) {
110 1
                throw new LogicException("Session can't be used for immutable authz service");
111
            }
112 13
            return;
113
        }
114
115 4
        $session = $this->getSession($request);
116 3
        $this->auth->initialize($session);
117
    }
118
119
    /**
120
     * Return a session service for the server request.
121
     */
122 4
    protected function getSession(ServerRequest $request): ?SessionInterface
123
    {
124 4
        if (!isset($this->getSession)) {
125 2
            return null;
126
        }
127
128 2
        return i\type_check(
129 2
            ($this->getSession)($request),
130 2
            SessionInterface::class,
131 2
            new UnexpectedValueException()
132 2
        );
133
    }
134
135
    /**
136
     * Check if the request is allowed by the current user.
137
     */
138 16
    protected function isAllowed(ServerRequest $request): bool
139
    {
140 16
        $requiredRole = ($this->getRequiredRole)($request);
141
142 16
        if ($requiredRole === null) {
143 5
            return true;
144
        }
145
146 11
        if (is_bool($requiredRole)) {
147 7
            return $this->auth->isLoggedIn() === $requiredRole;
148
        }
149
150 4
        return Pipeline::with(is_array($requiredRole) ? $requiredRole : [$requiredRole])
151 4
            ->hasAny(fn($role) => $this->auth->is($role));
152
    }
153
154
    /**
155
     * Respond with forbidden (or unauthorized).
156
     */
157 5
    protected function forbidden(ServerRequest $request, ?Response $response = null): Response
158
    {
159 5
        $forbiddenResponse = $this->createResponse($this->auth->isLoggedIn() ? 403 : 401, $response)
160 5
            ->withProtocolVersion($request->getProtocolVersion());
161 4
        $forbiddenResponse->getBody()->write('Access denied');
162
163 4
        return $forbiddenResponse;
164
    }
165
166
    /**
167
     * Create a response using the response factory.
168
     *
169
     * @param int           $status            Response status
170
     * @param Response|null $originalResponse
171
     * @return Response
172
     */
173 5
    protected function createResponse(int $status, ?Response $originalResponse = null): Response
174
    {
175 5
        if ($this->responseFactory !== null) {
176 2
            return $this->responseFactory->createResponse($status);
177
        }
178
179 3
        if ($originalResponse !== null) {
180
            // There is no standard way to get an empty body without a factory. One of these methods may work.
181 2
            $body = clone $originalResponse->getBody();
182 2
            $body->rewind();
183
184 2
            return $originalResponse->withStatus($status)->withBody($body);
185
        }
186
187 1
        throw new LogicException('Response factory not set');
188
    }
189
}
190