diff options
-rw-r--r-- | application/security/LoginManager.php | 37 | ||||
-rw-r--r-- | application/security/SessionManager.php | 3 | ||||
-rw-r--r-- | index.php | 12 | ||||
-rw-r--r-- | tests/security/LoginManagerTest.php | 31 |
4 files changed, 69 insertions, 14 deletions
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index 27247f3f..41fa9a20 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php | |||
@@ -8,6 +8,9 @@ use Shaarli\Config\ConfigManager; | |||
8 | */ | 8 | */ |
9 | class LoginManager | 9 | class LoginManager |
10 | { | 10 | { |
11 | /** @var string Name of the cookie set after logging in **/ | ||
12 | public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn'; | ||
13 | |||
11 | /** @var array A reference to the $_GLOBALS array */ | 14 | /** @var array A reference to the $_GLOBALS array */ |
12 | protected $globals = []; | 15 | protected $globals = []; |
13 | 16 | ||
@@ -26,6 +29,9 @@ class LoginManager | |||
26 | /** @var bool Whether the Shaarli instance is open to public edition **/ | 29 | /** @var bool Whether the Shaarli instance is open to public edition **/ |
27 | protected $openShaarli = false; | 30 | protected $openShaarli = false; |
28 | 31 | ||
32 | /** @var string User sign-in token depending on remote IP and credentials */ | ||
33 | protected $staySignedInToken = ''; | ||
34 | |||
29 | /** | 35 | /** |
30 | * Constructor | 36 | * Constructor |
31 | * | 37 | * |
@@ -46,15 +52,38 @@ class LoginManager | |||
46 | } | 52 | } |
47 | 53 | ||
48 | /** | 54 | /** |
55 | * Generate a token depending on deployment salt, user password and client IP | ||
56 | * | ||
57 | * @param string $clientIpAddress The remote client IP address | ||
58 | */ | ||
59 | public function generateStaySignedInToken($clientIpAddress) | ||
60 | { | ||
61 | $this->staySignedInToken = sha1( | ||
62 | $this->configManager->get('credentials.hash') | ||
63 | . $clientIpAddress | ||
64 | . $this->configManager->get('credentials.salt') | ||
65 | ); | ||
66 | } | ||
67 | |||
68 | /** | ||
69 | * Return the user's client stay-signed-in token | ||
70 | * | ||
71 | * @return string User's client stay-signed-in token | ||
72 | */ | ||
73 | public function getStaySignedInToken() | ||
74 | { | ||
75 | return $this->staySignedInToken; | ||
76 | } | ||
77 | |||
78 | /** | ||
49 | * Check user session state and validity (expiration) | 79 | * Check user session state and validity (expiration) |
50 | * | 80 | * |
51 | * @param array $cookie The $_COOKIE array | 81 | * @param array $cookie The $_COOKIE array |
52 | * @param string $clientIpId Client IP address identifier | 82 | * @param string $clientIpId Client IP address identifier |
53 | * @param string $token Session token | ||
54 | * | 83 | * |
55 | * @return bool true if the user session is valid, false otherwise | 84 | * @return bool true if the user session is valid, false otherwise |
56 | */ | 85 | */ |
57 | public function checkLoginState($cookie, $clientIpId, $token) | 86 | public function checkLoginState($cookie, $clientIpId) |
58 | { | 87 | { |
59 | if (! $this->configManager->exists('credentials.login')) { | 88 | if (! $this->configManager->exists('credentials.login')) { |
60 | // Shaarli is not configured yet | 89 | // Shaarli is not configured yet |
@@ -62,8 +91,8 @@ class LoginManager | |||
62 | return; | 91 | return; |
63 | } | 92 | } |
64 | 93 | ||
65 | if (isset($cookie[SessionManager::$LOGGED_IN_COOKIE]) | 94 | if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) |
66 | && $cookie[SessionManager::$LOGGED_IN_COOKIE] === $token | 95 | && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken |
67 | ) { | 96 | ) { |
68 | $this->sessionManager->storeLoginInfo($clientIpId); | 97 | $this->sessionManager->storeLoginInfo($clientIpId); |
69 | $this->isLoggedIn = true; | 98 | $this->isLoggedIn = true; |
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 0dcd7f90..58973130 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php | |||
@@ -14,9 +14,6 @@ class SessionManager | |||
14 | /** @var int Session expiration timeout, in seconds */ | 14 | /** @var int Session expiration timeout, in seconds */ |
15 | public static $LONG_TIMEOUT = 31536000; // 1 year | 15 | public static $LONG_TIMEOUT = 31536000; // 1 year |
16 | 16 | ||
17 | /** @var string Name of the cookie set after logging in **/ | ||
18 | public static $LOGGED_IN_COOKIE = 'shaarli_staySignedIn'; | ||
19 | |||
20 | /** @var array Local reference to the global $_SESSION array */ | 17 | /** @var array Local reference to the global $_SESSION array */ |
21 | protected $session = []; | 18 | protected $session = []; |
22 | 19 | ||
@@ -123,6 +123,7 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) | |||
123 | $conf = new ConfigManager(); | 123 | $conf = new ConfigManager(); |
124 | $sessionManager = new SessionManager($_SESSION, $conf); | 124 | $sessionManager = new SessionManager($_SESSION, $conf); |
125 | $loginManager = new LoginManager($GLOBALS, $conf, $sessionManager); | 125 | $loginManager = new LoginManager($GLOBALS, $conf, $sessionManager); |
126 | $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); | ||
126 | $clientIpId = client_ip_id($_SERVER); | 127 | $clientIpId = client_ip_id($_SERVER); |
127 | 128 | ||
128 | // LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead. | 129 | // LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead. |
@@ -176,10 +177,7 @@ if (! is_file($conf->getConfigFileExt())) { | |||
176 | install($conf, $sessionManager); | 177 | install($conf, $sessionManager); |
177 | } | 178 | } |
178 | 179 | ||
179 | // a token depending of deployment salt, user password, and the current ip | 180 | $loginManager->checkLoginState($_COOKIE, $clientIpId); |
180 | define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt'))); | ||
181 | |||
182 | $loginManager->checkLoginState($_COOKIE, $clientIpId, STAY_SIGNED_IN_TOKEN); | ||
183 | 181 | ||
184 | /** | 182 | /** |
185 | * Adapter function to ensure compatibility with third-party templates | 183 | * Adapter function to ensure compatibility with third-party templates |
@@ -219,8 +217,8 @@ if (isset($_POST['login'])) { | |||
219 | $expirationTime = $sessionManager->extendSession(); | 217 | $expirationTime = $sessionManager->extendSession(); |
220 | 218 | ||
221 | setcookie( | 219 | setcookie( |
222 | $sessionManager::$LOGGED_IN_COOKIE, | 220 | $loginManager::$STAY_SIGNED_IN_COOKIE, |
223 | STAY_SIGNED_IN_TOKEN, | 221 | $loginManager->getStaySignedInToken(), |
224 | $expirationTime, | 222 | $expirationTime, |
225 | WEB_PATH | 223 | WEB_PATH |
226 | ); | 224 | ); |
@@ -595,7 +593,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
595 | { | 593 | { |
596 | invalidateCaches($conf->get('resource.page_cache')); | 594 | invalidateCaches($conf->get('resource.page_cache')); |
597 | $sessionManager->logout(); | 595 | $sessionManager->logout(); |
598 | setcookie(SessionManager::$LOGGED_IN_COOKIE, 'false', 0, WEB_PATH); | 596 | setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH); |
599 | header('Location: ?'); | 597 | header('Location: ?'); |
600 | exit; | 598 | exit; |
601 | } | 599 | } |
diff --git a/tests/security/LoginManagerTest.php b/tests/security/LoginManagerTest.php index b957abe3..633f1bb9 100644 --- a/tests/security/LoginManagerTest.php +++ b/tests/security/LoginManagerTest.php | |||
@@ -18,6 +18,18 @@ class LoginManagerTest extends TestCase | |||
18 | protected $server = []; | 18 | protected $server = []; |
19 | protected $trustedProxy = '10.1.1.100'; | 19 | protected $trustedProxy = '10.1.1.100'; |
20 | 20 | ||
21 | /** @var string User login */ | ||
22 | protected $login = 'johndoe'; | ||
23 | |||
24 | /** @var string User password */ | ||
25 | protected $password = 'IC4nHazL0g1n?'; | ||
26 | |||
27 | /** @var string Hash of the salted user password */ | ||
28 | protected $passwordHash = ''; | ||
29 | |||
30 | /** @var string Salt used by hash functions */ | ||
31 | protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2'; | ||
32 | |||
21 | /** | 33 | /** |
22 | * Prepare or reset test resources | 34 | * Prepare or reset test resources |
23 | */ | 35 | */ |
@@ -27,7 +39,12 @@ class LoginManagerTest extends TestCase | |||
27 | unlink($this->banFile); | 39 | unlink($this->banFile); |
28 | } | 40 | } |
29 | 41 | ||
42 | $this->passwordHash = sha1($this->password . $this->login . $this->salt); | ||
43 | |||
30 | $this->configManager = new \FakeConfigManager([ | 44 | $this->configManager = new \FakeConfigManager([ |
45 | 'credentials.login' => $this->login, | ||
46 | 'credentials.hash' => $this->passwordHash, | ||
47 | 'credentials.salt' => $this->salt, | ||
31 | 'resource.ban_file' => $this->banFile, | 48 | 'resource.ban_file' => $this->banFile, |
32 | 'resource.log' => $this->logFile, | 49 | 'resource.log' => $this->logFile, |
33 | 'security.ban_after' => 4, | 50 | 'security.ban_after' => 4, |
@@ -196,4 +213,18 @@ class LoginManagerTest extends TestCase | |||
196 | $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() - 3600; | 213 | $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() - 3600; |
197 | $this->assertTrue($this->loginManager->canLogin($this->server)); | 214 | $this->assertTrue($this->loginManager->canLogin($this->server)); |
198 | } | 215 | } |
216 | |||
217 | /** | ||
218 | * Generate a token depending on the user credentials and client IP | ||
219 | */ | ||
220 | public function testGenerateStaySignedInToken() | ||
221 | { | ||
222 | $ipAddress = '10.1.47.179'; | ||
223 | $this->loginManager->generateStaySignedInToken($ipAddress); | ||
224 | |||
225 | $this->assertEquals( | ||
226 | sha1($this->passwordHash . $ipAddress . $this->salt), | ||
227 | $this->loginManager->getStaySignedInToken() | ||
228 | ); | ||
229 | } | ||
199 | } | 230 | } |