Completed
Push — master ( e5262b...dd6e5a )
by Artem
02:03
created

CSRF::signCertificate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
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 $shouldProtectCallback
53
     * @param callable $getIdentityCallback
54
     * @param string $secret
55
     * @param string $attribute
56
     * @param int $ttl
57
     * @param string $algorithm
58
     */
59
    public function __construct(
60
        callable $shouldProtectCallback,
61
        callable $getIdentityCallback,
62
        string $secret,
63
        string $attribute = self::ATTRIBUTE,
64
        int $ttl = self::TTL,
65
        string $algorithm = self::ALGORITHM
66
    ) {
67
        $this->shouldProtectCallback = $shouldProtectCallback;
68
        $this->getIdentityCallback = $getIdentityCallback;
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 = $this->signCertificate($certificate);
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
            $actualSignature = $this->signCertificate($certificate);
132
            $isSignatureValid = hash_equals($actualSignature, $signature);
133
            $isNotExpired = $expireAt > time();
134
            if ($isSignatureValid && $isNotExpired) {
135
                return true;
136
            }
137
        }
138
139
        return false;
140
    }
141
142
    /**
143
     * @param array $identity
144
     * @param int $expireAt
145
     * @return string
146
     */
147
    protected function createCertificate(array $identity, int $expireAt): string
148
    {
149
        $identity[] = $expireAt;
150
151
        return implode(static::CERTIFICATE_SEPARATOR, $identity);
152
    }
153
154
    /**
155
     * @param string $certificate
156
     * @return string
157
     */
158
    protected function signCertificate(string $certificate)
159
    {
160
        return hash_hmac($this->algorithm, $certificate, $this->secret);
161
    }
162
163
    /**
164
     * @param ServerRequestInterface $request
165
     * @return array
166
     */
167
    protected function getIdentity(ServerRequestInterface $request)
168
    {
169
        $identity = call_user_func($this->getIdentityCallback, $request);
170
        if (!is_array($identity)) {
171
            $identity = [$identity];
172
        }
173
174
        return $identity;
175
    }
176
}