Passed
Push — master ( ec1f88...abc159 )
by Artem
02:06
created

CSRF::getIdentity()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
declare(strict_types = 1);
3
4
namespace Zakirullin\Middlewares;
5
6
use Psr\Http\Server\MiddlewareInterface;
7
use Psr\Http\Server\RequestHandlerInterface;
8
use Psr\Http\Message\ServerRequestInterface;
9
use Psr\Http\Message\ResponseInterface;
10
use Middlewares\Utils\Factory;
11
12
class CSRF implements MiddlewareInterface
13
{
14
    /**
15
     * @var callable
16
     */
17
    protected $shouldProtectCallback;
18
19
    /**
20
     * @var callable
21
     */
22
    protected $getIdentityCallback;
23
24
    /**
25
     * @var string
26
     */
27
    protected $secret;
28
29
    /**
30
     * @var string
31
     */
32
    protected $attribute;
33
34
    /**
35
     * @var int
36
     */
37
    protected $ttl;
38
39
    /**
40
     * @var string
41
     */
42
    protected $algorithm;
43
44
    protected const METHODS = ['POST'];
45
    protected const STATUS_ON_ERROR = 403;
46
    protected const CERTIFICATE_SEPARATOR = ':';
47
    protected const ATTRIBUTE = 'csrf';
48
    protected const TTL = 60 * 20;
49
    protected const ALGORITHM = 'ripemd160';
50
51
    /**
52
     * @param callable $shouldProtect
53
     * @param callable $getIdentity
54
     * @param string $secret
55
     * @param string $attribute
56
     * @param int $ttl
57
     * @param string $algorithm
58
     */
59
    public function __construct(
60
        callable $shouldProtect,
61
        callable $getIdentity,
62
        string $secret,
63
        string $attribute = self::ATTRIBUTE,
64
        int $ttl = self::TTL,
65
        string $algorithm = self::ALGORITHM
66
    ) {
67
        $this->shouldProtectCallback = $shouldProtect;
68
        $this->getIdentityCallback = $getIdentity;
69
        $this->secret = $secret;
70
        $this->attribute = $attribute;
71
        $this->ttl = $ttl;
72
        $this->algorithm = $algorithm;
73
    }
74
75
    /**
76
     * @param ServerRequestInterface $request
77
     * @param RequestHandlerInterface $handler
78
     * @return ResponseInterface
79
     */
80
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
81
    {
82
        $shouldProtect = call_user_func($this->shouldProtectCallback, $request);
83
        if ($shouldProtect) {
84
            $shouldProtectMethod = in_array($request->getMethod(), static::METHODS);
85
            if ($shouldProtectMethod) {
86
                if (!$this->verify($request)) {
87
                    $response = Factory::createResponse(static::STATUS_ON_ERROR);
88
                    $response->getBody()->write('Invalid or missing CSRF token!');
89
90
                    return $response;
91
                }
92
            }
93
94
            $request = $this->add($request);
95
        }
96
97
        return $handler->handle($request);
98
    }
99
100
    /**
101
     * @param ServerRequestInterface $request
102
     * @return ServerRequestInterface $request
103
     */
104
    protected function add(ServerRequestInterface $request): ServerRequestInterface
105
    {
106
        $identity = $this->getIdentity($request);
107
        if (!empty($identity)) {
108
            $expireAt = time() + $this->ttl;
109
            $certificate = $this->createCertificate($identity, $expireAt);
110
            $signature = hash_hmac($this->algorithm, $certificate, $this->secret);
111
            $signatureWithExpiration = implode(static::CERTIFICATE_SEPARATOR, [$expireAt, $signature]);
112
113
            $request = $request->withAttribute($this->attribute, $signatureWithExpiration);
114
        }
115
116
        return $request;
117
    }
118
119
    /**
120
     * @param ServerRequestInterface $request
121
     * @return bool
122
     */
123
    protected function verify(ServerRequestInterface $request): bool
124
    {
125
        $token = trim($request->getParsedBody()[$this->attribute] ?? '');
126
        $parts = explode(static::CERTIFICATE_SEPARATOR, $token);
127
        if (count($parts) > 1) {
128
            list($expireAt, $signature) = explode(static::CERTIFICATE_SEPARATOR, $token);
129
            $identity = $this->getIdentity($request);
130
            $certificate = $this->createCertificate($identity, (int)$expireAt);
131
132
            $actualSignature = hash_hmac($this->algorithm, $certificate, $this->secret);
133
            $isSignatureValid = hash_equals($actualSignature, $signature);
134
            $isNotExpired = $expireAt > time();
135
            if ($isSignatureValid && $isNotExpired) {
136
                return true;
137
            }
138
        }
139
140
        return false;
141
    }
142
143
    /**
144
     * @param array $identity
145
     * @param int $expireAt
146
     * @return string
147
     */
148
    protected function createCertificate(array $identity, int $expireAt): string
149
    {
150
        $identity[] = $expireAt;
151
152
        return implode(static::CERTIFICATE_SEPARATOR, $identity);
153
    }
154
155
    /**
156
     * @param ServerRequestInterface $request
157
     * @return array
158
     */
159
    protected function getIdentity(ServerRequestInterface $request)
160
    {
161
        $identity = call_user_func($this->getIdentityCallback, $request);
162
        if (!is_array($identity)) {
163
            $identity = [$identity];
164
        }
165
166
        return $identity;
167
    }
168
}