Test Failed
Push — master ( fb5eec...70b4d1 )
by Sebastian
04:40 queued 02:12
created

SynchronizerTokenProvider   A

Complexity

Total Complexity 7

Size/Duplication

Total Lines 114
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 36
dl 0
loc 114
rs 10
c 0
b 0
f 0
wmc 7

3 Methods

Rating   Name   Duplication   Size   Complexity  
A validate() 0 34 4
A __construct() 0 18 1
A getToken() 0 16 2
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of the Linna Csrf Guard.
7
 *
8
 * @author Sebastian Rapetti <[email protected]>
9
 * @copyright (c) 2020, Sebastian Rapetti
10
 * @license http://opensource.org/licenses/MIT MIT License
11
 */
12
13
namespace Linna\CsrfGuard\Provider;
14
15
use Linna\CsrfGuard\Exception\BadExpireException;
16
use Linna\CsrfGuard\Exception\BadStorageSizeException;
17
use Linna\CsrfGuard\Exception\BadTokenLengthException;
18
use Linna\CsrfGuard\Exception\BadExpireTrait;
19
use Linna\CsrfGuard\Exception\BadStorageSizeTrait;
20
use Linna\CsrfGuard\Exception\BadTokenLengthTrait;
21
use Linna\CsrfGuard\Exception\SessionNotStartedException;
22
use Linna\CsrfGuard\Exception\SessionNotStartedTrait;
23
24
/**
25
 * Syncronizer token provider.
26
 *
27
 * <p>A random token with the expire time.</p>
28
 */
29
class SynchronizerTokenProvider implements TokenProviderInterface
30
{
31
    use BadExpireTrait;
32
    use BadStorageSizeTrait;
33
    use BadTokenLengthTrait;
34
    use SessionNotStartedTrait;
35
36
    /** @var string CSRF_TOKEN_STORAGE Token storage key name in session array. */
37
    private const CSRF_TOKEN_STORAGE = 'csrf_syncronizer_token';
38
39
    /** @var int $expire Token validity in seconds, default 600 -> 10 minutes. */
40
    private int $expire = 0;
41
42
    /** @var int $tokenLength Token length in chars. */
43
    private int $tokenLength = 32;
44
45
    /** @var int $storageSize Maximum token nonces stored in session. */
46
    private int $storageSize = 10;
47
48
    /**
49
     * Class constructor.
50
     *
51
     * @param int $expire      Token validity in seconds, default 600 -> 10 minutes.
52
     * @param int $storageSize Maximum token nonces stored in session.
53
     * @param int $tokenLength The desidered token length in bytes, consider that the time is added to the token.
54
     *
55
     * @throws BadExpireException      If <code>$expire</code> is less than 0 and greater than 86400.
56
     * @throws BadStorageSizeException If <code>$storageSize</code> is less than 2 and greater than 64.
57
     * @throws BadTokenLengthException If <code>$tokenLength</code> is less than 16 and greater than 128.
58
     */
59
    public function __construct(int $expire = 600, int $storageSize = 10, int $tokenLength = 32)
60
    {
61
        // from BadExpireTrait, BadStorageSizeTrait, BadTokenLengthException and SessionNotStartedTrait
62
        /** @throws BadExpireException */
63
        $this->checkBadExpire($expire);
64
        /** @throws BadStorageSizeException */
65
        $this->checkBadStorageSize($storageSize);
66
        /** @throws BadTokenLengthException */
67
        $this->checkBadTokenLength($tokenLength);
68
        /** @throws SessionNotStartedException */
69
        $this->checkSessionNotStarted();
70
71
        $this->expire = $expire;
72
        $this->tokenLength = $tokenLength;
73
        $this->storageSize = $storageSize;
74
75
        //if any token stored, initialize the session storage
76
        $_SESSION[self::CSRF_TOKEN_STORAGE] ??= [];
77
    }
78
79
    /**
80
     * Return new Synchronizer based Token.
81
     *
82
     * @return string The token in hex format.
83
     */
84
    public function getToken(): string
85
    {
86
        //generate new token
87
        $token = \bin2hex(\random_bytes(\max(1, $this->tokenLength)));
88
        $time = \base_convert((string) \time(), 10, 16);
89
90
        //store new token
91
        $_SESSION[self::CSRF_TOKEN_STORAGE][] = $token.$time;
92
93
        //check if the storage is growt beyond the maximun size
94
        if (\count($_SESSION[self::CSRF_TOKEN_STORAGE]) > $this->storageSize) {
95
            //remove the oldest stores nonce
96
            \array_shift($_SESSION[self::CSRF_TOKEN_STORAGE]);
97
        }
98
99
        return $token;
100
    }
101
102
    /**
103
     * Validate Synchronizer based Token.
104
     *
105
     * @param string $token Token must be validated, hex format.
106
     *
107
     * @return bool True if the token is valid, false otherwise.
108
     */
109
    public function validate(string $token): bool
110
    {
111
        //reference to token storage in session
112
        $tokens = &$_SESSION[self::CSRF_TOKEN_STORAGE];
113
        //get current size of token storage in session
114
        $size = \count($tokens);
115
        //bytes to b16 chars
116
        $chars = $this->tokenLength * 2;
117
        //current time
118
        $time = \time();
119
120
        for ($i = $size -1; $i > -1; $i--) {
121
            //get token from storage
122
            //if token in storage doesn't match user token continue
123
            if (($tmpToken = \substr($tokens[$i], 0, $chars)) !== $token) {
0 ignored issues
show
Unused Code introduced by
The assignment to $tmpToken is dead and can be removed.
Loading history...
124
                continue;
125
            }
126
127
            //get time from storage
128
            //subtract the length of the token to the token+time
129
            $tmpTime = \substr($tokens[$i], $chars);
130
            //timestamp from token time
131
            $timestamp = (int) \base_convert($tmpTime, 16, 10);
132
133
            //token expiration check
134
            if (($timestamp + $this->expire) > $time) {
135
                return true;
136
            }
137
138
            //remove token expired from session
139
            unset($tokens[$i]);
140
        }
141
142
        return false;
143
    }
144
}
145