MemcacheLoginLimitPlugin   A
last analyzed

Complexity

Total Complexity 18

Size/Duplication

Total Lines 218
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 18
eloc 41
c 2
b 0
f 0
dl 0
loc 218
ccs 39
cts 39
cp 1
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
A setAttemptKey() 0 14 5
A getLoginAttempts() 0 3 1
A __construct() 0 6 1
A setRemoteAddr() 0 3 1
A getMaxLoginAttempts() 0 3 1
A getKeys() 0 6 3
A login() 0 11 3
A setLoginAttempts() 0 10 2
A decrementAttempts() 0 3 1
1
<?php
2
/** @noinspection PhpComposerExtensionStubsInspection */
3
4
namespace Vectorface\Auth\Plugin\Limit;
5
6
use Memcache;
7
use Vectorface\Auth\Auth;
8
use Vectorface\Auth\Plugin\BaseAuthPlugin;
9
use Vectorface\Auth\Plugin\SharedLoggerTrait;
10
11
/**
12
 * Limit the number of logins allowed per login and IP-address in memcache(d)
13
 *
14
 * Notes:
15
 *  - The attempts per-login are visible via the public interfaces, but per-IP are not.
16
 *  - Per-login attempt limiting is meant to catch brute-force attempts on a single user.
17
 *  - Per-IP limiting is meant to catch and prevent floods.
18
 *
19
 * Todo:
20
 *  - There should be a fallback mechanism for this if the memcache server is not available.
21
 *  - Configurable logging mechanism should be introduced to be able to do more useful alerts.
22
 */
23
class MemcacheLoginLimitPlugin extends BaseAuthPlugin implements LoginLimitPluginInterface
24
{
25
    /**
26
     * Allows use of a logger attached to the auth class, if configured.
27
     */
28
    use SharedLoggerTrait;
0 ignored issues
show
Bug introduced by
The trait Vectorface\Auth\Plugin\SharedLoggerTrait requires the property $logger which is not provided by Vectorface\Auth\Plugin\L...emcacheLoginLimitPlugin.
Loading history...
29
30
    /**
31
     * Default maximum number of login attempts.
32
     */
33
    const MAX_LOGIN = 3;
34
35
    /**
36
     * Default maximum number of attempts per IP address.
37
     */
38
    const MAX_ADDR = 10;
39
40
    /**
41
     * Default timeout in seconds.
42
     */
43
    const TIMEOUT = 300;
44
45
    /**
46
     * The maximum allowed number of login attempts within the timeout.
47
     *
48
     * @var int
49
     */
50
    protected $maxAttemptsLogin;
51
52
    /**
53
     * The maximum allowed number of attempts from a given address within the timeout.
54
     *
55
     * @var int
56
     */
57
    protected $maxAttemptsAddr;
58
59
    /**
60
     * The amount of time that must pass after the maximum number of failed login attempts.
61
     *
62
     * @var int
63
     */
64
    protected $timeout;
65
66
    /**
67
     * The memcache instance to be used to store the timeout.
68
     *
69
     * @var Memcache
70
     */
71
    protected $memcache;
72
73
    /**
74
     * The number of attempts so far for this login.
75
     *
76
     * @var int
77
     */
78
    protected $attemptsLogin;
79
80
    /**
81
     * The number of attempts so far from this IP address.
82
     *
83
     * @var int
84
     */
85
    protected $attemptsAddr;
86
87
    /**
88
     * The unique IP address or hostname associated with the login attempts.
89
     *
90
     * @var string
91
     */
92
    protected $addr;
93
94
    /**
95
     * Stores the last username to be used for a login attempt.
96
     *
97
     * @var string
98
     */
99
    protected $username;
100
101
    /**
102
     * Create a new memcache login limiter.
103
     *
104
     * @param Memcache $mc The pre-configured Memcache handle.
105
     * @param int $login The maximum number of login attempts to be allowed.
106
     * @param int $addr The maximum number of login attempts per IP address.
107
     * @param int $sec The amount of time (in seconds) to block login attempts after max attempts has been surpassed.
108
     */
109 2
    public function __construct(Memcache $mc, $login = self::MAX_LOGIN, $addr = self::MAX_ADDR, $sec = self::TIMEOUT)
110
    {
111 2
        $this->memcache = $mc;
112 2
        $this->maxAttemptsLogin = (int)$login;
113 2
        $this->maxAttemptsAddr = (int)$addr;
114 2
        $this->timeout = (int)$sec;
115 2
    }
116
117
    /**
118
     * Set the remote address to be used. Falls back to PHP's REMOTE_ADDR.
119
     *
120
     * Note: This can be any string; a hostname, an IPv4 or IPv6 address or network, or even a hash.
121
     *
122
     * @param string $addr A unique string that maps to the address making login requests.
123
     */
124 2
    public function setRemoteAddr($addr)
125
    {
126 2
        $this->addr = $addr;
127 2
    }
128
129
    /**
130
     * Decrement the number of attempts.
131
     *
132
     * This is intended for application logic when a login attempt can be forgiven.
133
     */
134 1
    public function decrementAttempts()
135
    {
136 1
        $this->setLoginAttempts($this->username, false);
137 1
    }
138
139
    /**
140
     * Get the memcache cache key.
141
     *
142
     * @param string $username The username attempting to log in.
143
     * @return string[] A pair of cache keys for login and address tracking.
144
     */
145 2
    protected function getKeys($username)
146
    {
147 2
        $addr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'localhost';
148
        return [
149 2
            sprintf('LoginLimit::login(%s)', $username),
150 2
            sprintf('LoginLimit::addr(%s)', $this->addr ? $this->addr : $addr)
151
        ];
152
    }
153
154
    /**
155
     * Get the number of login attempts performed so far.
156
     *
157
     * Note: The number of attempts does not necessarily imply success or
158
     * failure, though if a logged-in user has 3 login attempts, that usually
159
     * means 2 failed 1 successful. If the user has not authenticated, 3
160
     * attempts means 3 failed attempts.
161
     *
162
     * @return int The number of login attempts for this user.
163
     */
164 2
    public function getLoginAttempts()
165
    {
166 2
        return $this->attemptsLogin ?? 0;
167
    }
168
169
    /**
170
     * Get the maximum number of allowed login attempts.
171
     *
172
     * Note: max attempts for a given IP isn't published.
173
     *
174
     * @return int
175
     */
176 1
    public function getMaxLoginAttempts()
177
    {
178 1
        return $this->maxAttemptsLogin;
179
    }
180
181
    /**
182
     * Adjust the number of attempts in a given memcache key.
183
     *
184
     * @param string $key The memcache key to alter.
185
     * @param bool $inc True to increment, false to decrement.
186
     * @return false|int
187
     */
188 2
    protected function setAttemptKey($key, $inc)
189
    {
190
        /* Increment or decrement, as appropriate. */
191 2
        $result = $inc ? $this->memcache->increment($key) : $this->memcache->decrement($key);
192
193
        /* If inc/dec fails, set the value directly. */
194 2
        if ($result === false) {
0 ignored issues
show
introduced by
The condition $result === false is always false.
Loading history...
195 2
            $result = $inc ? 1 : 0; /* No value, so either reset to 0, or set to 1. */
196 2
            if (!$this->memcache->set($key, $result, null, $this->timeout)) {
197 1
                $this->warning('Setting login attempt count in memcache failed. Login throttling may be broken.');
198
            }
199
        }
200
201 2
        return $result;
202
    }
203
204
    /**
205
     * Used internally to set the number of login attempts in memcache.
206
     *
207
     * @param string $username The username attempting to log in.
208
     * @param bool $inc True to increment the number of attempts, false to decrement.
209
     * @return int[] The number of attempts for the given username and IP address.
210
     */
211 2
    protected function setLoginAttempts($username, $inc)
212
    {
213 2
        list($keyLogin, $keyAddr) = $this->getKeys($username);
214
215 2
        if (!empty($username)) {
216 2
            $this->attemptsLogin = (int)$this->setAttemptKey($keyLogin, $inc);
217
        }
218 2
        $this->attemptsAddr = (int)$this->setAttemptKey($keyAddr, $inc);
219
220 2
        return [$this->attemptsLogin, $this->attemptsAddr];
221
    }
222
223
    /**
224
     * Auth plugin hook to be fired on login.
225
     *
226
     * @param string $username
227
     * @param string $password
228
     * @return int
229
     */
230 2
    public function login($username, $password)
231
    {
232 2
        $this->username = $username; /* Store the last-used username */
233
234 2
        list($login, $addr) = $this->setLoginAttempts($username, true);
235
236 2
        if ($login > $this->maxAttemptsLogin || $addr > $this->maxAttemptsAddr) {
237 1
            return Auth::RESULT_FAILURE;
238
        }
239
240 2
        return Auth::RESULT_NOOP;
241
    }
242
}
243