Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
ApiSignatureMiddleware
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
3 / 3
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 process
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
8
 unauthorized
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace App\Shared\Security;
6
7use Psr\Http\Message\ResponseFactoryInterface;
8use Psr\Http\Message\ResponseInterface;
9use Psr\Http\Message\ServerRequestInterface;
10use Psr\Http\Server\MiddlewareInterface;
11use Psr\Http\Server\RequestHandlerInterface;
12
13 class ApiSignatureMiddleware implements MiddlewareInterface
14{
15    private string $secretKey;
16
17    public function __construct(
18        private ResponseFactoryInterface $responseFactory,
19        string $secretKey = 'ammonly_client_secret_key'
20    ) {
21        $this->secretKey = $secretKey;
22    }
23
24    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
25    {
26        $path = $request->getUri()->getPath();
27        if (!str_starts_with($path, '/api/v1/')) {
28            return $handler->handle($request);
29        }
30
31        $apiKey = $request->getHeaderLine('X-Api-Key');
32        $timestamp = $request->getHeaderLine('X-Api-Timestamp');
33        $signature = $request->getHeaderLine('X-Api-Signature');
34        $response = null;
35
36        if ($apiKey === '' || $timestamp === '' || $signature === '') {
37            $response = $this->unauthorized('Missing API authentication headers');
38        } elseif (abs(time() - (int) $timestamp) > 300) {
39            $response = $this->unauthorized('Request timestamp out of bounds');
40        } else {
41            $bodyStream = $request->getBody();
42            $body = (string) $bodyStream;
43            if ($bodyStream->isSeekable()) {
44                $bodyStream->rewind();
45            }
46
47            $payload = $timestamp . $request->getMethod() . $path . $body;
48            $expectedSignature = hash_hmac('sha256', $payload, $this->secretKey);
49
50            if (!hash_equals($expectedSignature, $signature)) {
51                $response = $this->unauthorized('Invalid API Signature');
52            }
53        }
54
55        return $response ?? $handler->handle($request);
56    }
57
58    private function unauthorized(string $message): ResponseInterface
59    {
60        $response = $this->responseFactory->createResponse(401)
61            ->withHeader('Content-Type', 'application/json');
62        $response->getBody()->write((string) json_encode([
63            'error' => 'Unauthorized',
64            'message' => $message,
65        ]));
66        return $response;
67    }
68}