Completed
Push — master ( 962d73...19e2e2 )
by Artem
02:26
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 $getIdentityCallback;
18
19
    /**
20
     * @var string
21
     */
22
    protected $secret;
23
24
    /**
25
     * @var string
26
     */
27
    protected $attribute;
28
29
    /**
30
     * @var int
31
     */
32
    protected $ttl;
33
34
    /**
35
     * @var string
36
     */
37
    protected $algorithm;
38
39
    protected const READ_METHODS = ['HEAD', 'GET', 'OPTIONS'];
40
    protected const STATUS_ON_ERROR = 403;
41
    protected const CERTIFICATE_SEPARATOR = ':';
42
    protected const ATTRIBUTE = 'csrf';
43
    protected const TTL = 60 * 20;
44
    protected const ALGORITHM = 'ripemd160';
45
46
    /**
47
     * @param callable $getIdentityCallback
48
     * @param string $secret
49
     * @param string $attribute
50
     * @param int $ttl
51
     * @param string $algorithm
52
     */
53
    public function __construct(
54
        callable $getIdentityCallback,
55
        string $secret,
56
        string $attribute = self::ATTRIBUTE,
57
        int $ttl = self::TTL,
58
        string $algorithm = self::ALGORITHM
59
    ) {
60
        $this->getIdentityCallback = $getIdentityCallback;
61
        $this->secret = $secret;
62
        $this->attribute = $attribute;
63
        $this->ttl = $ttl;
64
        $this->algorithm = $algorithm;
65
    }
66
67
    /**
68
     * @param ServerRequestInterface $request
69
     * @param RequestHandlerInterface $handler
70
     * @return ResponseInterface
71
     */
72
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
73
    {
74
        $isWriteMethod = !in_array($request->getMethod(), static::READ_METHODS);
75
        if ($isWriteMethod) {
76
            if (!$this->verify($request)) {
77
                $response = Factory::createResponse(static::STATUS_ON_ERROR);
78
                $response->getBody()->write('Invalid or missing CSRF token!');
79
80
                return $response;
81
            }
82
        }
83
84
        $request = $this->add($request);
85
86
        return $handler->handle($request);
87
    }
88
89
    /**
90
     * @param ServerRequestInterface $request
91
     * @return ServerRequestInterface $request
92
     */
93
    protected function add(ServerRequestInterface $request): ServerRequestInterface
94
    {
95
        $identity = $this->getIdentity($request);
96
        if (!empty($identity)) {
97
            $expireAt = time() + $this->ttl;
98
            $certificate = $this->createCertificate($identity, $expireAt);
99
            $signature = $this->signCertificate($certificate);
100
            $signatureWithExpiration = implode(static::CERTIFICATE_SEPARATOR, [$expireAt, $signature]);
101
102
            $request = $request->withAttribute($this->attribute, $signatureWithExpiration);
103
        }
104
105
        return $request;
106
    }
107
108
    /**
109
     * @param ServerRequestInterface $request
110
     * @return bool
111
     */
112
    protected function verify(ServerRequestInterface $request): bool
113
    {
114
        $token = trim($request->getParsedBody()[$this->attribute] ?? '');
115
        $parts = explode(static::CERTIFICATE_SEPARATOR, $token);
116
        if (count($parts) > 1) {
117
            list($expireAt, $signature) = explode(static::CERTIFICATE_SEPARATOR, $token);
118
            $identity = $this->getIdentity($request);
119
            $certificate = $this->createCertificate($identity, (int)$expireAt);
120
            $actualSignature = $this->signCertificate($certificate);
121
            $isSignatureValid = hash_equals($actualSignature, $signature);
122
            $isNotExpired = $expireAt > time();
123
            if ($isSignatureValid && $isNotExpired) {
124
                return true;
125
            }
126
        }
127
128
        return false;
129
    }
130
131
    /**
132
     * @param array $identity
133
     * @param int $expireAt
134
     * @return string
135
     */
136
    protected function createCertificate(array $identity, int $expireAt): string
137
    {
138
        $identity[] = $expireAt;
139
140
        return implode(static::CERTIFICATE_SEPARATOR, $identity);
141
    }
142
143
    /**
144
     * @param string $certificate
145
     * @return string
146
     */
147
    protected function signCertificate(string $certificate)
148
    {
149
        return hash_hmac($this->algorithm, $certificate, $this->secret);
150
    }
151
152
    /**
153
     * @param ServerRequestInterface $request
154
     * @return array
155
     */
156
    protected function getIdentity(ServerRequestInterface $request)
157
    {
158
        $identity = call_user_func($this->getIdentityCallback, $request);
159
        if (!is_array($identity)) {
160
            $identity = [$identity];
161
        }
162
163
        return $identity;
164
    }
165
}