diff options
author | ArthurHoaro <arthur@hoa.ro> | 2020-10-13 12:05:08 +0200 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2020-10-13 12:05:08 +0200 |
commit | b6f678a5a1d15acf284ebcec16c905e976671ce1 (patch) | |
tree | 33c7da831482ed79c44896ef19c73c72ada84f2e /application/security | |
parent | b14687036b9b800681197f51fdc47e62f0c88e2e (diff) | |
parent | 1c1520b6b98ab20201bfe15577782a52320339df (diff) | |
download | Shaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.tar.gz Shaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.tar.zst Shaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.zip |
Merge branch 'v0.12' into latest
Diffstat (limited to 'application/security')
-rw-r--r-- | application/security/CookieManager.php | 33 | ||||
-rw-r--r-- | application/security/LoginManager.php | 95 | ||||
-rw-r--r-- | application/security/SessionManager.php | 114 |
3 files changed, 220 insertions, 22 deletions
diff --git a/application/security/CookieManager.php b/application/security/CookieManager.php new file mode 100644 index 00000000..cde4746e --- /dev/null +++ b/application/security/CookieManager.php | |||
@@ -0,0 +1,33 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Security; | ||
6 | |||
7 | class CookieManager | ||
8 | { | ||
9 | /** @var string Name of the cookie set after logging in **/ | ||
10 | public const STAY_SIGNED_IN = 'shaarli_staySignedIn'; | ||
11 | |||
12 | /** @var mixed $_COOKIE set by reference */ | ||
13 | protected $cookies; | ||
14 | |||
15 | public function __construct(array &$cookies) | ||
16 | { | ||
17 | $this->cookies = $cookies; | ||
18 | } | ||
19 | |||
20 | public function setCookieParameter(string $key, string $value, int $expires, string $path): self | ||
21 | { | ||
22 | $this->cookies[$key] = $value; | ||
23 | |||
24 | setcookie($key, $value, $expires, $path); | ||
25 | |||
26 | return $this; | ||
27 | } | ||
28 | |||
29 | public function getCookieParameter(string $key, string $default = null): ?string | ||
30 | { | ||
31 | return $this->cookies[$key] ?? $default; | ||
32 | } | ||
33 | } | ||
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index 0b0ce0b1..d74c3118 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php | |||
@@ -1,6 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | namespace Shaarli\Security; | 2 | namespace Shaarli\Security; |
3 | 3 | ||
4 | use Exception; | ||
4 | use Shaarli\Config\ConfigManager; | 5 | use Shaarli\Config\ConfigManager; |
5 | 6 | ||
6 | /** | 7 | /** |
@@ -8,9 +9,6 @@ use Shaarli\Config\ConfigManager; | |||
8 | */ | 9 | */ |
9 | class LoginManager | 10 | class LoginManager |
10 | { | 11 | { |
11 | /** @var string Name of the cookie set after logging in **/ | ||
12 | public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn'; | ||
13 | |||
14 | /** @var array A reference to the $_GLOBALS array */ | 12 | /** @var array A reference to the $_GLOBALS array */ |
15 | protected $globals = []; | 13 | protected $globals = []; |
16 | 14 | ||
@@ -31,17 +29,21 @@ class LoginManager | |||
31 | 29 | ||
32 | /** @var string User sign-in token depending on remote IP and credentials */ | 30 | /** @var string User sign-in token depending on remote IP and credentials */ |
33 | protected $staySignedInToken = ''; | 31 | protected $staySignedInToken = ''; |
32 | /** @var CookieManager */ | ||
33 | protected $cookieManager; | ||
34 | 34 | ||
35 | /** | 35 | /** |
36 | * Constructor | 36 | * Constructor |
37 | * | 37 | * |
38 | * @param ConfigManager $configManager Configuration Manager instance | 38 | * @param ConfigManager $configManager Configuration Manager instance |
39 | * @param SessionManager $sessionManager SessionManager instance | 39 | * @param SessionManager $sessionManager SessionManager instance |
40 | * @param CookieManager $cookieManager CookieManager instance | ||
40 | */ | 41 | */ |
41 | public function __construct($configManager, $sessionManager) | 42 | public function __construct($configManager, $sessionManager, $cookieManager) |
42 | { | 43 | { |
43 | $this->configManager = $configManager; | 44 | $this->configManager = $configManager; |
44 | $this->sessionManager = $sessionManager; | 45 | $this->sessionManager = $sessionManager; |
46 | $this->cookieManager = $cookieManager; | ||
45 | $this->banManager = new BanManager( | 47 | $this->banManager = new BanManager( |
46 | $this->configManager->get('security.trusted_proxies', []), | 48 | $this->configManager->get('security.trusted_proxies', []), |
47 | $this->configManager->get('security.ban_after'), | 49 | $this->configManager->get('security.ban_after'), |
@@ -85,10 +87,9 @@ class LoginManager | |||
85 | /** | 87 | /** |
86 | * Check user session state and validity (expiration) | 88 | * Check user session state and validity (expiration) |
87 | * | 89 | * |
88 | * @param array $cookie The $_COOKIE array | ||
89 | * @param string $clientIpId Client IP address identifier | 90 | * @param string $clientIpId Client IP address identifier |
90 | */ | 91 | */ |
91 | public function checkLoginState($cookie, $clientIpId) | 92 | public function checkLoginState($clientIpId) |
92 | { | 93 | { |
93 | if (! $this->configManager->exists('credentials.login')) { | 94 | if (! $this->configManager->exists('credentials.login')) { |
94 | // Shaarli is not configured yet | 95 | // Shaarli is not configured yet |
@@ -96,9 +97,7 @@ class LoginManager | |||
96 | return; | 97 | return; |
97 | } | 98 | } |
98 | 99 | ||
99 | if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) | 100 | if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) { |
100 | && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken | ||
101 | ) { | ||
102 | // The user client has a valid stay-signed-in cookie | 101 | // The user client has a valid stay-signed-in cookie |
103 | // Session information is updated with the current client information | 102 | // Session information is updated with the current client information |
104 | $this->sessionManager->storeLoginInfo($clientIpId); | 103 | $this->sessionManager->storeLoginInfo($clientIpId); |
@@ -139,26 +138,86 @@ class LoginManager | |||
139 | */ | 138 | */ |
140 | public function checkCredentials($remoteIp, $clientIpId, $login, $password) | 139 | public function checkCredentials($remoteIp, $clientIpId, $login, $password) |
141 | { | 140 | { |
142 | $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); | 141 | // Check login matches config |
142 | if ($login !== $this->configManager->get('credentials.login')) { | ||
143 | return false; | ||
144 | } | ||
143 | 145 | ||
144 | if ($login != $this->configManager->get('credentials.login') | 146 | // Check credentials |
145 | || $hash != $this->configManager->get('credentials.hash') | 147 | try { |
146 | ) { | 148 | $useLdapLogin = !empty($this->configManager->get('ldap.host')); |
149 | if ((false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) | ||
150 | || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) | ||
151 | ) { | ||
152 | $this->sessionManager->storeLoginInfo($clientIpId); | ||
153 | logm( | ||
154 | $this->configManager->get('resource.log'), | ||
155 | $remoteIp, | ||
156 | 'Login successful' | ||
157 | ); | ||
158 | return true; | ||
159 | } | ||
160 | } | ||
161 | catch(Exception $exception) { | ||
147 | logm( | 162 | logm( |
148 | $this->configManager->get('resource.log'), | 163 | $this->configManager->get('resource.log'), |
149 | $remoteIp, | 164 | $remoteIp, |
150 | 'Login failed for user ' . $login | 165 | 'Exception while checking credentials: ' . $exception |
151 | ); | 166 | ); |
152 | return false; | ||
153 | } | 167 | } |
154 | 168 | ||
155 | $this->sessionManager->storeLoginInfo($clientIpId); | ||
156 | logm( | 169 | logm( |
157 | $this->configManager->get('resource.log'), | 170 | $this->configManager->get('resource.log'), |
158 | $remoteIp, | 171 | $remoteIp, |
159 | 'Login successful' | 172 | 'Login failed for user ' . $login |
173 | ); | ||
174 | return false; | ||
175 | } | ||
176 | |||
177 | |||
178 | /** | ||
179 | * Check user credentials from local config | ||
180 | * | ||
181 | * @param string $login Username | ||
182 | * @param string $password Password | ||
183 | * | ||
184 | * @return bool true if the provided credentials are valid, false otherwise | ||
185 | */ | ||
186 | public function checkCredentialsFromLocalConfig($login, $password) { | ||
187 | $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); | ||
188 | |||
189 | return $login == $this->configManager->get('credentials.login') | ||
190 | && $hash == $this->configManager->get('credentials.hash'); | ||
191 | } | ||
192 | |||
193 | /** | ||
194 | * Check user credentials are valid through LDAP bind | ||
195 | * | ||
196 | * @param string $remoteIp Remote client IP address | ||
197 | * @param string $clientIpId Client IP address identifier | ||
198 | * @param string $login Username | ||
199 | * @param string $password Password | ||
200 | * | ||
201 | * @return bool true if the provided credentials are valid, false otherwise | ||
202 | */ | ||
203 | public function checkCredentialsFromLdap($login, $password, $connect = null, $bind = null) | ||
204 | { | ||
205 | $connect = $connect ?? function($host) { | ||
206 | $resource = ldap_connect($host); | ||
207 | |||
208 | ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3); | ||
209 | |||
210 | return $resource; | ||
211 | }; | ||
212 | $bind = $bind ?? function($handle, $dn, $password) { | ||
213 | return ldap_bind($handle, $dn, $password); | ||
214 | }; | ||
215 | |||
216 | return $bind( | ||
217 | $connect($this->configManager->get('ldap.host')), | ||
218 | sprintf($this->configManager->get('ldap.dn'), $login), | ||
219 | $password | ||
160 | ); | 220 | ); |
161 | return true; | ||
162 | } | 221 | } |
163 | 222 | ||
164 | /** | 223 | /** |
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index b8b8ab8d..36df8c1c 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php | |||
@@ -8,6 +8,14 @@ use Shaarli\Config\ConfigManager; | |||
8 | */ | 8 | */ |
9 | class SessionManager | 9 | class SessionManager |
10 | { | 10 | { |
11 | public const KEY_LINKS_PER_PAGE = 'LINKS_PER_PAGE'; | ||
12 | public const KEY_VISIBILITY = 'visibility'; | ||
13 | public const KEY_UNTAGGED_ONLY = 'untaggedonly'; | ||
14 | |||
15 | public const KEY_SUCCESS_MESSAGES = 'successes'; | ||
16 | public const KEY_WARNING_MESSAGES = 'warnings'; | ||
17 | public const KEY_ERROR_MESSAGES = 'errors'; | ||
18 | |||
11 | /** @var int Session expiration timeout, in seconds */ | 19 | /** @var int Session expiration timeout, in seconds */ |
12 | public static $SHORT_TIMEOUT = 3600; // 1 hour | 20 | public static $SHORT_TIMEOUT = 3600; // 1 hour |
13 | 21 | ||
@@ -23,16 +31,35 @@ class SessionManager | |||
23 | /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ | 31 | /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ |
24 | protected $staySignedIn = false; | 32 | protected $staySignedIn = false; |
25 | 33 | ||
34 | /** @var string */ | ||
35 | protected $savePath; | ||
36 | |||
26 | /** | 37 | /** |
27 | * Constructor | 38 | * Constructor |
28 | * | 39 | * |
29 | * @param array $session The $_SESSION array (reference) | 40 | * @param array $session The $_SESSION array (reference) |
30 | * @param ConfigManager $conf ConfigManager instance | 41 | * @param ConfigManager $conf ConfigManager instance |
42 | * @param string $savePath Session save path returned by builtin function session_save_path() | ||
31 | */ | 43 | */ |
32 | public function __construct(& $session, $conf) | 44 | public function __construct(&$session, $conf, string $savePath) |
33 | { | 45 | { |
34 | $this->session = &$session; | 46 | $this->session = &$session; |
35 | $this->conf = $conf; | 47 | $this->conf = $conf; |
48 | $this->savePath = $savePath; | ||
49 | } | ||
50 | |||
51 | /** | ||
52 | * Initialize XSRF token and links per page session variables. | ||
53 | */ | ||
54 | public function initialize(): void | ||
55 | { | ||
56 | if (!isset($this->session['tokens'])) { | ||
57 | $this->session['tokens'] = []; | ||
58 | } | ||
59 | |||
60 | if (!isset($this->session['LINKS_PER_PAGE'])) { | ||
61 | $this->session['LINKS_PER_PAGE'] = $this->conf->get('general.links_per_page', 20); | ||
62 | } | ||
36 | } | 63 | } |
37 | 64 | ||
38 | /** | 65 | /** |
@@ -156,7 +183,6 @@ class SessionManager | |||
156 | unset($this->session['expires_on']); | 183 | unset($this->session['expires_on']); |
157 | unset($this->session['username']); | 184 | unset($this->session['username']); |
158 | unset($this->session['visibility']); | 185 | unset($this->session['visibility']); |
159 | unset($this->session['untaggedonly']); | ||
160 | } | 186 | } |
161 | } | 187 | } |
162 | 188 | ||
@@ -196,4 +222,84 @@ class SessionManager | |||
196 | } | 222 | } |
197 | return true; | 223 | return true; |
198 | } | 224 | } |
225 | |||
226 | /** @return array Local reference to the global $_SESSION array */ | ||
227 | public function getSession(): array | ||
228 | { | ||
229 | return $this->session; | ||
230 | } | ||
231 | |||
232 | /** | ||
233 | * @param mixed $default value which will be returned if the $key is undefined | ||
234 | * | ||
235 | * @return mixed Content stored in session | ||
236 | */ | ||
237 | public function getSessionParameter(string $key, $default = null) | ||
238 | { | ||
239 | return $this->session[$key] ?? $default; | ||
240 | } | ||
241 | |||
242 | /** | ||
243 | * Store a variable in user session. | ||
244 | * | ||
245 | * @param string $key Session key | ||
246 | * @param mixed $value Session value to store | ||
247 | * | ||
248 | * @return $this | ||
249 | */ | ||
250 | public function setSessionParameter(string $key, $value): self | ||
251 | { | ||
252 | $this->session[$key] = $value; | ||
253 | |||
254 | return $this; | ||
255 | } | ||
256 | |||
257 | /** | ||
258 | * Store a variable in user session. | ||
259 | * | ||
260 | * @param string $key Session key | ||
261 | * | ||
262 | * @return $this | ||
263 | */ | ||
264 | public function deleteSessionParameter(string $key): self | ||
265 | { | ||
266 | unset($this->session[$key]); | ||
267 | |||
268 | return $this; | ||
269 | } | ||
270 | |||
271 | public function getSavePath(): string | ||
272 | { | ||
273 | return $this->savePath; | ||
274 | } | ||
275 | |||
276 | /* | ||
277 | * Next public functions wrapping native PHP session API. | ||
278 | */ | ||
279 | |||
280 | public function destroy(): bool | ||
281 | { | ||
282 | $this->session = []; | ||
283 | |||
284 | return session_destroy(); | ||
285 | } | ||
286 | |||
287 | public function start(): bool | ||
288 | { | ||
289 | if (session_status() === PHP_SESSION_ACTIVE) { | ||
290 | $this->destroy(); | ||
291 | } | ||
292 | |||
293 | return session_start(); | ||
294 | } | ||
295 | |||
296 | public function cookieParameters(int $lifeTime, string $path, string $domain): bool | ||
297 | { | ||
298 | return session_set_cookie_params($lifeTime, $path, $domain); | ||
299 | } | ||
300 | |||
301 | public function regenerateId(bool $deleteOldSession = false): bool | ||
302 | { | ||
303 | return session_regenerate_id($deleteOldSession); | ||
304 | } | ||
199 | } | 305 | } |