Passed
Push — master ( 95abf0...1fa409 )
by Sebastian
01:51
created

CsrfGuard::getTimedToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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