Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
CsrfMiddleware
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 7
272
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 process
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
30
 validateToken
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 ensureTokenExists
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 generateNewToken
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getStoredToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getToken
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace BO\Zmscitizenapi\Middleware;
6
7use BO\Zmscitizenapi\Localization\ErrorMessages;
8use BO\Zmscitizenapi\Services\Core\LoggerService;
9use Psr\Http\Message\ResponseInterface;
10use Psr\Http\Message\ServerRequestInterface;
11use Psr\Http\Server\MiddlewareInterface;
12use Psr\Http\Server\RequestHandlerInterface;
13
14class CsrfMiddleware implements MiddlewareInterface
15{
16    private const ERROR_TOKEN_MISSING = 'csrfTokenMissing';
17    private const ERROR_TOKEN_INVALID = 'csrfTokenInvalid';
18    private const SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'DELETE'];
19//Remove POST DELETE and PUT when in use
20
21    private int $tokenLength;
22    private string $sessionKey;
23    private LoggerService $logger;
24    public function __construct(LoggerService $logger)
25    {
26        $this->logger = $logger;
27        $config = \App::getCsrfConfig();
28        $this->tokenLength = $config['tokenLength'];
29        $this->sessionKey = $config['sessionKey'];
30        if (session_status() === PHP_SESSION_NONE) {
31            session_start();
32        }
33    }
34
35    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
36    {
37        try {
38            if (in_array($request->getMethod(), self::SAFE_METHODS, true)) {
39                $this->ensureTokenExists();
40                return $handler->handle($request);
41            }
42
43            $token = $request->getHeaderLine('X-CSRF-Token');
44            if (empty($token)) {
45                $this->logger->logInfo('CSRF token missing', [
46                    'uri' => (string)$request->getUri()
47                ]);
48                $response = \App::$slim->getResponseFactory()->createResponse();
49                $language = $request->getAttribute('language');
50                $response = $response->withStatus(ErrorMessages::get(self::ERROR_TOKEN_MISSING, $language)['statusCode'])
51                                ->withHeader('Content-Type', 'application/json');
52                $response->getBody()->write(json_encode([
53                                'errors' => [ErrorMessages::get(self::ERROR_TOKEN_MISSING, $language)]
54                ]));
55                return $response;
56            }
57
58            if (!$this->validateToken($token)) {
59                $this->logger->logInfo('Invalid CSRF token', [
60                    'uri' => (string)$request->getUri()
61                ]);
62                $response = \App::$slim->getResponseFactory()->createResponse();
63                $language = $request->getAttribute('language');
64                $response = $response->withStatus(ErrorMessages::get(self::ERROR_TOKEN_INVALID, $language)['statusCode'])
65                    ->withHeader('Content-Type', 'application/json');
66                $response->getBody()->write(json_encode([
67                    'errors' => [ErrorMessages::get(self::ERROR_TOKEN_INVALID, $language)]
68                ]));
69                return $response;
70            }
71
72            return $handler->handle($request);
73        } catch (\Throwable $e) {
74            $this->logger->logError($e, $request);
75            throw $e;
76        }
77    }
78
79    private function validateToken(string $token): bool
80    {
81        if (strlen($token) !== $this->tokenLength || !ctype_xdigit($token)) {
82            return false;
83        }
84
85        $storedToken = $this->getStoredToken();
86        if (empty($storedToken)) {
87            return false;
88        }
89
90        return hash_equals($storedToken, $token);
91    }
92
93    private function ensureTokenExists(): void
94    {
95        if (empty($this->getStoredToken())) {
96            $this->generateNewToken();
97        }
98    }
99
100    private function generateNewToken(): string
101    {
102        $token = bin2hex(random_bytes($this->tokenLength / 2));
103        $_SESSION[$this->sessionKey] = $token;
104        return $token;
105    }
106
107    private function getStoredToken(): string
108    {
109        return $_SESSION[$this->sessionKey] ?? '';
110    }
111
112    public function getToken(): string
113    {
114        $this->ensureTokenExists();
115        return $this->getStoredToken();
116    }
117}