Completed
Push — master ( 08aa50...0440a7 )
by Sebastian
01:45
created

CsrfGuard::deleteToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 2
crap 1
1
<?php
2
3
/**
4
 * Linna Cross-site Request Forgery Guard
5
 *
6
 * @author Sebastian Rapetti <[email protected]>
7
 * @copyright (c) 2017, Sebastian Rapetti
8
 * @license http://opensource.org/licenses/MIT MIT License
9
 */
10
declare(strict_types=1);
11
12
namespace Linna;
13
14
use RuntimeException;
15
16
/**
17
 * Cross-site Request Forgery Guard
18
 */
19
class CsrfGuard
20
{
21
    /**
22
     * @var array Php session data from superglobal.
23
     */
24
    private $session;
25
26
    /**
27
     * @var int Max number of tokens stored in session.
28
     */
29
    private $maxStorage;
30
31
    /**
32
     * @var int Rapresent the lenght of the token in bytes.
33
     */
34
    private $tokenStrength;
35
    
36
    /**
37
     * Class constructor.
38
     *
39
     * @param int $maxStorage    Max number of tokens stored in session, work as
40
     *                           FIFO data structure, when maximun capacity is
41
     *                           reached, oldest token be dequeued from storage.
42
     * @param int $tokenStrength Rapresent the lenght of the token in bytes.
43
     */
44 23
    public function __construct(int $maxStorage, int $tokenStrength)
45
    {
46 23
        if (session_status() === 1) {
47 1
            throw new RuntimeException(__CLASS__.': Session must be started before create '.__CLASS__.' instance.');
48
        }
49
        
50
        //if csrf array doesn't exist inside session, initialize it.
51
        //for code shortness: Null coalescing operator
52
        //http://php.net/manual/en/migration70.new-features.php
53 22
        $_SESSION['CSRF'] = $_SESSION['CSRF'] ?? [];
54
55 22
        $this->session = &$_SESSION;
56 22
        $this->maxStorage = $maxStorage;
57 22
        $this->tokenStrength = $tokenStrength;
58 22
    }
59
60
    /**
61
     * Limit number of token stored in session.
62
     * 
63
     * @param array $array
64
     */
65 21
    private function dequeue(array &$array)
66
    {
67 21
        $size = count($array);
68
        
69 21
        while ($size > $this->maxStorage) {
70 14
            array_shift($array);
71 14
            $size--;
72
        }
73 21
    }
74
75
    /**
76
     * Return csrf token as array.
77
     *
78
     * @return array
79
     */
80 18
    public function getToken() : array
81
    {
82 18
        $token = $this->generateToken();
83
84 18
        $name = $token['name'];
85
86 18
        $this->session['CSRF'][$name] = $token;
87
88
        //storage cleaning!
89
        //warning!! if you get in a page more token of maximun storage,
90
        //will there a leak of token, the firsts generated
91
        //in future I think throw and exception.
92 18
        $this->dequeue($this->session['CSRF']);
93
94 18
        return $token;
95
    }
96
97
    /**
98
     * Return timed csrf token as array.
99
     *
100
     * @param int $ttl Time to live for the token.
101
     *
102
     * @return array
103
     */
104 3
    public function getTimedToken(int $ttl) : array
105
    {
106 3
        $token = $this->generateToken();
107 3
        $token['time'] = time() + $ttl;
108
        
109 3
        $name = $token['name'];
110
111 3
        $this->session['CSRF'][$name] = $token;
112
113 3
        $this->dequeue($this->session['CSRF']);
114
115 3
        return $token;
116
    }
117
    
118
    /**
119
     * Generate a random token.
120
     *
121
     * @return array
122
     */
123 21
    private function generateToken() : array
124
    {
125 21
        $name = 'csrf_'.bin2hex(random_bytes(8));
126 21
        $value = bin2hex(random_bytes($this->tokenStrength));
127
        
128 21
        return ['name' => $name, 'value' => $value];
129
    }
130
    
131
    /**
132
     * Return csrf token as hidden input form.
133
     *
134
     * @return string
135
     *
136
     * @deprecated since version 1.1.0
137
     */
138 1
    public function getHiddenInput() : string
139
    {
140 1
        $token = $this->getToken();
141
142 1
        return '<input type="hidden" name="'.$token['name'].'" value="'.$token['value'].'" />';
143
    }
144
145
    /**
146
     * Validate a csrf token or a csrf timed token.
147
     *
148
     * @param array $requestData From request or from superglobal variables $_POST,
149
     *                           $_GET, $_REQUEST and $_COOKIE.
150
     *
151
     * @return bool
152
     */
153 4
    public function validate(array $requestData) : bool
154
    {
155
        //apply matchToken method elements of passed data,
156
        //using this instead of forach for code shortness.
157 4
        $array = array_filter($requestData, array($this, 'doChecks'), ARRAY_FILTER_USE_BOTH);
158
159 4
        return (bool) count($array);
160
    }
161
    
162
    /**
163
     * Tests for valid token.
164
     *
165
     * @param string $value
166
     * @param string $key
167
     *
168
     * @return bool
169
     */
170 4
    private function doChecks(string $value, string $key) : bool
171
    {
172 4
        $tokens = &$this->session['CSRF'];
173
174 4
        return $this->tokenIsValid($tokens, $value, $key) &&
175 4
               $this->tokenIsExiperd($tokens, $key)  &&
176 4
               $this->deleteToken($tokens, $key);
177
    }
178
179
    /**
180
     * Delete token after validation.
181
     *
182
     * @param array $tokens
183
     * @param string $key
184
     * @return bool
185
     */
186 3
    private function deleteToken(array &$tokens, string &$key) : bool
187
    {
188 3
        unset($tokens[$key]);
189
190 3
        return true;
191
    }
192
    
193
    /**
194
     * Check if token is valid
195
     *
196
     * @param array $tokens
197
     * @param string $value
198
     * @param string $key
199
     *
200
     * @return bool
201
     */
202 4
    private function tokenIsValid(array &$tokens, string &$value, string &$key) : bool
203
    {
204
        //if token exist
205 4
        if (!isset($tokens[$key])) {
206 2
            return false;
207
        }
208
209
        //if token has valid value
210 4
        if (!hash_equals($tokens[$key]['value'], $value)) {
211 1
            return false;
212
        }
213
214 4
        return true;
215
    }
216
217
    /**
218
     * Check if timed token is expired.
219
     *
220
     * @param array $tokens
221
     * @param string $key
222
     *
223
     * @return bool
224
     */
225 4
    private function tokenIsExiperd(array &$tokens, string &$key) : bool
226
    {
227
        //if timed and if time is valid
228 4
        if (isset($tokens[$key]['time']) && $tokens[$key]['time'] < time()) {
229 1
            return false;
230
        }
231
232 3
        return true;
233
    }
234
}
235