diff --git a/config-templates/authsources.php b/config-templates/authsources.php index e80d69a0fae2727094a4aa78e346fc0403003efc..093e9dfaa5b5330e1140f72d593418e35023cfd7 100644 --- a/config-templates/authsources.php +++ b/config-templates/authsources.php @@ -74,6 +74,17 @@ $config = array( ), */ + /* + 'htpasswd' => array( + 'authcrypt:Htpasswd', + 'htpasswd_file' => '/var/www/foo.edu/legacy_app/.htpasswd', + 'static_attributes' => array( + 'eduPersonAffiliation' => array('member', 'employee'), + 'Organization' => array('University of Foo'), + ), + ), + */ + /* // This authentication source serves as an example of integration with an // external authentication engine. Take a look at the comment in the beginning diff --git a/lib/SimpleSAML/Utils/Crypto.php b/lib/SimpleSAML/Utils/Crypto.php index 2ca47b6587da28d30dce52e5be5235da276af583..76c1b188818f2c6bc9abae7ca9a0767ea3a0eda8 100644 --- a/lib/SimpleSAML/Utils/Crypto.php +++ b/lib/SimpleSAML/Utils/Crypto.php @@ -81,4 +81,63 @@ class SimpleSAML_Utils_Crypto { return $crypted === $clear; } } + + /** + * This function generates an Apache 'apr1' password hash, which uses a modified + * version of MD5: http://httpd.apache.org/docs/2.2/misc/password_encryptions.html + * @param $password The unencrypted password + * @param $salt Optional salt + */ + public static function apr1Md5Hash($password, $salt = NULL) { + assert('is_string($password)'); + + $chars = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + if(!$salt) { + $salt = substr(str_shuffle($allowed_chars), 0, 8); + } + + $len = strlen($password); + $text = $password.'$apr1$'.$salt; + $bin = pack("H32", md5($password.$salt.$password)); + for($i = $len; $i > 0; $i -= 16) { $text .= substr($bin, 0, min(16, $i)); } + for($i = $len; $i > 0; $i >>= 1) { $text .= ($i & 1) ? chr(0) : $password{0}; } + $bin = pack("H32", md5($text)); + for($i = 0; $i < 1000; $i++) { + $new = ($i & 1) ? $password : $bin; + if ($i % 3) $new .= $salt; + if ($i % 7) $new .= $password; + $new .= ($i & 1) ? $bin : $password; + $bin = pack("H32", md5($new)); + } + $tmp= ''; + for ($i = 0; $i < 5; $i++) { + $k = $i + 6; + $j = $i + 12; + if ($j == 16) $j = 5; + $tmp = $bin[$i].$bin[$k].$bin[$j].$tmp; + } + $tmp = chr(0).chr(0).$bin[11].$tmp; + $tmp = strtr( + strrev(substr(base64_encode($tmp), 2)), + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", + $chars + ); + return "$"."apr1"."$".$salt."$".$tmp; + } + + + /** + * This function verifies an Apache 'apr1' password hash + */ + public static function apr1Md5Valid($crypted, $clear) { + assert('is_string($crypted)'); + assert('is_string($clear)'); + $pattern = '/^\$apr1\$([A-Za-z0-9\.\/]{8})\$([A-Za-z0-9\.\/]{22})$/'; + + if(preg_match($pattern, $crypted, $matches)) { + $salt = $matches[1]; + return ( $crypted == self::apr1Md5Hash($clear, $salt) ); + } + return FALSE; + } } diff --git a/modules/authcrypt/lib/Auth/Source/Htpasswd.php b/modules/authcrypt/lib/Auth/Source/Htpasswd.php new file mode 100644 index 0000000000000000000000000000000000000000..88ba76819247664b81de5ec4606d96f034994d1b --- /dev/null +++ b/modules/authcrypt/lib/Auth/Source/Htpasswd.php @@ -0,0 +1,97 @@ +<?php + +/** + * Authentication source for Apache 'htpasswd' files. + * + * @author Dyonisius (Dick) Visser, TERENA. + * @package simpleSAMLphp + * @version $Id$ + */ +class sspmod_authcrypt_Auth_Source_Htpasswd extends sspmod_core_Auth_UserPassBase { + + + /** + * Our users, stored in an array, where each value is "<username>:<passwordhash>". + */ + private $users; + + /** + * Constructor for this authentication source. + * + * @param array $info Information about this authentication source. + * @param array $config Configuration. + */ + public function __construct($info, $config) { + assert('is_array($info)'); + assert('is_array($config)'); + + /* Call the parent constructor first, as required by the interface. */ + parent::__construct($info, $config); + + $this->users = array(); + + if(!$htpasswd = file_get_contents($config['htpasswd_file'])) { + throw new Exception('Could not read ' . $config['htpasswd_file']); + } + + $this->users = explode("\n", trim($htpasswd)); + + try { + $this->attributes = SimpleSAML_Utilities::parseAttributes($config['static_attributes']); + } catch(Exception $e) { + throw new Exception('Invalid static_attributes in authentication source ' . + $this->authId . ': ' . $e->getMessage()); + } + } + + + /** + * Attempt to log in using the given username and password. + * + * On a successful login, this function should return the username as 'uid' attribute, + * and merged attributes from the configuration file. + * On failure, it should throw an exception. A SimpleSAML_Error_Error('WRONGUSERPASS') + * should be thrown in case of a wrong username OR a wrong password, to prevent the + * enumeration of usernames. + * + * @param string $username The username the user wrote. + * @param string $password The password the user wrote. + * @return array Associative array with the users attributes. + */ + protected function login($username, $password) { + assert('is_string($username)'); + assert('is_string($password)'); + + foreach($this->users as $userpass) { + $matches = explode(':', $userpass, 2); + if($matches[0] == $username) { + + $crypted = $matches[1]; + + // This is about the only attribute we can add + $attributes = array_merge(array('uid' => array($username)), $this->attributes); + + // Traditional crypt(3) + if(crypt($password, $crypted) == $crypted) { + SimpleSAML_Logger::debug('User '. $username . ' authenticated successfully'); + return $attributes; + } + + // Apache's custom MD5 + if(SimpleSAML_Utils_Crypto::apr1Md5Valid($crypted, $password)) { + SimpleSAML_Logger::debug('User '. $username . ' authenticated successfully'); + return $attributes; + } + + // SHA1 or plain-text + if(SimpleSAML_Utils_Crypto::pwValid($crypted, $password)) { + SimpleSAML_Logger::debug('User '. $username . ' authenticated successfully'); + return $attributes; + } + throw new SimpleSAML_Error_Error('WRONGUSERPASS'); + } + } + throw new SimpleSAML_Error_Error('WRONGUSERPASS'); + } + +}