/inc/auth.php
PHP | 432 lines | 327 code | 56 blank | 49 comment | 78 complexity | 3fb0b238881c67b4e72b61267199cde4 MD5 | raw file
Possible License(s): BSD-3-Clause
- <?php # vim:ts=2:sw=2:et:
- /* For licensing and copyright terms, see the file named LICENSE */
- interface IMTrackAuth {
- /** Returns the authenticated user, or null if authentication is
- * required */
- function authenticate();
- /** Called if the user is not authenticated as a registered
- * user and if the page requires it.
- * Should initiate whatever is appropriate to begin the authentication
- * process (eg: displaying logon information).
- * You may assume that no output has been sent to the client at
- * the time that this function is called.
- * Returns null if not supported, throw an exception if failed,
- * else return a the authenticated user (if it can be determined
- * by the time the function returns).
- * If an alternate login page is displayed, this function should
- * exit instead of returning.
- */
- function doAuthenticate();
- /** Returns a list of available groups.
- * Returns null if not supported, throw an exception if failed. */
- function enumGroups();
- /** Returns a list of groups that a given user belongs to.
- * Returns null if not supported, throw an exception if failed. */
- function getGroups($username);
- /** Adds a user to a group.
- * Returns null if not supported, throw an exception if failed,
- * return true if succeeded */
- function addToGroup($username, $groupname);
- /** Removes a user from a group.
- * Returns null if not supported, throw an exception if failed,
- * return true if succeeded */
- function removeFromGroup($username, $groupname);
- }
- class MTrackAuth_HTTP implements IMTrackAuth {
- public $htgroup = null;
- public $htpasswd = null;
- public $use_digest = false;
- public $realm = 'mtrack';
- function __construct($group = null, $passwd = null) {
- $this->htgroup = $group;
- if ($passwd !== null) {
- if (!strncmp('digest:', $passwd, 7)) {
- $this->use_digest = true;
- $passwd = substr($passwd, 7);
- }
- $this->htpasswd = $passwd;
- }
- MTrackAuth::registerMech($this);
- }
- function parseDigest($string)
- {
- $resp = trim($string);
- $DIG = array();
- while (strlen($resp)) {
- if (!preg_match('/^([a-z]+)\s*=\s*(.*)$/', $resp, $M)) {
- # error_log("unable to parse $string [$resp]");
- return null;
- }
- $name = $M[1];
- $param = null;
- $rest = $M[2];
- if ($rest[0] == '"' || $rest[0] == "'") {
- $delim = $rest[0];
- $delim_offset = 1;
- } else {
- $delim = ',';
- $delim_offset = 0;
- }
- $len = strlen($rest);
- $i = $delim_offset;
- while ($i < $len) {
- if ($delim != ',' && $rest[$i] == '\\') {
- $i += 2;
- if ($i >= $len) {
- # error_log("unable to parse $string (unterminated quotes)");
- return null;
- }
- continue;
- }
- if ($rest[$i] == $delim) {
- $param = substr($rest, $delim_offset, $i - $delim_offset);
- $resp = substr($rest, $i + 1);
- break;
- }
- $i++;
- }
- if ($param === null && $delim != ',') {
- # error_log("unable to parse $string, unterminated delim $delim");
- return null;
- }
- if ($param === null) {
- $param = $rest;
- $resp = '';
- }
- $DIG[$name] = $param;
- if (preg_match('/^,\s*(.*)$/', $resp, $M)) {
- $resp = $M[1];
- }
- $resp = trim($resp);
- }
- return $DIG;
- }
- /* Leave authentication to the web server configuration */
- function authenticate() {
- /* web server based auth */
- if (isset($_SERVER['REMOTE_USER'])) {
- return $_SERVER['REMOTE_USER'];
- }
- /* PHP based auth */
- if (($this->use_digest && isset($_SERVER['PHP_AUTH_DIGEST'])) ||
- (!$this->use_digest && isset($_SERVER['PHP_AUTH_USER'])))
- {
- /* validate the password */
- if ($this->use_digest) {
- /* parse the digest response */
- $DIG = $this->parseDigest($_SERVER['PHP_AUTH_DIGEST']);
- if ($DIG['nc'] != '00000001') {
- // only allow a nonce-count of 1
- return null;
- }
- if ($DIG['realm'] != $this->realm) {
- return null;
- }
- $secret = $this->getSecret();
- $domain = $_GLOBALS['ABSWEB'];
- $opaque = sha1($domain . $secret);
- if ($DIG['opaque'] != $opaque) {
- // secret expired
- return null;
- }
- $user = $DIG['username'];
- } else {
- $user = $_SERVER['PHP_AUTH_USER'];
- }
- if (!strlen($user)) {
- return null;
- }
- if ($this->htpasswd === null) {
- error_log("no password file defined, unable to validate $user");
- return null;
- }
- $fp = fopen($this->htpasswd, 'r');
- if (!$fp) {
- error_log("unable to open password file to validate user $user");
- return null;
- }
- if (!flock($fp, LOCK_SH)) {
- error_log("unable to lock password file to validate user $user");
- return null;
- }
- $puser = preg_quote($user);
- $correct_password = null;
- while (true) {
- $line = fgets($fp);
- if ($line === false) {
- $user = false;
- break;
- }
- if ($this->use_digest) {
- if (preg_match("/^$puser:(.*):(.*)$/", $line, $M)) {
- if ($M[1] != $this->realm) {
- continue;
- }
- // $M[2] is: md5($user . ":" . $realm . ":" . $pw)
- $expect = $M[2];
- $uri = md5($_SERVER['REQUEST_METHOD'] . ':' . $DIG['uri']);
- $resp = md5("$expect:$DIG[nonce]:$DIG[nc]:$DIG[cnonce]:$DIG[qop]:$uri");
- if ($resp != $DIG['response']) {
- /* invalid */
- $user = null;
- }
- break;
- }
- } else {
- if (preg_match("/^$puser\s*:\s*(\S+)/", $line, $M)) {
- if (crypt($_SERVER['PHP_AUTH_PW'], $M[1]) != $M[1]) {
- /* invalid */
- $user = null;
- }
- break;
- }
- }
- }
- flock($fp, LOCK_UN);
- $fp = null;
- return $user;
- }
- return null;
- }
- function getSecret() {
- $secret_file = dirname(__FILE__) . '/../var/.digest.secret';
- if (file_exists($secret_file)) {
- if (filemtime($secret_file) + 300 > time()) {
- return file_get_contents($secret_file);
- }
- unlink($secret_file);
- }
- $secret = uniqid();
- file_put_contents($secret_file, $secret);
- return $secret;
- }
- function doAuthenticate() {
- /* This is only triggered if the web server isn't configured
- * to handle auth itself */
- $realm = $this->realm;
- if ($this->use_digest) {
- $secret = $this->getSecret();
- $nonce = sha1(uniqid() . $secret);
- $domain = $_GLOBALS['ABSWEB'];
- $opaque = sha1($domain . $secret);
- header("WWW-Authenticate: Digest realm=\"$realm\",qop=\"auth\",nonce=\"$nonce\",opaque=\"$opaque\"");
- } else {
- header("WWW-Authenticate: Basic realm=\"$realm\"");
- }
- header('HTTP/1.0 401 Unauthorized');
- ?>
- <h1>Authentication Required</h1>
- <p>I need to know who you are to allow you to access to this site.</p>
- <?php
- exit;
- }
- protected function readGroupFile($filename) {
- $fp = fopen($filename, 'r');
- if (!$fp) return null;
- if (!flock($fp, LOCK_SH)) return null;
- /* an apache style group file */
- $groups = array();
- $users = array();
- while (true) {
- $line = fgets($fp);
- if ($line === false) {
- break;
- }
- $line = trim($line);
- if ($line[0] == '#') {
- continue;
- }
- if (preg_match('/^([a-zA-Z][a-zA-Z0-9_]+)\s*:\s*(.*)$/', $line,
- $M)) {
- $groupname = $M[1];
- $members = $M[2];
- foreach (preg_split('/\s+/', $members) as $user) {
- $users[$user][] = $groupname;
- $groups[$groupname][] = $user;
- }
- }
- }
- flock($fp, LOCK_UN);
- $fp = null;
- return array($groups, $users);
- }
- function enumGroups() {
- if (strlen($this->htgroup)) {
- list($groups, $users) = $this->readGroupFile($this->htgroup);
- return array_keys($groups);
- }
- return null;
- }
- function getGroups($username) {
- if (strlen($this->htgroup)) {
- list($groups, $users) = $this->readGroupFile($this->htgroup);
- return $users[$username];
- }
- return null;
- }
- function addToGroup($username, $groupname)
- {
- return null;
- }
- function removeFromGroup($username, $groupname)
- {
- return null;
- }
- }
- class MTrackAuth
- {
- static $stack = array();
- static $mechs = array();
- static $group_assoc = array();
- public static function registerMech(IMTrackAuth $mech) {
- self::$mechs[] = $mech;
- }
- /** switch user */
- public static function su($user) {
- if (!strlen($user)) throw new Exception("invalid user");
- array_unshift(self::$stack, $user);
- }
- /** drop identity set by last su */
- public static function drop() {
- if (count(self::$stack) == 0) {
- throw new Exception("no privs to drop");
- }
- return array_shift(self::$stack);
- }
- /** returns the authenticated user, or null if authentication
- * is required */
- public static function authenticate() {
- foreach (self::$mechs as $mech) {
- $name = $mech->authenticate();
- if ($name !== null) {
- return $name;
- }
- }
- /* always fall back on the unix username when running from
- * the console */
- if (php_sapi_name() == 'cli') {
- static $envs = array('MTRACK_LOGNAME', 'LOGNAME', 'USER');
- foreach ($envs as $name) {
- if (isset($_ENV[$name])) {
- return $_ENV[$name];
- }
- }
- }
- return null;
- }
- /** determine the current identity. If doauth is true (default),
- * then the authentication hook will be invoked */
- public static function whoami($doauth = true) {
- if (count(self::$stack) == 0 && $doauth) {
- try {
- $who = self::authenticate();
- if ($who === null) {
- foreach (self::$mechs as $mech) {
- $who = $mech->doAuthenticate();
- if ($who !== null) {
- break;
- }
- }
- }
- if ($who !== null) {
- self::su($who);
- }
- } catch (Exception $e) {
- header('HTTP/1.0 401 Unauthorized');
- echo "<h1>Not authorized</h1>";
- echo htmlentities($e->getMessage());
- error_log($e->getMessage());
- exit;
- }
- }
- if (!count(self::$stack)) {
- return "anonymous";
- }
- return self::$stack[0];
- }
- static function getGroups() {
- $user = self::whoami();
- if (isset(self::$group_assoc[$user])) {
- return self::$group_assoc[$user];
- }
- $roles = array($user => $user);
- $user_class = MTrackConfig::get('user_classes', $user);
- if ($user_class === null) {
- if ($user == 'anonymous') {
- $user_class = 'anonymous';
- } else {
- $user_class = 'authenticated';
- }
- }
- $class_roles = MTrackConfig::get('user_class_roles', $user_class);
- foreach (preg_split('/\s*,\s*/', $class_roles) as $role) {
- $roles[$role] = $role;
- }
- foreach (self::$mechs as $mech) {
- $g = $mech->getGroups($user);
- if (is_array($g)) {
- foreach ($g as $grp) {
- $roles[$grp] = $grp;
- }
- }
- }
- self::$group_assoc[$user] = $roles;
- return $roles;
- }
- }