CsrfTokenStorage   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 226
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 22
lcom 1
cbo 0
dl 0
loc 226
ccs 60
cts 60
cp 1
rs 10
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A create() 0 23 3
A check() 0 12 2
A getSessionStorage() 0 4 1
A setSessionStorage() 0 6 1
A getTokenStorageKey() 0 4 1
A setTokenStorageKey() 0 8 1
A getMaxTokens() 0 4 1
A setMaxTokens() 0 8 3
A getMaxTokensGcThreshold() 0 4 1
A setMaxTokensGcThreshold() 0 8 2
A createTokenValue() 0 6 1
A createTokenTimestamp() 0 4 1
A getTokenStorage() 0 10 2
A setTokenStorage() 0 6 1
1
<?php declare(strict_types=1);
2
3
namespace Limoncello\Application\Packages\Csrf;
4
5
/**
6
 * Copyright 2015-2020 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use ArrayAccess;
22
use Exception;
23
use Limoncello\Application\Contracts\Csrf\CsrfTokenStorageInterface;
24
use function array_key_exists;
25
use function array_slice;
26
use function asort;
27
use function assert;
28
use function bin2hex;
29
use function count;
30
use function random_bytes;
31
use function time;
32
33
/**
34
 * @package Limoncello\Application
35
 */
36
class CsrfTokenStorage implements CsrfTokenStorageInterface
37
{
38
    /**
39
     * Number of random bytes in a token.
40
     */
41
    protected const TOKEN_BYTE_LENGTH = 16;
42
43
    /**
44
     * @var ArrayAccess
45
     */
46
    private $sessionStorage;
47
48
    /**
49
     * @var string
50
     */
51
    private $tokenStorageKey;
52
53
    /**
54
     * @var null|int
55
     */
56
    private $maxTokens = null;
57
58
    /**
59 3
     * @var int
60
     */
61
    private $maxTokensGcThreshold;
62
63
    /**
64
     * @param ArrayAccess $sessionStorage
65 3
     * @param string      $tokenStorageKey
66 3
     * @param int|null    $maxTokens
67 3
     * @param int         $maxTokensGcThreshold
68 3
     */
69
    public function __construct(
70
        ArrayAccess $sessionStorage,
71
        string $tokenStorageKey,
72
        ?int $maxTokens,
73
        int $maxTokensGcThreshold
74
    ) {
75
        $this->setSessionStorage($sessionStorage)
76 2
            ->setTokenStorageKey($tokenStorageKey)
77
            ->setMaxTokens($maxTokens)
78 2
            ->setMaxTokensGcThreshold($maxTokensGcThreshold);
79 2
    }
80 2
81
    /**
82 2
     * @inheritdoc
83
     *
84
     * @throws Exception
85 2
     */
86 2
    public function create(): string
87 2
    {
88
        $tokenStorage = $this->getTokenStorage();
89
        $value        = $this->createTokenValue();
90 1
        $timestamp    = $this->createTokenTimestamp();
91 1
92
        $tokenStorage[$value] = $timestamp;
93
94
        // check if we should limit number to stored tokens
95 2
        $maxTokens = $this->getMaxTokens();
96
        if ($maxTokens !== null &&
97 2
            count($tokenStorage) > $maxTokens + $this->getMaxTokensGcThreshold()
98
        ) {
99
            // sort by timestamp and take last $maxTokens
100
            asort($tokenStorage, SORT_NUMERIC);
101
            $tokenStorage = array_slice($tokenStorage, -$maxTokens, null, true);
102
            // minus means count from the end ---------^
103 1
        }
104
105 1
        $this->setTokenStorage($tokenStorage);
106 1
107 1
        return $value;
108
    }
109 1
110 1
    /**
111
     * @inheritdoc
112
     */
113 1
    public function check(string $token): bool
114
    {
115
        $tokenStorage = $this->getTokenStorage();
116
        $tokenFound   = array_key_exists($token, $tokenStorage);
117
        if ($tokenFound === true) {
118
            // remove the token so it cannot be used again
119 2
            unset($tokenStorage[$token]);
120
            $this->setTokenStorage($tokenStorage);
121 2
        }
122
123
        return $tokenFound;
124
    }
125
126
    /**
127
     * @return ArrayAccess
128
     */
129 3
    protected function getSessionStorage(): ArrayAccess
130
    {
131 3
        return $this->sessionStorage;
132
    }
133 3
134
    /**
135
     * @param ArrayAccess $sessionStorage
136
     *
137
     * @return self
138
     */
139 2
    protected function setSessionStorage(ArrayAccess $sessionStorage): self
140
    {
141 2
        $this->sessionStorage = $sessionStorage;
142
143
        return $this;
144
    }
145
146
    /**
147
     * @return string
148
     */
149 3
    protected function getTokenStorageKey(): string
150
    {
151 3
        return $this->tokenStorageKey;
152
    }
153 3
154
    /**
155 3
     * @param string $tokenStorageKey
156
     *
157
     * @return self
158
     */
159
    protected function setTokenStorageKey(string $tokenStorageKey): self
160
    {
161 2
        assert(empty($tokenStorageKey) === false);
162
163 2
        $this->tokenStorageKey = $tokenStorageKey;
164
165
        return $this;
166
    }
167
168
    /**
169
     * @return int|null
170
     */
171 3
    protected function getMaxTokens(): ?int
172
    {
173 3
        return $this->maxTokens;
174
    }
175 3
176
    /**
177 3
     * @param int|null $maxTokens
178
     *
179
     * @return self
180
     */
181
    protected function setMaxTokens(?int $maxTokens): self
182
    {
183 2
        assert($maxTokens === null || $maxTokens > 0);
184
185 2
        $this->maxTokens = $maxTokens > 0 ? $maxTokens : null;
186
187
        return $this;
188
    }
189
190
    /**
191
     * @return int
192
     */
193 3
    protected function getMaxTokensGcThreshold(): int
194
    {
195 3
        return $this->maxTokensGcThreshold;
196
    }
197 3
198
    /**
199 3
     * @param int $maxTokensGcThreshold
200
     *
201
     * @return self
202
     */
203
    protected function setMaxTokensGcThreshold(int $maxTokensGcThreshold): self
204
    {
205
        assert($maxTokensGcThreshold >= 0);
206
207 2
        $this->maxTokensGcThreshold = $maxTokensGcThreshold >= 0 ? $maxTokensGcThreshold : 0;
208
209 2
        return $this;
210
    }
211 2
212
    /**
213
     * @return string
214
     *
215
     * @throws Exception
216
     */
217
    protected function createTokenValue(): string
218
    {
219 2
        $value = bin2hex(random_bytes(static::TOKEN_BYTE_LENGTH));
220
221 2
        return $value;
222
    }
223
224
    /**
225
     * Additional information that would be stored with a token. For example, could be creation timestamp.
226
     *
227 2
     * @return int
228
     */
229 2
    protected function createTokenTimestamp(): int
230 2
    {
231
        return time();
232
    }
233 2
234
    /**
235 2
     * @return array
236
     */
237
    protected function getTokenStorage(): array
238
    {
239
        $sessionStorage = $this->getSessionStorage();
240
        $storageKey     = $this->getTokenStorageKey();
241
242
        $tokenStorage
243
            = $sessionStorage->offsetExists($storageKey) === true ? $sessionStorage->offsetGet($storageKey) : [];
244
245 2
        return $tokenStorage;
246
    }
247 2
248
    /**
249 2
     * Replace whole token storage.
250
     *
251
     * @param array $tokenStorage
252
     *
253
     * @return self
254
     */
255
    protected function setTokenStorage(array $tokenStorage): self
256
    {
257
        $this->getSessionStorage()->offsetSet($this->getTokenStorageKey(), $tokenStorage);
258
259
        return $this;
260
    }
261
}
262