|
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; |
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
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
|
|
|
|