Completed
Push — master ( 7ed94a...8bdf28 )
by Artem
02:18
created

CSRF::isValid()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 3
nop 1
dl 0
loc 17
rs 9.2
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 && !$this->isValid($request)) {
76
            $response = Factory::createResponse(static::STATUS_ON_ERROR);
77
            $response->getBody()->write('Invalid or missing CSRF token!');
78
79
            return $response;
80
        }
81
82
        $request = $this->add($request);
83
84
        return $handler->handle($request);
85
    }
86
87
    /**
88
     * @param ServerRequestInterface $request
89
     * @return ServerRequestInterface $request
90
     */
91
    protected function add(ServerRequestInterface $request): ServerRequestInterface
92
    {
93
        $identity = call_user_func($this->getIdentityCallback, $request);
94
        if (!empty($identity)) {
95
            $expireAt = time() + $this->ttl;
96
            $certificate = $this->createCertificate($identity, $expireAt);
97
            $signature = $this->signCertificate($certificate);
98
            $signatureWithExpiration = implode(static::CERTIFICATE_SEPARATOR, [$expireAt, $signature]);
99
100
            $request = $request->withAttribute($this->attribute, $signatureWithExpiration);
101
        }
102
103
        return $request;
104
    }
105
106
    /**
107
     * @param ServerRequestInterface $request
108
     * @return bool
109
     */
110
    protected function isValid(ServerRequestInterface $request): bool
111
    {
112
        $token = trim($request->getParsedBody()[$this->attribute] ?? '');
113
        $parts = explode(static::CERTIFICATE_SEPARATOR, $token);
114
        if (count($parts) > 1) {
115
            list($expireAt, $signature) = explode(static::CERTIFICATE_SEPARATOR, $token);
116
            $identity = call_user_func($this->getIdentityCallback, $request);
117
            $certificate = $this->createCertificate($identity, (int)$expireAt);
118
            $actualSignature = $this->signCertificate($certificate);
119
            $isSignatureValid = hash_equals($actualSignature, $signature);
120
            $isNotExpired = $expireAt > time();
121
            if ($isSignatureValid && $isNotExpired) {
122
                return true;
123
            }
124
        }
125
126
        return false;
127
    }
128
129
    /**
130
     * @param string $identity
131
     * @param int $expireAt
132
     * @return string
133
     */
134
    protected function createCertificate(string $identity, int $expireAt): string
135
    {
136
        return implode(static::CERTIFICATE_SEPARATOR, [$identity, $expireAt]);
137
    }
138
139
    /**
140
     * @param string $certificate
141
     * @return string
142
     */
143
    protected function signCertificate(string $certificate)
144
    {
145
        return hash_hmac($this->algorithm, $certificate, $this->secret);
146
    }
147
}
148