Search code examples
phpauthenticationpluginsremote-accessmoodle

Prevent local duplicates with the External Database Authentication Plugin on Moodle


I'm using Moodle's external database plugin and am having a very annoying problem. Here's an example:

username: [email protected] logs in with his password and both are saved in an external DB. Moodle grabs that data (username, password) and saves it locally (mysql) with that user's external DB id. Keep in mind the external DB is the main DB where everything happens, Moodle is only for authorized users that need to take tests. So in Moodle, I have:

username: [email protected] password: blabla userid: 1234

It works great, except for when [email protected] decides to update his username/email on the external DB, so he wants it to be [email protected] now. When he tries to log in, it logs in fine, since Moodle checks with the external DB for both username/password. Problem: A new record is created within Moodle for [email protected] with same password AND userid.

My question is: Where can I safely check with the local DB if userid already exists and NOT create a new record? I do not want to update the moodlelib doc because I don't want to have upgrading problems in the future. I can update the External DB plugin, but can't figure out where would be best.

Here's a workaround someone did - https://moodle.org/mod/forum/discuss.php?d=232163 - but it's done in a cron job instead of immediately on Login.

UPDATE: It looks like I'll have to update moodlelib and the external db plugin, I'll post my solution if nobody posts.


Solution

  • Since nobody replied, this is the best I could come up with. I have my own comments with *UPDATED so you know where I updated the code.

    Under \moodle\lib\moodlelib.php (Unfortunately I had to update this file because of an authentication call that would trigger the duplicate creation).

    1) Update function authenticate_user_login(); replace line 4342 - 4382 with:

    $authsenabled = get_enabled_auth_plugins();
    
    // *UPDATED - begin
    $authplugin = get_auth_plugin('DB');
    $userinfo = ($authplugin->get_userinfo($username)) ? $authplugin->get_userinfo($username) : 0;
    // *UPDATED - end    
    
    // *UPDATED - added second elseif
    if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {    
        // Use manual if auth not set.
        $auth = empty($user->auth) ? 'manual' : $user->auth;
        if (!empty($user->suspended)) {
            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
            $failurereason = AUTH_LOGIN_SUSPENDED;
            return false;
        }
        if ($auth=='nologin' or !is_enabled_auth($auth)) {
            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
            // Legacy way to suspend user.
            $failurereason = AUTH_LOGIN_SUSPENDED;
            return false;
        }
        $auths = array($auth);
    
    } 
    elseif ($user = get_complete_user_data('idnumber', $userinfo['idnumber'], $CFG->mnet_localhost_id)) {
        $auth = empty($user->auth) ? 'manual' : $user->auth;  // use manual if auth not set
        if (!empty($user->suspended)) {
            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
            $failurereason = AUTH_LOGIN_SUSPENDED;
            return false;
        }
        if ($auth=='nologin' or !is_enabled_auth($auth)) {
            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
            $failurereason = AUTH_LOGIN_SUSPENDED; // Legacy way to suspend user.
            return false;
        }
        $auths = array($auth);    
    }    
    else {
        // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
        if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
            $failurereason = AUTH_LOGIN_NOUSER;
            return false;
        }
    
        // Do not try to authenticate non-existent accounts when user creation is not disabled.
        if (!empty($CFG->authpreventaccountcreation)) {
            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".$_SERVER['HTTP_USER_AGENT']);
            $failurereason = AUTH_LOGIN_NOUSER;
            return false;
        }
    
        // User does not exist.
        $auths = $authsenabled;
        $user = new stdClass();
        $user->id = 0;
    }
    

    2) Same function, replace line 4408 - 4427 with:

    if ($user->id) {
        // User already exists in database.
        if (empty($user->auth)) {
            // For some reason auth isn't set yet.
            // *UPDATED $DB->set_field('user', 'auth', $auth, array('username'=>$username));
            $DB->set_field('user', 'auth', $auth, array('idnumber'=>$user->idnumber));                
            $user->auth = $auth;
        }
    
        // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
        // the current hash algorithm while we have access to the user's password.
        update_internal_user_password($user, $password);
    
        if ($authplugin->is_synchronised_with_external()) {
            // Update user record from external DB.
            // *UPDATED $user = update_user_record($username);
            $user = $authplugin->update_user_record($username, $user->idnumber);                
        }
    } else {
        // Create account, we verified above that user creation is allowed.
        $user = create_user_record($username, $password, $auth);
    }
    

    Under \moodle\auth\db\auth.php (External DB plugin Lib)

    1) Update function update_user_record() to receive the IDNUMBER parameter:

    function update_user_record($username, $idnumber='', $updatekeys=false) {  
    

    2) Replace line 512 with:

    $user = $DB->get_record('user', array('idnumber'=>$idnumber, 'mnethostid'=>$CFG->mnet_localhost_id)); 
    

    So now, the main function under the moodle lib will check if user exists by username, if not, checks if user ID exists...if so, it will update the user's info with the new info coming from the external DB. It works fine on Moodle 2.4 and 2.6 for me.