1+ <?php
2+
3+ declare (strict_types=1 );
4+
5+ namespace App \Infrastructure \Http \Middleware \Csrf ;
6+
7+ use App \Infrastructure \Http \Middleware \Csrf \Traits \CsrfTokenAware ;
8+ use App \Shared \Http \RequestMethod ;
9+ use Psr \Http \Message \ResponseInterface ;
10+ use Psr \Http \Message \ServerRequestInterface ;
11+ use Psr \Http \Server \MiddlewareInterface ;
12+ use Psr \Http \Server \RequestHandlerInterface ;
13+ use Qubus \Config \ConfigContainer ;
14+ use Qubus \Exception \Exception ;
15+ use Qubus \Http \Factories \JsonResponseFactory ;
16+ use Qubus \Http \Session \SessionService ;
17+
18+ final class CsrfProtectionMiddleware implements MiddlewareInterface
19+ {
20+ use CsrfTokenAware;
21+
22+ public function __construct (protected ConfigContainer $ configContainer , protected SessionService $ sessionService )
23+ {
24+ }
25+
26+ /**
27+ * @inheritDoc
28+ * @throws Exception
29+ * @throws \Exception
30+ */
31+ public function process (ServerRequestInterface $ request , RequestHandlerInterface $ handler ): ResponseInterface
32+ {
33+ $ response = $ handler ->handle ($ request );
34+
35+ if (true === $ this ->needsProtection ($ request ) && ! $ this ->tokensMatch ($ request )) {
36+ return JsonResponseFactory::create (
37+ data: 'Bad CSRF Token. ' ,
38+ status: $ this ->configContainer ->getConfigKey ('csrf.error_status_code ' )
39+ );
40+ }
41+
42+ return $ response ;
43+ }
44+
45+ /**
46+ * Check for methods not defined as safe.
47+ *
48+ * @param ServerRequestInterface $request
49+ * @return bool
50+ */
51+ private function needsProtection (ServerRequestInterface $ request ): bool
52+ {
53+ return RequestMethod::isSafe ($ request ->getMethod ()) === false ;
54+ }
55+
56+ /**
57+ * @throws Exception
58+ */
59+ private function tokensMatch (ServerRequestInterface $ request ): bool
60+ {
61+ $ expected = $ this ->fetchToken ($ request );
62+ $ provided = $ this ->getTokenFromRequest ($ request );
63+
64+ return hash_equals ($ expected , $ provided );
65+ }
66+
67+
68+ /**
69+ * @throws Exception
70+ * @throws \Exception
71+ */
72+ private function fetchToken (ServerRequestInterface $ request ): string
73+ {
74+ $ token = $ request ->getAttribute (CsrfTokenMiddleware::SESSION_ATTRIBUTE );
75+
76+ // Ensure the token stored previously by the CsrfTokenMiddleware is present and has a valid format.
77+ if (is_string ($ token ) &&
78+ ctype_alnum ($ token ) &&
79+ strlen ($ token ) === $ this ->configContainer ->getConfigKey (key: 'csrf.csrf_token_length ' )
80+ ) {
81+ return $ token ;
82+ }
83+
84+ return '' ;
85+ }
86+
87+ /**
88+ * @throws Exception
89+ */
90+ private function getTokenFromRequest (ServerRequestInterface $ request ): string
91+ {
92+ if ($ request ->hasHeader ($ this ->configContainer ->getConfigKey (key: 'csrf.header ' ))) {
93+ return (string ) $ request ->getHeaderLine ($ this ->configContainer ->getConfigKey (key: 'csrf.header ' ));
94+ }
95+
96+ // Handle the case for a POST form.
97+ $ body = $ request ->getParsedBody ();
98+
99+ if (is_array (
100+ $ body ) &&
101+ isset ($ body [$ this ->configContainer ->getConfigKey (key: 'csrf.csrf_token ' )]) &&
102+ is_string ($ body [$ this ->configContainer ->getConfigKey (key: 'csrf.csrf_token ' )])) {
103+ return $ body [$ this ->configContainer ->getConfigKey (key: 'csrf.csrf_token ' )];
104+ }
105+
106+ return '' ;
107+ }
108+ }
0 commit comments