PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/auth.php

https://bitbucket.org/scoates/mtrack
PHP | 432 lines | 327 code | 56 blank | 49 comment | 78 complexity | 3fb0b238881c67b4e72b61267199cde4 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php # vim:ts=2:sw=2:et:
  2. /* For licensing and copyright terms, see the file named LICENSE */
  3. interface IMTrackAuth {
  4. /** Returns the authenticated user, or null if authentication is
  5. * required */
  6. function authenticate();
  7. /** Called if the user is not authenticated as a registered
  8. * user and if the page requires it.
  9. * Should initiate whatever is appropriate to begin the authentication
  10. * process (eg: displaying logon information).
  11. * You may assume that no output has been sent to the client at
  12. * the time that this function is called.
  13. * Returns null if not supported, throw an exception if failed,
  14. * else return a the authenticated user (if it can be determined
  15. * by the time the function returns).
  16. * If an alternate login page is displayed, this function should
  17. * exit instead of returning.
  18. */
  19. function doAuthenticate();
  20. /** Returns a list of available groups.
  21. * Returns null if not supported, throw an exception if failed. */
  22. function enumGroups();
  23. /** Returns a list of groups that a given user belongs to.
  24. * Returns null if not supported, throw an exception if failed. */
  25. function getGroups($username);
  26. /** Adds a user to a group.
  27. * Returns null if not supported, throw an exception if failed,
  28. * return true if succeeded */
  29. function addToGroup($username, $groupname);
  30. /** Removes a user from a group.
  31. * Returns null if not supported, throw an exception if failed,
  32. * return true if succeeded */
  33. function removeFromGroup($username, $groupname);
  34. }
  35. class MTrackAuth_HTTP implements IMTrackAuth {
  36. public $htgroup = null;
  37. public $htpasswd = null;
  38. public $use_digest = false;
  39. public $realm = 'mtrack';
  40. function __construct($group = null, $passwd = null) {
  41. $this->htgroup = $group;
  42. if ($passwd !== null) {
  43. if (!strncmp('digest:', $passwd, 7)) {
  44. $this->use_digest = true;
  45. $passwd = substr($passwd, 7);
  46. }
  47. $this->htpasswd = $passwd;
  48. }
  49. MTrackAuth::registerMech($this);
  50. }
  51. function parseDigest($string)
  52. {
  53. $resp = trim($string);
  54. $DIG = array();
  55. while (strlen($resp)) {
  56. if (!preg_match('/^([a-z]+)\s*=\s*(.*)$/', $resp, $M)) {
  57. # error_log("unable to parse $string [$resp]");
  58. return null;
  59. }
  60. $name = $M[1];
  61. $param = null;
  62. $rest = $M[2];
  63. if ($rest[0] == '"' || $rest[0] == "'") {
  64. $delim = $rest[0];
  65. $delim_offset = 1;
  66. } else {
  67. $delim = ',';
  68. $delim_offset = 0;
  69. }
  70. $len = strlen($rest);
  71. $i = $delim_offset;
  72. while ($i < $len) {
  73. if ($delim != ',' && $rest[$i] == '\\') {
  74. $i += 2;
  75. if ($i >= $len) {
  76. # error_log("unable to parse $string (unterminated quotes)");
  77. return null;
  78. }
  79. continue;
  80. }
  81. if ($rest[$i] == $delim) {
  82. $param = substr($rest, $delim_offset, $i - $delim_offset);
  83. $resp = substr($rest, $i + 1);
  84. break;
  85. }
  86. $i++;
  87. }
  88. if ($param === null && $delim != ',') {
  89. # error_log("unable to parse $string, unterminated delim $delim");
  90. return null;
  91. }
  92. if ($param === null) {
  93. $param = $rest;
  94. $resp = '';
  95. }
  96. $DIG[$name] = $param;
  97. if (preg_match('/^,\s*(.*)$/', $resp, $M)) {
  98. $resp = $M[1];
  99. }
  100. $resp = trim($resp);
  101. }
  102. return $DIG;
  103. }
  104. /* Leave authentication to the web server configuration */
  105. function authenticate() {
  106. /* web server based auth */
  107. if (isset($_SERVER['REMOTE_USER'])) {
  108. return $_SERVER['REMOTE_USER'];
  109. }
  110. /* PHP based auth */
  111. if (($this->use_digest && isset($_SERVER['PHP_AUTH_DIGEST'])) ||
  112. (!$this->use_digest && isset($_SERVER['PHP_AUTH_USER'])))
  113. {
  114. /* validate the password */
  115. if ($this->use_digest) {
  116. /* parse the digest response */
  117. $DIG = $this->parseDigest($_SERVER['PHP_AUTH_DIGEST']);
  118. if ($DIG['nc'] != '00000001') {
  119. // only allow a nonce-count of 1
  120. return null;
  121. }
  122. if ($DIG['realm'] != $this->realm) {
  123. return null;
  124. }
  125. $secret = $this->getSecret();
  126. $domain = $_GLOBALS['ABSWEB'];
  127. $opaque = sha1($domain . $secret);
  128. if ($DIG['opaque'] != $opaque) {
  129. // secret expired
  130. return null;
  131. }
  132. $user = $DIG['username'];
  133. } else {
  134. $user = $_SERVER['PHP_AUTH_USER'];
  135. }
  136. if (!strlen($user)) {
  137. return null;
  138. }
  139. if ($this->htpasswd === null) {
  140. error_log("no password file defined, unable to validate $user");
  141. return null;
  142. }
  143. $fp = fopen($this->htpasswd, 'r');
  144. if (!$fp) {
  145. error_log("unable to open password file to validate user $user");
  146. return null;
  147. }
  148. if (!flock($fp, LOCK_SH)) {
  149. error_log("unable to lock password file to validate user $user");
  150. return null;
  151. }
  152. $puser = preg_quote($user);
  153. $correct_password = null;
  154. while (true) {
  155. $line = fgets($fp);
  156. if ($line === false) {
  157. $user = false;
  158. break;
  159. }
  160. if ($this->use_digest) {
  161. if (preg_match("/^$puser:(.*):(.*)$/", $line, $M)) {
  162. if ($M[1] != $this->realm) {
  163. continue;
  164. }
  165. // $M[2] is: md5($user . ":" . $realm . ":" . $pw)
  166. $expect = $M[2];
  167. $uri = md5($_SERVER['REQUEST_METHOD'] . ':' . $DIG['uri']);
  168. $resp = md5("$expect:$DIG[nonce]:$DIG[nc]:$DIG[cnonce]:$DIG[qop]:$uri");
  169. if ($resp != $DIG['response']) {
  170. /* invalid */
  171. $user = null;
  172. }
  173. break;
  174. }
  175. } else {
  176. if (preg_match("/^$puser\s*:\s*(\S+)/", $line, $M)) {
  177. if (crypt($_SERVER['PHP_AUTH_PW'], $M[1]) != $M[1]) {
  178. /* invalid */
  179. $user = null;
  180. }
  181. break;
  182. }
  183. }
  184. }
  185. flock($fp, LOCK_UN);
  186. $fp = null;
  187. return $user;
  188. }
  189. return null;
  190. }
  191. function getSecret() {
  192. $secret_file = dirname(__FILE__) . '/../var/.digest.secret';
  193. if (file_exists($secret_file)) {
  194. if (filemtime($secret_file) + 300 > time()) {
  195. return file_get_contents($secret_file);
  196. }
  197. unlink($secret_file);
  198. }
  199. $secret = uniqid();
  200. file_put_contents($secret_file, $secret);
  201. return $secret;
  202. }
  203. function doAuthenticate() {
  204. /* This is only triggered if the web server isn't configured
  205. * to handle auth itself */
  206. $realm = $this->realm;
  207. if ($this->use_digest) {
  208. $secret = $this->getSecret();
  209. $nonce = sha1(uniqid() . $secret);
  210. $domain = $_GLOBALS['ABSWEB'];
  211. $opaque = sha1($domain . $secret);
  212. header("WWW-Authenticate: Digest realm=\"$realm\",qop=\"auth\",nonce=\"$nonce\",opaque=\"$opaque\"");
  213. } else {
  214. header("WWW-Authenticate: Basic realm=\"$realm\"");
  215. }
  216. header('HTTP/1.0 401 Unauthorized');
  217. ?>
  218. <h1>Authentication Required</h1>
  219. <p>I need to know who you are to allow you to access to this site.</p>
  220. <?php
  221. exit;
  222. }
  223. protected function readGroupFile($filename) {
  224. $fp = fopen($filename, 'r');
  225. if (!$fp) return null;
  226. if (!flock($fp, LOCK_SH)) return null;
  227. /* an apache style group file */
  228. $groups = array();
  229. $users = array();
  230. while (true) {
  231. $line = fgets($fp);
  232. if ($line === false) {
  233. break;
  234. }
  235. $line = trim($line);
  236. if ($line[0] == '#') {
  237. continue;
  238. }
  239. if (preg_match('/^([a-zA-Z][a-zA-Z0-9_]+)\s*:\s*(.*)$/', $line,
  240. $M)) {
  241. $groupname = $M[1];
  242. $members = $M[2];
  243. foreach (preg_split('/\s+/', $members) as $user) {
  244. $users[$user][] = $groupname;
  245. $groups[$groupname][] = $user;
  246. }
  247. }
  248. }
  249. flock($fp, LOCK_UN);
  250. $fp = null;
  251. return array($groups, $users);
  252. }
  253. function enumGroups() {
  254. if (strlen($this->htgroup)) {
  255. list($groups, $users) = $this->readGroupFile($this->htgroup);
  256. return array_keys($groups);
  257. }
  258. return null;
  259. }
  260. function getGroups($username) {
  261. if (strlen($this->htgroup)) {
  262. list($groups, $users) = $this->readGroupFile($this->htgroup);
  263. return $users[$username];
  264. }
  265. return null;
  266. }
  267. function addToGroup($username, $groupname)
  268. {
  269. return null;
  270. }
  271. function removeFromGroup($username, $groupname)
  272. {
  273. return null;
  274. }
  275. }
  276. class MTrackAuth
  277. {
  278. static $stack = array();
  279. static $mechs = array();
  280. static $group_assoc = array();
  281. public static function registerMech(IMTrackAuth $mech) {
  282. self::$mechs[] = $mech;
  283. }
  284. /** switch user */
  285. public static function su($user) {
  286. if (!strlen($user)) throw new Exception("invalid user");
  287. array_unshift(self::$stack, $user);
  288. }
  289. /** drop identity set by last su */
  290. public static function drop() {
  291. if (count(self::$stack) == 0) {
  292. throw new Exception("no privs to drop");
  293. }
  294. return array_shift(self::$stack);
  295. }
  296. /** returns the authenticated user, or null if authentication
  297. * is required */
  298. public static function authenticate() {
  299. foreach (self::$mechs as $mech) {
  300. $name = $mech->authenticate();
  301. if ($name !== null) {
  302. return $name;
  303. }
  304. }
  305. /* always fall back on the unix username when running from
  306. * the console */
  307. if (php_sapi_name() == 'cli') {
  308. static $envs = array('MTRACK_LOGNAME', 'LOGNAME', 'USER');
  309. foreach ($envs as $name) {
  310. if (isset($_ENV[$name])) {
  311. return $_ENV[$name];
  312. }
  313. }
  314. }
  315. return null;
  316. }
  317. /** determine the current identity. If doauth is true (default),
  318. * then the authentication hook will be invoked */
  319. public static function whoami($doauth = true) {
  320. if (count(self::$stack) == 0 && $doauth) {
  321. try {
  322. $who = self::authenticate();
  323. if ($who === null) {
  324. foreach (self::$mechs as $mech) {
  325. $who = $mech->doAuthenticate();
  326. if ($who !== null) {
  327. break;
  328. }
  329. }
  330. }
  331. if ($who !== null) {
  332. self::su($who);
  333. }
  334. } catch (Exception $e) {
  335. header('HTTP/1.0 401 Unauthorized');
  336. echo "<h1>Not authorized</h1>";
  337. echo htmlentities($e->getMessage());
  338. error_log($e->getMessage());
  339. exit;
  340. }
  341. }
  342. if (!count(self::$stack)) {
  343. return "anonymous";
  344. }
  345. return self::$stack[0];
  346. }
  347. static function getGroups() {
  348. $user = self::whoami();
  349. if (isset(self::$group_assoc[$user])) {
  350. return self::$group_assoc[$user];
  351. }
  352. $roles = array($user => $user);
  353. $user_class = MTrackConfig::get('user_classes', $user);
  354. if ($user_class === null) {
  355. if ($user == 'anonymous') {
  356. $user_class = 'anonymous';
  357. } else {
  358. $user_class = 'authenticated';
  359. }
  360. }
  361. $class_roles = MTrackConfig::get('user_class_roles', $user_class);
  362. foreach (preg_split('/\s*,\s*/', $class_roles) as $role) {
  363. $roles[$role] = $role;
  364. }
  365. foreach (self::$mechs as $mech) {
  366. $g = $mech->getGroups($user);
  367. if (is_array($g)) {
  368. foreach ($g as $grp) {
  369. $roles[$grp] = $grp;
  370. }
  371. }
  372. }
  373. self::$group_assoc[$user] = $roles;
  374. return $roles;
  375. }
  376. }