PageRenderTime 29ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/htdocs/auth/internal/lib.php

https://gitlab.com/mahara/mahara
PHP | 331 lines | 163 code | 31 blank | 137 comment | 47 complexity | 7de408607f5a39f284c801cc757dfca0 MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, MIT, LGPL-2.1, LGPL-3.0, GPL-3.0
  1. <?php
  2. /**
  3. *
  4. * @package mahara
  5. * @subpackage auth-internal
  6. * @author Catalyst IT Ltd
  7. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
  8. * @copyright For copyright information on Mahara, please see the README file distributed with this software.
  9. *
  10. */
  11. defined('INTERNAL') || die();
  12. require_once(get_config('docroot') . 'auth/lib.php');
  13. /**
  14. * The internal authentication method, which authenticates users against the
  15. * Mahara database.
  16. */
  17. class AuthInternal extends Auth {
  18. public function __construct($id = null) {
  19. $this->has_instance_config = false;
  20. $this->type = 'internal';
  21. if (!empty($id)) {
  22. return $this->init($id);
  23. }
  24. return true;
  25. }
  26. public function init($id) {
  27. $this->ready = parent::init($id);
  28. return true;
  29. }
  30. /**
  31. * Attempt to authenticate user
  32. *
  33. * @param object $user As returned from the usr table
  34. * @param string $password The password being used for authentication
  35. * @return bool True/False based on whether the user
  36. * authenticated successfully
  37. * @throws AuthUnknownUserException If the user does not exist
  38. */
  39. public function authenticate_user_account($user, $password) {
  40. $this->must_be_ready();
  41. $result = $this->validate_password($password, $user->password, $user->salt);
  42. // If result == 1, password is correct
  43. // If result > 1, password is correct but using old settings, should be changed
  44. if ($result > 1 ) {
  45. if ($user->passwordchange != 1) {
  46. $userobj = new User();
  47. $userobj->find_by_id($user->id);
  48. $this->change_password($userobj, $password);
  49. $user->password = $userobj->password;
  50. $user->salt = $userobj->salt;
  51. }
  52. }
  53. return $result > 0;
  54. }
  55. /**
  56. * Internal authentication never auto-creates users - users instead
  57. * register through register.php
  58. */
  59. public function can_auto_create_users() {
  60. return false;
  61. }
  62. public static function can_use_registration_captcha() {
  63. return true;
  64. }
  65. /**
  66. * For internal authentication, passwords can contain a range of letters,
  67. * numbers and symbols. There is a minimum limit of eight characters allowed
  68. * for the password, and no upper limit
  69. *
  70. * @param string $password The password to check
  71. * @return bool Whether the password is valid
  72. */
  73. public function is_password_valid($password) {
  74. list($minlength, $format) = get_password_policy(true);
  75. if (!preg_match('/^[a-zA-Z0-9 ~!@#\$%\^&\*\(\)_\-=\+\,\.<>\/\?;:"\[\]\{\}\\\|`\']{' . $minlength . ',}$/', $password)) {
  76. return false;
  77. }
  78. // for unicode character properties in php see:
  79. // http://php.net/manual/en/regexp.reference.unicode.php
  80. $containsUppercase = preg_match('/\p{Lu}/', $password); // '/[A-Z]/'
  81. $containsLowercase = preg_match('/\p{Ll}/', $password); // '/[a-z]/'
  82. $containsNumber = preg_match('/\pN/', $password); // '/\d/'
  83. $containsSymbol = preg_match('/[^\pL\pN]/', $password); // '/[^a-zA-Z\d]/'
  84. if ($format == 'ul') {
  85. return $containsUppercase && $containsLowercase;
  86. }
  87. if ($format == 'uln') {
  88. return ($containsUppercase && $containsLowercase && $containsNumber);
  89. }
  90. if ($format == 'ulns') {
  91. return ($containsUppercase && $containsLowercase && $containsNumber && $containsSymbol);
  92. }
  93. return false;
  94. }
  95. /**
  96. * Changes the user's password.
  97. *
  98. * This method is not strictly part of the authentication API, but if
  99. * defined allows the method to change a user's password.
  100. *
  101. * @param object $user The user to change the password for
  102. * @param string $password The password to set for the user
  103. * @param boolean $resetpasswordchange Whether to reset the passwordchange variable or not
  104. * @return string The new password, or empty if the password could not be set
  105. */
  106. public function change_password(User $user, $password, $resetpasswordchange = true, $quickhash = false) {
  107. $this->must_be_ready();
  108. // Create a salted password and set it for the user
  109. $user->salt = substr(md5(rand(1000000, 9999999)), 2, 8);
  110. if ($quickhash) {
  111. // $6$ is SHA512, used as a quick hash instead of bcrypt for now.
  112. $user->password = $this->encrypt_password($password, $user->salt, '$6$', get_config('passwordsaltmain'));
  113. }
  114. else {
  115. // $2a$ is bcrypt hash. See http://php.net/manual/en/function.crypt.php
  116. // It requires a cost, a 2 digit number in the range 04-31
  117. $user->password = $this->encrypt_password($password, $user->salt, '$2a$' . get_config('bcrypt_cost') . '$', get_config('passwordsaltmain'));
  118. }
  119. if ($resetpasswordchange) {
  120. $user->passwordchange = 0;
  121. }
  122. $user->commit();
  123. return $user->password;
  124. }
  125. /**
  126. * Internal authentication allows most standard us-keyboard-typable characters
  127. * for username, as long as the username is between three and thirty
  128. * characters in length.
  129. *
  130. * This method is NOT part of the authentication API. Other authentication
  131. * methods never have to do anything regarding usernames being validated on
  132. * the Mahara side, so they do not need this method.
  133. *
  134. * @param string $username The username to check
  135. * @return bool Whether the username is valid
  136. */
  137. public function is_username_valid($username) {
  138. return preg_match('/^[a-zA-Z0-9!@#$%^&*()\-_=+\[{\]}\\|;:\'",<\.>\/?`]{3,30}$/', $username);
  139. }
  140. /**
  141. * Internal authentication allows most standard us-keyboard-typable characters
  142. * for username, as long as the username is between three and 236
  143. * characters in length.
  144. *
  145. * This method is NOT part of the authentication API. Other authentication
  146. * methods never have to do anything regarding usernames being validated on
  147. * the Mahara side, so they do not need this method.
  148. *
  149. * This method is meant to only be called for validation by an admin of the user
  150. * and is able to set a password longer than thirty characters in length
  151. *
  152. * @param string $username The username to check
  153. * @return bool Whether the username is valid
  154. */
  155. public function is_username_valid_admin($username) {
  156. return preg_match('/^[a-zA-Z0-9!@#$%^&*()\-_=+\[{\]}\\|;:\'",<\.>\/?`]{3,236}$/', $username);
  157. }
  158. /**
  159. * Changes the user's username.
  160. *
  161. * This method is not strictly part of the authentication API, but if
  162. * defined allows the method to change a user's username.
  163. *
  164. * @param object $user The user to change the password for
  165. * @param string $username The username to set for the user
  166. * @return string The new username, or the original username if it could not be set
  167. */
  168. public function change_username(User $user, $username) {
  169. global $USER;
  170. $this->must_be_ready();
  171. // proposed username must pass validation
  172. $valid = false;
  173. if ($USER->is_admin_for_user($user)) {
  174. $valid = $this->is_username_valid_admin($username);
  175. } else {
  176. $valid = $this->is_username_valid($username);
  177. }
  178. if ($valid) {
  179. $user->username = $username;
  180. $user->commit();
  181. }
  182. // return the new username, or the original one if it failed validation
  183. return $user->username;
  184. }
  185. /*
  186. The following two functions are inspired by Andrew McMillan's salted md5
  187. functions in AWL, adapted with his kind permission. Changed to use sha1
  188. and match the coding standards for Mahara.
  189. */
  190. /**
  191. * Given a password and an optional salt, encrypt the given password.
  192. *
  193. * Passwords are stored in SHA1 form.
  194. *
  195. * @param string $password The password to encrypt
  196. * @param string $salt The salt to use to encrypt the password
  197. * @param string $alg The algorithm to use, defaults to $6$ which is SHA512
  198. * @param string $sitesalt A salt to combine with the user's salt to add an extra layer or salting
  199. * @todo salt mandatory
  200. */
  201. public function encrypt_password($password, $salt='', $alg='$6$', $sitesalt='') {
  202. if ($salt == '') {
  203. $salt = substr(md5(rand(1000000, 9999999)), 2, 8);
  204. }
  205. if ($alg == '$6$') { // $6$ is the identifier for the SHA512 algorithm
  206. // Return a hash which is sha512(originalHash, salt), where original is sha1(salt + password)
  207. $password = sha1($salt . $password);
  208. // Generate a salt based on a supplied salt and the passwordsaltmain
  209. $fullsalt = substr(md5($sitesalt . $salt), 0, 16); // SHA512 expects 16 chars of salt
  210. }
  211. else { // This is most likely bcrypt $2a$, but any other algorithm can take up to 22 chars of salt
  212. // Generate a salt based on a supplied salt and the passwordsaltmain
  213. $fullsalt = substr(md5($sitesalt . $salt), 0, 22); // bcrypt expects 22 chars of salt
  214. }
  215. $hash = crypt($password, $alg . $fullsalt);
  216. // Strip out the computed salt
  217. // We strip out the salt hide the computed salt (in case the sitesalt was used which isn't in the database)
  218. $hash = substr($hash, 0, strlen($alg)) . substr($hash, strlen($alg)+strlen($fullsalt));
  219. return $hash;
  220. }
  221. /**
  222. * Given a password that the user has sent, the password we have for them
  223. * and the salt we have, see if the password they sent is correct.
  224. *
  225. * @param string $theysent The password the user sent
  226. * @param string $wehave The salted and hashed password we have in the database for them
  227. * @param string $salt The salt we have.
  228. * @returns int 0 means not validated, 1 means validated, 2 means validated but needs updating
  229. */
  230. protected function validate_password($theysent, $wehave, $salt) {
  231. $this->must_be_ready();
  232. if ($salt == '*') {
  233. // This is a special salt that means this user simply CAN'T log in.
  234. // It is used on the root user (id=0)
  235. return false;
  236. }
  237. if (empty($wehave)) {
  238. // This means the user has not been set up completely yet
  239. // Common cause is that still in registration phase
  240. return false;
  241. }
  242. $sitesalt = get_config('passwordsaltmain');
  243. $bcrypt = substr($wehave, 0, 4) == '$2a$';
  244. if ($bcrypt) {
  245. $alg = substr($wehave, 0, 7);
  246. $hash = $this->encrypt_password($theysent, $salt, $alg, $sitesalt);
  247. }
  248. else {
  249. $alg = substr($wehave, 0, 3);
  250. $hash = $this->encrypt_password($theysent, $salt, $alg, $sitesalt);
  251. }
  252. if ($hash == $wehave) {
  253. if (!$bcrypt || substr($alg, 4, 2) != get_config('bcrypt_cost')) {
  254. // Either not using bcrypt yet, or the cost parameter has changed, update the hash
  255. return 2;
  256. }
  257. // Using bcrypt with the correct cost parameter, leave as is.
  258. return 1;
  259. }
  260. // See http://docs.moodle.org/20/en/Password_salting#Changing_the_salt
  261. if (!empty($sitesalt)) {
  262. // There is a sitesalt set, try without it, and update if passes
  263. $hash = $this->encrypt_password($theysent, $salt, $alg, '');
  264. if ($hash == $wehave) {
  265. return 2;
  266. }
  267. }
  268. for ($i = 1; $i <= 20; ++ $i) {
  269. // Try 20 alternate sitesalts
  270. $alt = get_config('passwordsaltalt' . $i);
  271. if (!empty($alt)) {
  272. $hash = $this->encrypt_password($theysent, $salt, $alg, $alt);
  273. if ($hash == $wehave) {
  274. return 2;
  275. }
  276. }
  277. }
  278. // Nothing works, fail
  279. return 0;
  280. }
  281. }
  282. /**
  283. * Plugin configuration class
  284. */
  285. class PluginAuthInternal extends PluginAuth {
  286. public static function has_config() {
  287. return false;
  288. }
  289. public static function get_config_options() {
  290. return array();
  291. }
  292. public static function has_instance_config() {
  293. return false;
  294. }
  295. public static function get_instance_config_options($institution, $instance = 0) {
  296. return array();
  297. }
  298. }