Passed
Push — master ( 84ad64...95abf0 )
by Sebastian
50s
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
     * 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 20
    public function __construct(int $maxStorage, int $tokenStrength)
45
    {
46 20
        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 19
        $_SESSION['CSRF'] = $_SESSION['CSRF'] ?? [];
54
55 19
        $this->session = &$_SESSION;
56 19
        $this->maxStorage = $maxStorage;
57 19
        $this->tokenStrength = $tokenStrength;
58 19
    }
59
60
    /**
61
     * Limit number of token stored in session.
62
     */
63 18
    private function dequeue(array &$array)
64
    {
65 18
        $size = count($array);
66
        
67 18
        while ($size > $this->maxStorage) {
68 14
            array_shift($array);
69 14
            $size--;
70
        }
71 18
    }
72
73
    /**
74
     * Return csrf token as array.
75
     *
76
     * @return array
77
     */
78 17
    public function getToken() : array
79
    {
80 17
        $token = $this->generateToken();
81
82 17
        $name = $token['name'];
83
84 17
        $this->session['CSRF'][$name] = $token;
85
86
        //storage cleaning!
87
        //warning!! if you get in a page more token of maximun storage,
88
        //will there a leak of token, the firsts generated
89
        //in future I think throw and exception.
90 17
        $this->dequeue($this->session['CSRF']);
91
92 17
        return $token;
93
    }
94
95
    /**
96
     * Return timed csrf token as array.
97
     *
98
     * @param int $ttl Time to live for the token.
99
     *
100
     * @return array
101
     */
102 1
    public function getTimedToken(int $ttl) : array
103
    {
104 1
        $token = $this->generateToken();
105 1
        $token['time'] = time() + $ttl;
106
        
107 1
        $name = $token['name'];
108
109 1
        $this->session['CSRF'][$name] = $token;
110
111 1
        $this->dequeue($this->session['CSRF']);
112
113 1
        return $token;
114
    }
115
    
116
    /**
117
     * Generate a random token.
118
     *
119
     * @return array
120
     */
121 18
    private function generateToken() : array
122
    {
123 18
        $name = 'csrf_'.bin2hex(random_bytes(8));
124 18
        $value = bin2hex(random_bytes($this->tokenStrength));
125
        
126 18
        return ['name' => $name, 'value' => $value];
127
    }
128
    
129
    /**
130
     * Return csrf token as hidden input form.
131
     *
132
     * @return string
133
     *
134
     * @deprecated since version 1.1.0
135
     */
136 1
    public function getHiddenInput() : string
137
    {
138 1
        $token = $this->getToken();
139
140 1
        return '<input type="hidden" name="'.$token['name'].'" value="'.$token['value'].'" />';
141
    }
142
143
    /**
144
     * Validate a csrf token.
145
     *
146
     * @param array $requestData From request or from superglobal variables $_POST,
147
     *                           $_GET, $_REQUEST and $_COOKIE.
148
     *
149
     * @return bool
150
     */
151 1
    public function validate(array $requestData) : bool
152
    {
153
        //apply matchToken method elements of passed data,
154
        //using this instead of forach for code shortness.
155 1
        $array = array_filter($requestData, array($this, 'matchToken'), ARRAY_FILTER_USE_BOTH);
156
157 1
        return (bool) count($array);
158
    }
159
    
160
    /**
161
     * Tests for valid token.
162
     *
163
     * @param string $value
164
     * @param string $key
165
     *
166
     * @return bool
167
     */
168 1
    private function matchToken(string $value, string $key) : bool
169
    {
170 1
        $tokens = $this->session['CSRF'];
171
172
        //check if token exist
173 1
        if (!isset($tokens[$key])) {
174 1
            return false;
175
        }
176
177
        //check if token is valid
178 1
        if (!hash_equals($tokens[$key]['value'], $value)) {
179 1
            return false;
180
        }
181
182
        //check if token is expired if timed
183 1
        if (isset($tokens[$key]['time']) && $tokens[$key]['time'] < time()) {
184
            return false;
185
        }
186
 
187 1
        return true;
188
    }
189
}
190