LoginManager.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <?php
  2. namespace Shaarli\Security;
  3. use Shaarli\Config\ConfigManager;
  4. /**
  5. * User login management
  6. */
  7. class LoginManager
  8. {
  9. /** @var string Name of the cookie set after logging in **/
  10. public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn';
  11. /** @var array A reference to the $_GLOBALS array */
  12. protected $globals = [];
  13. /** @var ConfigManager Configuration Manager instance **/
  14. protected $configManager = null;
  15. /** @var SessionManager Session Manager instance **/
  16. protected $sessionManager = null;
  17. /** @var string Path to the file containing IP bans */
  18. protected $banFile = '';
  19. /** @var bool Whether the user is logged in **/
  20. protected $isLoggedIn = false;
  21. /** @var bool Whether the Shaarli instance is open to public edition **/
  22. protected $openShaarli = false;
  23. /** @var string User sign-in token depending on remote IP and credentials */
  24. protected $staySignedInToken = '';
  25. /**
  26. * Constructor
  27. *
  28. * @param array $globals The $GLOBALS array (reference)
  29. * @param ConfigManager $configManager Configuration Manager instance
  30. * @param SessionManager $sessionManager SessionManager instance
  31. */
  32. public function __construct(& $globals, $configManager, $sessionManager)
  33. {
  34. $this->globals = &$globals;
  35. $this->configManager = $configManager;
  36. $this->sessionManager = $sessionManager;
  37. $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php');
  38. $this->readBanFile();
  39. if ($this->configManager->get('security.open_shaarli') === true) {
  40. $this->openShaarli = true;
  41. }
  42. }
  43. /**
  44. * Generate a token depending on deployment salt, user password and client IP
  45. *
  46. * @param string $clientIpAddress The remote client IP address
  47. */
  48. public function generateStaySignedInToken($clientIpAddress)
  49. {
  50. $this->staySignedInToken = sha1(
  51. $this->configManager->get('credentials.hash')
  52. . $clientIpAddress
  53. . $this->configManager->get('credentials.salt')
  54. );
  55. }
  56. /**
  57. * Return the user's client stay-signed-in token
  58. *
  59. * @return string User's client stay-signed-in token
  60. */
  61. public function getStaySignedInToken()
  62. {
  63. return $this->staySignedInToken;
  64. }
  65. /**
  66. * Check user session state and validity (expiration)
  67. *
  68. * @param array $cookie The $_COOKIE array
  69. * @param string $clientIpId Client IP address identifier
  70. */
  71. public function checkLoginState($cookie, $clientIpId)
  72. {
  73. if (! $this->configManager->exists('credentials.login')) {
  74. // Shaarli is not configured yet
  75. $this->isLoggedIn = false;
  76. return;
  77. }
  78. if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE])
  79. && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
  80. ) {
  81. // The user client has a valid stay-signed-in cookie
  82. // Session information is updated with the current client information
  83. $this->sessionManager->storeLoginInfo($clientIpId);
  84. } elseif ($this->sessionManager->hasSessionExpired()
  85. || $this->sessionManager->hasClientIpChanged($clientIpId)
  86. ) {
  87. $this->sessionManager->logout();
  88. $this->isLoggedIn = false;
  89. return;
  90. }
  91. $this->isLoggedIn = true;
  92. $this->sessionManager->extendSession();
  93. }
  94. /**
  95. * Return whether the user is currently logged in
  96. *
  97. * @return true when the user is logged in, false otherwise
  98. */
  99. public function isLoggedIn()
  100. {
  101. if ($this->openShaarli) {
  102. return true;
  103. }
  104. return $this->isLoggedIn;
  105. }
  106. /**
  107. * Check user credentials are valid
  108. *
  109. * @param string $remoteIp Remote client IP address
  110. * @param string $clientIpId Client IP address identifier
  111. * @param string $login Username
  112. * @param string $password Password
  113. *
  114. * @return bool true if the provided credentials are valid, false otherwise
  115. */
  116. public function checkCredentials($remoteIp, $clientIpId, $login, $password)
  117. {
  118. $hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
  119. if ($login != $this->configManager->get('credentials.login')
  120. || $hash != $this->configManager->get('credentials.hash')
  121. ) {
  122. logm(
  123. $this->configManager->get('resource.log'),
  124. $remoteIp,
  125. 'Login failed for user ' . $login
  126. );
  127. return false;
  128. }
  129. $this->sessionManager->storeLoginInfo($clientIpId);
  130. logm(
  131. $this->configManager->get('resource.log'),
  132. $remoteIp,
  133. 'Login successful'
  134. );
  135. return true;
  136. }
  137. /**
  138. * Read a file containing banned IPs
  139. */
  140. protected function readBanFile()
  141. {
  142. if (! file_exists($this->banFile)) {
  143. return;
  144. }
  145. include $this->banFile;
  146. }
  147. /**
  148. * Write the banned IPs to a file
  149. */
  150. protected function writeBanFile()
  151. {
  152. if (! array_key_exists('IPBANS', $this->globals)) {
  153. return;
  154. }
  155. file_put_contents(
  156. $this->banFile,
  157. "<?php\n\$GLOBALS['IPBANS']=" . var_export($this->globals['IPBANS'], true) . ";\n?>"
  158. );
  159. }
  160. /**
  161. * Handle a failed login and ban the IP after too many failed attempts
  162. *
  163. * @param array $server The $_SERVER array
  164. */
  165. public function handleFailedLogin($server)
  166. {
  167. $ip = $server['REMOTE_ADDR'];
  168. $trusted = $this->configManager->get('security.trusted_proxies', []);
  169. if (in_array($ip, $trusted)) {
  170. $ip = getIpAddressFromProxy($server, $trusted);
  171. if (! $ip) {
  172. // the IP is behind a trusted forward proxy, but is not forwarded
  173. // in the HTTP headers, so we do nothing
  174. return;
  175. }
  176. }
  177. // increment the fail count for this IP
  178. if (isset($this->globals['IPBANS']['FAILURES'][$ip])) {
  179. $this->globals['IPBANS']['FAILURES'][$ip]++;
  180. } else {
  181. $this->globals['IPBANS']['FAILURES'][$ip] = 1;
  182. }
  183. if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) {
  184. $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800);
  185. logm(
  186. $this->configManager->get('resource.log'),
  187. $server['REMOTE_ADDR'],
  188. 'IP address banned from login'
  189. );
  190. }
  191. $this->writeBanFile();
  192. }
  193. /**
  194. * Handle a successful login
  195. *
  196. * @param array $server The $_SERVER array
  197. */
  198. public function handleSuccessfulLogin($server)
  199. {
  200. $ip = $server['REMOTE_ADDR'];
  201. // FIXME unban when behind a trusted proxy?
  202. unset($this->globals['IPBANS']['FAILURES'][$ip]);
  203. unset($this->globals['IPBANS']['BANS'][$ip]);
  204. $this->writeBanFile();
  205. }
  206. /**
  207. * Check if the user can login from this IP
  208. *
  209. * @param array $server The $_SERVER array
  210. *
  211. * @return bool true if the user is allowed to login
  212. */
  213. public function canLogin($server)
  214. {
  215. $ip = $server['REMOTE_ADDR'];
  216. if (! isset($this->globals['IPBANS']['BANS'][$ip])) {
  217. // the user is not banned
  218. return true;
  219. }
  220. if ($this->globals['IPBANS']['BANS'][$ip] > time()) {
  221. // the user is still banned
  222. return false;
  223. }
  224. // the ban has expired, the user can attempt to log in again
  225. logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.');
  226. unset($this->globals['IPBANS']['FAILURES'][$ip]);
  227. unset($this->globals['IPBANS']['BANS'][$ip]);
  228. $this->writeBanFile();
  229. return true;
  230. }
  231. }