Test Failed
Push — master ( 8bdf28...10fc67 )
by Artem
02:19
created

CSRF::signRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 6
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 && !$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
        $expireAt = time() + $this->ttl;
94
        $signature = $this->signRequest($request, $expireAt);
95
        $signatureWithExpiration = implode(static::CERTIFICATE_SEPARATOR, [$expireAt, $signature]);
96
97
        $request = $request->withAttribute($this->attribute, $signatureWithExpiration);
98
99
        return $request;
100
    }
101
102
    /**
103
     * @param ServerRequestInterface $request
104
     * @return bool
105
     */
106
    protected function isValid(ServerRequestInterface $request): bool
107
    {
108
        $token = trim($request->getParsedBody()[$this->attribute] ?? '');
109
        $parts = explode(static::CERTIFICATE_SEPARATOR, $token);
110
        if (count($parts) > 1) {
111
            list($expireAt, $signature) = explode(static::CERTIFICATE_SEPARATOR, $token);
112
            $actualSignature = $this->signRequest($request, $expireAt);
0 ignored issues
show
Bug introduced by
$expireAt of type string is incompatible with the type integer expected by parameter $expireAt of Zakirullin\Middlewares\CSRF::signRequest(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

112
            $actualSignature = $this->signRequest($request, /** @scrutinizer ignore-type */ $expireAt);
Loading history...
113
            $isSignatureValid = hash_equals($actualSignature, $signature);
114
            $isNotExpired = $expireAt > time();
115
            if ($isSignatureValid && $isNotExpired) {
116
                return true;
117
            }
118
        }
119
120
        return false;
121
    }
122
123
    /**
124
     * @param string $identity
125
     * @param int $expireAt
126
     * @return string
127
     */
128
    protected function createCertificate(string $identity, int $expireAt): string
129
    {
130
        return implode(static::CERTIFICATE_SEPARATOR, [$identity, $expireAt]);
131
    }
132
133
    /**
134
     * @param string $certificate
135
     * @return string
136
     */
137
    protected function signCertificate(string $certificate)
138
    {
139
        return hash_hmac($this->algorithm, $certificate, $this->secret);
140
    }
141
142
    /**
143
     * @param ServerRequestInterface $request
144
     * @param int $expireAt
145
     * @return string
146
     */
147
    protected function signRequest(ServerRequestInterface $request, int $expireAt)
148
    {
149
        $identity = call_user_func($this->getIdentityCallback, $request);
150
        $certificate = $this->createCertificate($identity, $expireAt);
151
152
        return $this->signCertificate($certificate);
153
    }
154
}
155