lib.php 12.1 KB
Newer Older
1
2
3
4
<?php
/**
 *
 * @package    mahara
Penny Leach's avatar
Penny Leach committed
5
 * @subpackage auth-internal
6
 * @author     Catalyst IT Ltd
7
8
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
 * @copyright  For copyright information on Mahara, please see the README file distributed with this software.
9
10
11
12
 *
 */

defined('INTERNAL') || die();
13
require_once(get_config('docroot') . 'auth/lib.php');
14

15
16
17
18
/**
 * The internal authentication method, which authenticates users against the
 * Mahara database.
 */
19
class AuthInternal extends Auth {
20

21
    public function __construct($id = null) {
22
        $this->has_instance_config = false;
23
        $this->type       = 'internal';
24
        if (!empty($id)) {
25
            return $this->init($id);
26
        }
27
        return true;
28
29
    }

30
31
32
    public function init($id) {
        $this->ready = parent::init($id);
        return true;
33
34
    }

35
    /**
36
     * Attempt to authenticate user
37
     *
38
39
40
41
42
     * @param object $user     As returned from the usr table
     * @param string $password The password being used for authentication
     * @return bool            True/False based on whether the user
     *                         authenticated successfully
     * @throws AuthUnknownUserException If the user does not exist
43
     */
44
45
    public function authenticate_user_account($user, $password) {
        $this->must_be_ready();
46
47
48
49
50
51
52
53
54
55
56
57
58
        $result = $this->validate_password($password, $user->password, $user->salt);
        // If result == 1, password is correct
        // If result > 1, password is correct but using old settings, should be changed
        if ($result > 1 ) {
            if ($user->passwordchange != 1) {
                $userobj = new User();
                $userobj->find_by_id($user->id);
                $this->change_password($userobj, $password);
                $user->password = $userobj->password;
                $user->salt = $userobj->salt;
            }
        }
        return $result > 0;
59
60
    }

61
    /**
Aaron Wells's avatar
Aaron Wells committed
62
     * Internal authentication never auto-creates users - users instead
63
64
65
66
67
68
     * register through register.php
     */
    public function can_auto_create_users() {
        return false;
    }

69
70
71
72
    public static function can_use_registration_captcha() {
        return true;
    }

73
74
    /**
     * For internal authentication, passwords can contain a range of letters,
Gregor Anzelj's avatar
Gregor Anzelj committed
75
     * numbers and symbols. There is a minimum limit of eight characters allowed
76
77
78
79
80
     * for the password, and no upper limit
     *
     * @param string $password The password to check
     * @return bool            Whether the password is valid
     */
81
    public function is_password_valid($password) {
Gregor Anzelj's avatar
Gregor Anzelj committed
82
83
84
        list($minlength, $format) = get_password_policy(true);

        if (!preg_match('/^[a-zA-Z0-9 ~!@#\$%\^&\*\(\)_\-=\+\,\.<>\/\?;:"\[\]\{\}\\\|`\']{' . $minlength . ',}$/', $password)) {
85
86
            return false;
        }
Gregor Anzelj's avatar
Gregor Anzelj committed
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

        $containsLetter = preg_match('/\pL/',       $password); // '/[a-zA-Z]/'
        $containsNumber = preg_match('/\pN/',       $password); // '/\d/'
        $containsSymbol = preg_match('/[^\pL\pN]/', $password); // '/[^a-zA-Z\d]/'

        if ($format == 'ul') {
            return $containsLetter;
        }
        if ($format == 'uln') {
            return ($containsLetter && $containsNumber);
        }
        if ($format == 'ulns') {
            return ($containsLetter && $containsNumber && $containsSymbol);
        }

        return false;
103
104
    }

Nigel McNie's avatar
Nigel McNie committed
105
106
107
    /**
     * Changes the user's password.
     *
108
109
110
     * This method is not strictly part of the authentication API, but if
     * defined allows the method to change a user's password.
     *
111
112
     * @param object  $user     The user to change the password for
     * @param string  $password The password to set for the user
113
     * @param boolean $resetpasswordchange Whether to reset the passwordchange variable or not
Nigel McNie's avatar
Nigel McNie committed
114
115
     * @return string The new password, or empty if the password could not be set
     */
116
    public function change_password(User $user, $password, $resetpasswordchange = true, $quickhash = false) {
117
        $this->must_be_ready();
Nigel McNie's avatar
Nigel McNie committed
118
        // Create a salted password and set it for the user
119
        $user->salt = substr(md5(rand(1000000, 9999999)), 2, 8);
120
        if ($quickhash) {
121
122
            // $6$ is SHA512, used as a quick hash instead of bcrypt for now.
            $user->password = $this->encrypt_password($password, $user->salt, '$6$', get_config('passwordsaltmain'));
123
124
125
126
        }
        else {
            // $2a$ is bcrypt hash. See http://php.net/manual/en/function.crypt.php
            // It requires a cost, a 2 digit number in the range 04-31
127
            $user->password = $this->encrypt_password($password, $user->salt, '$2a$' . get_config('bcrypt_cost') . '$', get_config('passwordsaltmain'));
128
        }
129
130
131
        if ($resetpasswordchange) {
            $user->passwordchange = 0;
        }
132
        $user->commit();
133
134
135
136
        return $user->password;
    }

    /**
137
     * Internal authentication allows most standard us-keyboard-typable characters
Aaron Wells's avatar
Aaron Wells committed
138
     * for username, as long as the username is between three and thirty
139
     * characters in length.
140
141
142
143
144
145
146
147
     *
     * This method is NOT part of the authentication API. Other authentication
     * methods never have to do anything regarding usernames being validated on
     * the Mahara side, so they do not need this method.
     *
     * @param string $username The username to check
     * @return bool            Whether the username is valid
     */
148
    public function is_username_valid($username) {
149
        return preg_match('/^[a-zA-Z0-9!@#$%^&*()\-_=+\[{\]}\\|;:\'",<\.>\/?`]{3,30}$/', $username);
Nigel McNie's avatar
Nigel McNie committed
150
    }
151
152
153
154
    /**


     * Internal authentication allows most standard us-keyboard-typable characters
Aaron Wells's avatar
Aaron Wells committed
155
     * for username, as long as the username is between three and 236
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
     * characters in length.
     *
     * This method is NOT part of the authentication API. Other authentication
     * methods never have to do anything regarding usernames being validated on
     * the Mahara side, so they do not need this method.
     *
     * This method is meant to only be called for validation by an admin of the user
     * and is able to set a password longer than thirty characters in length
     *
     * @param string $username The username to check
     * @return bool            Whether the username is valid
     */
    public function is_username_valid_admin($username) {
        return preg_match('/^[a-zA-Z0-9!@#$%^&*()\-_=+\[{\]}\\|;:\'",<\.>\/?`]{3,236}$/', $username);
    }
Nigel McNie's avatar
Nigel McNie committed
171

172
173
174
175
176
177
178
179
180
181
182
    /**
     * Changes the user's username.
     *
     * This method is not strictly part of the authentication API, but if
     * defined allows the method to change a user's username.
     *
     * @param object  $user     The user to change the password for
     * @param string  $username The username to set for the user
     * @return string The new username, or the original username if it could not be set
     */
    public function change_username(User $user, $username) {
183
184
        global $USER;

185
186
187
        $this->must_be_ready();

        // proposed username must pass validation
188
189
190
191
192
193
194
195
        $valid = false;
        if ($USER->is_admin_for_user($user)) {
            $valid = $this->is_username_valid_admin($username);
        } else {
            $valid = $this->is_username_valid($username);
        }

        if ($valid) {
196
197
198
199
200
201
202
203
            $user->username = $username;
            $user->commit();
        }

        // return the new username, or the original one if it failed validation
        return $user->username;
    }

204
205
206
207
208
    /*
     The following two functions are inspired by Andrew McMillan's salted md5
     functions in AWL, adapted with his kind permission. Changed to use sha1
     and match the coding standards for Mahara.
    */
209
210
211
212
213
214
215
216

   /**
    * Given a password and an optional salt, encrypt the given password.
    *
    * Passwords are stored in SHA1 form.
    *
    * @param string $password The password to encrypt
    * @param string $salt     The salt to use to encrypt the password
217
    * @param string $alg      The algorithm to use, defaults to $6$ which is SHA512
218
    * @param string $sitesalt A salt to combine with the user's salt to add an extra layer or salting
219
    * @todo salt mandatory
220
    */
221
    public function encrypt_password($password, $salt='', $alg='$6$', $sitesalt='') {
222
223
224
        if ($salt == '') {
            $salt = substr(md5(rand(1000000, 9999999)), 2, 8);
        }
225
226
227
        if ($alg == '$6$') { // $6$ is the identifier for the SHA512 algorithm
            // Return a hash which is sha512(originalHash, salt), where original is sha1(salt + password)
            $password = sha1($salt . $password);
228
229
            // Generate a salt based on a supplied salt and the passwordsaltmain
            $fullsalt = substr(md5($sitesalt . $salt), 0, 16); // SHA512 expects 16 chars of salt
230
231
        }
        else { // This is most likely bcrypt $2a$, but any other algorithm can take up to 22 chars of salt
232
233
            // Generate a salt based on a supplied salt and the passwordsaltmain
            $fullsalt = substr(md5($sitesalt . $salt), 0, 22); // bcrypt expects 22 chars of salt
234
235
236
237
238
239
        }
        $hash = crypt($password, $alg . $fullsalt);
        // Strip out the computed salt
        // We strip out the salt hide the computed salt (in case the sitesalt was used which isn't in the database)
        $hash = substr($hash, 0, strlen($alg)) . substr($hash, strlen($alg)+strlen($fullsalt));
        return $hash;
240
241
    }

242
243
244
245
246
    /**
     * Given a password that the user has sent, the password we have for them
     * and the salt we have, see if the password they sent is correct.
     *
     * @param string $theysent The password the user sent
247
     * @param string $wehave   The salted and hashed password we have in the database for them
248
     * @param string $salt     The salt we have.
249
     * @returns int     0 means not validated, 1 means validated, 2 means validated but needs updating
250
     */
251
    protected function validate_password($theysent, $wehave, $salt) {
252
        $this->must_be_ready();
253

Martyn Smith's avatar
Martyn Smith committed
254
255
256
257
258
259
        if ($salt == '*') {
            // This is a special salt that means this user simply CAN'T log in.
            // It is used on the root user (id=0)
            return false;
        }

260
261
262
263
264
265
        if (empty($wehave)) {
            // This means the user has not been set up completely yet
            // Common cause is that still in registration phase
            return false;
        }

266
        $sitesalt = get_config('passwordsaltmain');
267
268
269
        $bcrypt = substr($wehave, 0, 4) == '$2a$';
        if ($bcrypt) {
            $alg = substr($wehave, 0, 7);
270
            $hash = $this->encrypt_password($theysent, $salt, $alg, $sitesalt);
271
272
        }
        else {
273
274
            $alg = substr($wehave, 0, 3);
            $hash = $this->encrypt_password($theysent, $salt, $alg, $sitesalt);
275
276
277
278
279
280
281
282
283
        }
        if ($hash == $wehave) {
            if (!$bcrypt || substr($alg, 4, 2) != get_config('bcrypt_cost')) {
                // Either not using bcrypt yet, or the cost parameter has changed, update the hash
                return 2;
            }
            // Using bcrypt with the correct cost parameter, leave as is.
            return 1;
        }
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
        // See http://docs.moodle.org/20/en/Password_salting#Changing_the_salt
        if (!empty($sitesalt)) {
            // There is a sitesalt set, try without it, and update if passes
            $hash = $this->encrypt_password($theysent, $salt, $alg, '');
            if ($hash == $wehave) {
                return 2;
            }
        }
        for ($i = 1; $i <= 20; ++ $i) {
            // Try 20 alternate sitesalts
            $alt = get_config('passwordsaltalt' . $i);
            if (!empty($alt)) {
                $hash = $this->encrypt_password($theysent, $salt, $alg, $alt);
                if ($hash == $wehave) {
                    return 2;
                }
            }
        }
302
303
        // Nothing works, fail
        return 0;
304
    }
305

306
307
}

308
/**
309
 * Plugin configuration class
310
 */
311
class PluginAuthInternal extends PluginAuth {
312

313
    public static function has_config() {
314
        return false;
315
    }
316
317
318
319

    public static function get_config_options() {
        return array();
    }
320
321
322
323
324

    public static function has_instance_config() {
        return false;
    }

325
    public static function get_instance_config_options($institution, $instance = 0) {
326
327
        return array();
    }
328
}