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
|
|
|
|