Issues (13)

src/ServerMiddleware.php (3 issues)

1
<?php declare(strict_types=1);
2
3
namespace Jasny\HttpSignature;
4
5
use Psr\Http\Message\ServerRequestInterface as ServerRequest;
6
use Psr\Http\Message\ResponseInterface as Response;
7
use Psr\Http\Message\ResponseFactoryInterface as ResponseFactory;
8
use Psr\Http\Server\MiddlewareInterface;
9
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
10
11
/**
12
 * Middleware to verify HTTP Signature authentication.
13
 * Can be used both as single pass (PSR-15) and double pass middleware.
14
 */
15
class ServerMiddleware implements MiddlewareInterface
16
{
17
    /**
18
     * @var HttpSignature
19
     */
20
    protected $service;
21
22
    /**
23
     * @var ResponseFactory|null
24
     */
25
    protected $responseFactory;
26
27
    /**
28
     * Class constructor.
29
     *
30
     * @param HttpSignature        $service
31
     * @param ResponseFactory|null $responseFactory
32
     */
33 10
    public function __construct(HttpSignature $service, ?ResponseFactory $responseFactory = null)
34
    {
35 10
        $this->service = $service;
36 10
        $this->responseFactory = $responseFactory;
37 10
    }
38
39
40
    /**
41
     * Process an incoming server request (PSR-15).
42
     *
43
     * @param ServerRequest  $request
44
     * @param RequestHandler $handler
45
     * @return Response
46
     * @throws \RuntimeException if unauthorized response can't be created
47
     */
48 5
    public function process(ServerRequest $request, RequestHandler $handler): Response
49
    {
50 5
        if (!$this->isRequestSigned($request)) {
51 2
            return $handler->handle($request);
52
        }
53
54
        $next = function (ServerRequest $request) use ($handler) {
0 ignored issues
show
Anonymous function should have native return typehint "Psr\Http\Message\ResponseInterface".
Loading history...
55 1
            return $handler->handle($request);
56 3
        };
57
58 3
        return $this->handleSignedRequest($request, null, $next);
59
    }
60
61
    /**
62
     * Get a callback that can be used as double pass middleware.
63
     *
64
     * @return callable
65
     */
66 5
    public function asDoublePass(): callable
67
    {
68
        return function (ServerRequest $request, Response $response, callable $next): Response {
69 5
            return $this->isRequestSigned($request)
70 3
                ? $this->handleSignedRequest($request, $response, $next)
71 5
                : $next($request, $response);
72 5
        };
73
    }
74
75
    /**
76
     * Handle signed request.
77
     *
78
     * @param ServerRequest  $request
79
     * @param Response|null  $response
80
     * @param callable       $next
81
     * @return Response
82
     * @throws \RuntimeException when the unauthorized response can't be created.
83
     */
84 6
    protected function handleSignedRequest(ServerRequest $request, ?Response $response, callable $next): Response
85
    {
86
        try {
87 6
            $keyId = $this->service->verify($request);
88 2
            $request = $request->withAttribute('signature_key_id', $keyId);
89
90 2
            $nextResponse = $next($request, $response);
91 4
        } catch (HttpSignatureException $exception) {
92 4
            $nextResponse = $this->createUnauthorizedResponse($request, $response, $exception->getMessage());
93
        }
94
95 5
        return $nextResponse;
96
    }
97
98
    /**
99
     * Check if the request contains a signature authorization header.
100
     *
101
     * @param ServerRequest $request
102
     * @return bool
103
     */
104 10
    protected function isRequestSigned(ServerRequest $request): bool
105
    {
106
        return
107 10
            $request->hasHeader('authorization') &&
108 10
            (substr(strtolower($request->getHeaderLine('authorization')), 0, 10) === 'signature ');
109
    }
110
111
    /**
112
     * Create a response using the response factory.
113
     *
114
     * @param int           $status            Response status
115
     * @param Response|null $originalResponse
116
     * @return Response
117
     */
118 4
    protected function createResponse(int $status, ?Response $originalResponse = null): Response
119
    {
120 4
        if ($this->responseFactory === null && $originalResponse === null) {
121 1
            throw new \BadMethodCallException('Response factory not set');
122
        }
123
124 3
        return $this->responseFactory !== null
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->responseFa...nalResponse->getBody()) could return the type null which is incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
125 2
            ? $this->responseFactory->createResponse($status)
126 3
            : $originalResponse->withStatus($status)->withBody(clone $originalResponse->getBody());
0 ignored issues
show
The method withStatus() does not exist on null. ( Ignorable by Annotation )

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

126
            : $originalResponse->/** @scrutinizer ignore-call */ withStatus($status)->withBody(clone $originalResponse->getBody());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
127
    }
128
129
    /**
130
     * Create a `401 Unauthorized` response.
131
     *
132
     * @param ServerRequest $request
133
     * @param Response|null $response
134
     * @param string        $message
135
     * @return Response
136
     * @throws \RuntimeException when can't write body.
137
     */
138 4
    protected function createUnauthorizedResponse(
139
        ServerRequest $request,
140
        ?Response $response,
141
        string $message
142
    ): Response {
143 4
        $newResponse = $this->createResponse(401, $response);
144
145 3
        $errorResponse = $this->service->setAuthenticateResponseHeader($request->getMethod(), $newResponse)
146 3
            ->withHeader('Content-Type', 'text/plain');
147
148 3
        $errorResponse->getBody()->write($message);
149
150 3
        return $errorResponse;
151
    }
152
}
153