Auth.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <?php
  2. /**
  3. * This class handles all authentication process.
  4. */
  5. class FreshRSS_Auth {
  6. /**
  7. * Determines if user is connected.
  8. */
  9. private static $login_ok = false;
  10. /**
  11. * This method initializes authentication system.
  12. */
  13. public static function init() {
  14. self::$login_ok = Minz_Session::param('loginOk', false);
  15. $current_user = Minz_Session::param('currentUser', '');
  16. if ($current_user === '') {
  17. $current_user = Minz_Configuration::defaultUser();
  18. Minz_Session::_param('currentUser', $current_user);
  19. }
  20. $access_ok = self::accessControl();
  21. if ($access_ok) {
  22. self::giveAccess();
  23. } else {
  24. // Be sure all accesses are removed!
  25. self::removeAccess();
  26. }
  27. }
  28. /**
  29. * This method checks if user is allowed to connect.
  30. *
  31. * Required session parameters are also set in this method (such as
  32. * currentUser).
  33. *
  34. * @return boolean true if user can be connected, false else.
  35. */
  36. public static function accessControl() {
  37. if (self::$login_ok) {
  38. return true;
  39. }
  40. switch (Minz_Configuration::authType()) {
  41. case 'form':
  42. $credentials = FreshRSS_FormAuth::getCredentialsFromCookie();
  43. $current_user = '';
  44. if (isset($credentials[1])) {
  45. $current_user = trim($credentials[0]);
  46. Minz_Session::_param('currentUser', $current_user);
  47. Minz_Session::_param('passwordHash', trim($credentials[1]));
  48. }
  49. return $current_user != '';
  50. case 'http_auth':
  51. $current_user = httpAuthUser();
  52. $login_ok = $current_user != '';
  53. if ($login_ok) {
  54. Minz_Session::_param('currentUser', $current_user);
  55. }
  56. return $login_ok;
  57. case 'persona':
  58. $email = filter_var(Minz_Session::param('mail'), FILTER_VALIDATE_EMAIL);
  59. $persona_file = DATA_PATH . '/persona/' . $email . '.txt';
  60. if (($current_user = @file_get_contents($persona_file)) !== false) {
  61. $current_user = trim($current_user);
  62. Minz_Session::_param('currentUser', $current_user);
  63. Minz_Session::_param('mail', $email);
  64. return true;
  65. }
  66. return false;
  67. case 'none':
  68. return true;
  69. default:
  70. // TODO load extension
  71. return false;
  72. }
  73. }
  74. /**
  75. * Gives access to the current user.
  76. */
  77. public static function giveAccess() {
  78. $current_user = Minz_Session::param('currentUser');
  79. try {
  80. $conf = new FreshRSS_Configuration($current_user);
  81. } catch(Minz_Exception $e) {
  82. die($e->getMessage());
  83. }
  84. switch (Minz_Configuration::authType()) {
  85. case 'form':
  86. self::$login_ok = Minz_Session::param('passwordHash') === $conf->passwordHash;
  87. break;
  88. case 'http_auth':
  89. self::$login_ok = strcasecmp($current_user, httpAuthUser()) === 0;
  90. break;
  91. case 'persona':
  92. self::$login_ok = strcasecmp(Minz_Session::param('mail'), $conf->mail_login) === 0;
  93. break;
  94. case 'none':
  95. self::$login_ok = true;
  96. break;
  97. default:
  98. // TODO: extensions
  99. self::$login_ok = false;
  100. }
  101. Minz_Session::_param('loginOk', self::$login_ok);
  102. }
  103. /**
  104. * Returns if current user has access to the given scope.
  105. *
  106. * @param string $scope general (default) or admin
  107. * @return boolean true if user has corresponding access, false else.
  108. */
  109. public static function hasAccess($scope = 'general') {
  110. $ok = self::$login_ok;
  111. switch ($scope) {
  112. case 'general':
  113. break;
  114. case 'admin':
  115. $ok &= Minz_Session::param('currentUser') === Minz_Configuration::defaultUser();
  116. break;
  117. default:
  118. $ok = false;
  119. }
  120. return $ok;
  121. }
  122. /**
  123. * Removes all accesses for the current user.
  124. */
  125. public static function removeAccess() {
  126. Minz_Session::_param('loginOk');
  127. self::$login_ok = false;
  128. Minz_Session::_param('currentUser', Minz_Configuration::defaultUser());
  129. switch (Minz_Configuration::authType()) {
  130. case 'form':
  131. Minz_Session::_param('passwordHash');
  132. FreshRSS_FormAuth::deleteCookie();
  133. break;
  134. case 'persona':
  135. Minz_Session::_param('mail');
  136. break;
  137. case 'http_auth':
  138. case 'none':
  139. // Nothing to do...
  140. break;
  141. default:
  142. // TODO: extensions
  143. }
  144. }
  145. }
  146. class FreshRSS_FormAuth {
  147. public static function checkCredentials($username, $hash, $nonce, $challenge) {
  148. if (!ctype_alnum($username) ||
  149. !ctype_graph($challenge) ||
  150. !ctype_alnum($nonce)) {
  151. Minz_Log::debug('Invalid credential parameters:' .
  152. ' user=' . $username .
  153. ' challenge=' . $challenge .
  154. ' nonce=' . $nonce);
  155. return false;
  156. }
  157. if (!function_exists('password_verify')) {
  158. include_once(LIB_PATH . '/password_compat.php');
  159. }
  160. return password_verify($nonce . $hash, $challenge);
  161. }
  162. public static function getCredentialsFromCookie() {
  163. $token = Minz_Session::getLongTermCookie('FreshRSS_login');
  164. if (!ctype_alnum($token)) {
  165. return array();
  166. }
  167. $token_file = DATA_PATH . '/tokens/' . $token . '.txt';
  168. $mtime = @filemtime($token_file);
  169. if ($mtime + 2629744 < time()) {
  170. // Token has expired (> 1 month) or does not exist.
  171. // TODO: 1 month -> use a configuration instead
  172. @unlink($token_file);
  173. return array();
  174. }
  175. $credentials = @file_get_contents($token_file);
  176. return $credentials === false ? array() : explode("\t", $credentials, 2);
  177. }
  178. public static function makeCookie($username, $password_hash) {
  179. do {
  180. $token = sha1(Minz_Configuration::salt() . $username . uniqid(mt_rand(), true));
  181. $token_file = DATA_PATH . '/tokens/' . $token . '.txt';
  182. } while (file_exists($token_file));
  183. if (@file_put_contents($token_file, $username . "\t" . $password_hash) === false) {
  184. return false;
  185. }
  186. $expire = time() + 2629744; //1 month //TODO: Use a configuration instead
  187. Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
  188. return $token;
  189. }
  190. public static function deleteCookie() {
  191. $token = Minz_Session::getLongTermCookie('FreshRSS_login');
  192. Minz_Session::deleteLongTermCookie('FreshRSS_login');
  193. if (ctype_alnum($token)) {
  194. @unlink(DATA_PATH . '/tokens/' . $token . '.txt');
  195. }
  196. if (rand(0, 10) === 1) {
  197. self::purgeTokens();
  198. }
  199. }
  200. public static function purgeTokens() {
  201. $oldest = time() - 2629744; // 1 month // TODO: Use a configuration instead
  202. foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $file_info) {
  203. // $extension = $file_info->getExtension(); doesn't work in PHP < 5.3.7
  204. $extension = pathinfo($file_info->getFilename(), PATHINFO_EXTENSION);
  205. if ($extension === 'txt' && $file_info->getMTime() < $oldest) {
  206. @unlink($file_info->getPathname());
  207. }
  208. }
  209. }
  210. }