Prevent local duplicates with the External Database Authentication Plugin on Moodle - php

I'm using Moodle's external database plugin and am having a very annoying problem. Here's an example:
username: johndoe#gmail.com 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: johndoe#gmail.com
password: blabla
userid: 1234
It works great, except for when johndoe#gmail.com decides to update his username/email on the external DB, so he wants it to be johndoe#hotmail.com 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 johndoe#hotmail.com 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.

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.

This is quite an old answer, but there's an alternative that requires no manual patching.
The OP uses email addresses (instead of arbitrary usernames) to identify users. This behaviour can be maintained by setting Site administration > Plugins > Authentication > Manage authentication > Allow log in via email (authloginviaemail) to true. Now the username can be populated by the (stable) external database ID. Changing email addresses hence won't result in new accounts.

Related

Is there a action/filter hook to check if the user's account is verified in Wordpress?

I've created a simple algorithm to add a user to our site's API after it registers his or her info.
function send_reg_userdata_to_api($user_id) {
$reg_data = get_userdata($user_id);
//check if $_POST['pass1'] exist, set $_POST['pass1'] to password, else set it to ******** (default password for social login)
if (isset($_POST['pass1'])) {
$password = $_POST['pass1'];
} else {
$password = '********';
}
$user_data = array(
'user_name' => $reg_data->display_name,
'password' => $password
);
//**PROBLEM**
//Want to trigger this apply_filters() when user's account will verified
//apply_filters('wp_my_api', 'post', $user_data);
//instead I store it to a variable to pass its data when a HOOK that i needed will triggered
$reg_user_data = $user_data;
}
add_action('user_register', 'send_reg_userdata_to_api');
I checked all at the WordPress Codex and also here about the solution about the problem but still have no luck on it. I'm using Theme My Login for register, login and verifying user's info and Social Socializer to connect users to the site using social accounts.
Any help, ideas and suggestions is heavenly appreciated. Thanks in Advance.

Override the login Controller in FOSUserBundle (Symfony2)

I'm currently migrating from a WordPress to a Symfony2 website.
I imported all my WP user on Symfony2 but now I'm looking for a way to make additional checks when the user tries to log in (typically check if the user was imported from WP and check his old password).
What's the best way to add some checks on the User authentication ? (login_check on fosuserbundle).
I simply try to override the SecurityController, but it doesn't work as the login doesn't seem to be made here.
Thanks for your help.
Edit: I need to add my check during the login process, not after. During the login, if the user comes from WordPress, I want to check if the password he provides is the same as his old WordPress password (that is stored in the DB too).
I finaly found a solution, but not sure it's the best way to do the stuff.
I added a listener when the login failed and check if it's a user from WordPress.
Now I'm looking for a solution to handle the "remember me" checkbox because the user is a authenticate programmatically. Here is the code :
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$username = $request->request->get('_username');
$password = $request->request->get('_password');
$user = $this->doctrine->getRepository('AppBundle:User')->findOneByUsername($username);
if ($user instanceof User && $user->getFromWordpress() == true) {
//The class use by WordPress to check / encode passwords
$hasher = new PasswordHash(8, TRUE);
//User provide the right password
if ($hasher->CheckPassword($password, $user->getWordpressPassword())){
//Programmatically authenticate the user
$token = new UsernamePasswordToken($user, $user->getPassword(), "main", $user->getRoles());
$this->tokenStorage->setToken($token);
$event = new InteractiveLoginEvent($request, $token);
$this->eventDispacher->dispatch("security.interactive_login", $event);
//Set the password with the Symfony2 encoder
$encoder = $this->encoderFactory->getEncoder($user);
$password = $encoder->encodePassword($password, $user->getSalt());
$user->setPassword($password);
$user->setFromWordpress(false);
$this->doctrine->getManager()->persist($user);
$this->doctrine->getManager()->flush();
//Finnaly send login ok response
return $this->onAuthenticationSuccess($request, $token);
}
}
//Login failed code ...
//.....
}

fatfree user auth global variable

Iam auth my user with the build in function. But is fatfree not setting the username global? Cant find it
$angemeldet = new \DB\SQL\Mapper($this->db, 'users');
$auth = new \Auth($angemeldet, array('id'=>'name', 'pw'=>'email'));
$auth->basic(); // a network login prompt will display to authenticate the user
This is working.
http://www.willis-owen.co.uk/2013/02/blog-tutorial-with-fat-free-framework-v3/
Here i found that he is setting it in the SESSION. But if i try to access it, its completly empty. Where can i find the username?
And how can i manipulate the password? So i can compare a hash?
The F3 Auth will not set the user to the SESSION by default, you would have to do that yourself. You can't store PDO objects in the SESSION because they can't be serialized. The username will be stored in the SERVER.PHP_AUTH_USER, so you can use that to fetch the $user.
To hash the password, you pass a function to the $auth->basic( function( $password ){} ); and return the hashed password. Using SHA256 with a salt will be much more secure than MD5.
This is the code I am using to do BASIC auth.
// I call this from the constructor of the handler of secure pages.
public static function authenticate(){
// map to the users db table
$user = new \DB\SQL\Mapper( F3::get( 'db' ), 'users' );
// tell F3 to use the columns username and password
$auth = new \Auth( $user, array( 'id'=>'username', 'pw'=>'password' ) );
// check to see if they are authed, use a function to create hashed password
$authenticated = $auth->basic( function ( $password ){
$salt = 'Some secure salt string...';
return hash( 'sha256', $password . $salt );
} );
// if we are logged in and the user isn't set....
if ( $authenticated && !F3::exists( 'user' ) ){
// use SERVER.PHP_AUTH_USER to load user by username
$user->load( array( 'username=?', F3::get( 'SERVER.PHP_AUTH_USER' ) ) );
// set user to variable if it could be loaded
if ( !$user->dry() ){
F3::set( 'user', $user );
}
}
}
Look at the code below:
public function verify($f3, $params) {
$username = $f3->get('POST.email');
$password = $f3->get('POST.password');
$hashedPassword = md5($password);
$submit = $f3->get('POST.submit');
if (isset($submit)) {
global $db;
$user = new DB\SQL\Mapper($db, 'users');
// username is email, password is password in our case
$auth = new \Auth($user, array('id' => 'email', 'pw' => 'password'));
$loginResult = $auth->login($username, $hashedPassword); // Cross-check with users with hashedPassword
// Authenticated the user successfully
if ($loginResult == true) {
$f3->set('message', 'Logged in successfully.');
$f3->reroute('/login');
}
} else {
$f3->set('message', 'Please enter valid username/password');
}
$f3->reroute('/login');
}
In the above code you can see that I have used md5 hash in the password. This might give you idea on what you are trying to achieve.
check the API docs for the basic auth method: http://fatfreeframework.com/auth#basic
i think it should be something like
$auth->basic(function($input){
return md5($input);
});
unfortunately this method does not return any user data on a valid login. but since objects are passed by reference in php, you could try to have a look at the mapper after the auth process, if some data was populated. var_dump($angemeldet->cast());
This is the easiest way to do basic (browser popup) auth, from my experience:
function renderAdmin() {
$user=new DB\SQL\Mapper($this->db,'users');
$auth== null;
$auth = new \Auth($user, array('id'=>'username', 'pw'=>'password'));
$login_result = $auth->basic();
if ($login_result) $f3->set('SESSION.username','SERVER.PHP_AUTH_USER');
if (!$f3->get('SESSION.username')) $f3->error(401);
else {
// all your code for the admin section
echo \Template::instance()->render('admin.html');
}
}

Overview of integrating OAuth 2.0 with a Custom API (written in Laravel)

I created an Android App that communicates with an API I set up using PHP and am going to attempt to rewrite the PHP stuff using Laravel. One of the big changes I want to make is to allow OAuth login into my app that is associated with a user account on my local servers and I am confused about the application flow involved to do this.
Should I do my OAuth authentication on my Android app before registering, then store some sort of credential along with user information when I call my API to create my local user account?
Also, when logging a user in using OAuth, how do I know that the person who is authenticated by the provider, say Facebook, Is associated with a local user account on my servers? Is there some sort of token I can store?
Thanks for any overview information you can provide.
I did the same (without OAuth provider from my site) with HybridAuth There is also a composer package at packagist.org.
I have two tables for that:
users with columns id, email, password (table with my normal users and user infos)
authentications with columns: id, user_id, provider, provider_uid (authentication table with the stuff for OAuth.)
The Code I have in my AuthController (for the route /login/social Route::controller('login','AuthController');)
<?php
class AuthController extends BaseController {
//#see http://www.mrcasual.com/on/coding/laravel4-package-management-with-composer/
public function getSocial($action = '')
{
// check URL segment
if ($action == 'auth') {
// process authentication
try {
Hybrid_Endpoint::process();
}
catch (Exception $e) {
// redirect back to http://URL/social/
return Redirect::to('login/social');
}
return;
}
try {
// create a HybridAuth object
$socialAuth = new Hybrid_Auth(app_path() . '/config/hybridauth.php');
// authenticate with provider
if($action != ''){
// only allow facebook and google
if(in_array($action,array('google','facebook'))){
$provider = $socialAuth->authenticate($action);
}else{
// catch this form_error in the login form
return Redirect::to('login')->withErrors(array('form_errors'=>'The url was invalid'));
}
}else{
return Redirect::to('login');
}
// fetch user profile
$userProfile = $provider->getUserProfile();
}
catch(Exception $e) {
// exception codes can be found on HybBridAuth's web site
return Redirect::to('login')->withErrors(array('form_errors'=>'Error on Login: #'.$e->getCode().': '.$e->getMessage()));
}
/*access user profile data
echo "Connected with: <b>{$provider->id}</b><br />";
echo "As: <b>{$userProfile->displayName}</b><br />";
echo "<pre>" . print_r( $userProfile, true ) . "</pre><br />";
die();*/
//check if User exists
if($user_id = DB::table('authentications')->where('provider', $provider->id)->where('provider_uid',$userProfile->identifier)->pluck('user_id')){
//login user
Auth::loginUsingId($user_id);
//update user details (eg photo, name, etc)
//Here you can update the user details
return Redirect::to('dashboard');
}else{
//lets see if we already know this email -> connect it with the registered account
if($user = User::where('email',$userProfile->email)->first()){
$user->authentications()->save(new Authentication(array('provider'=>$provider->id, 'provider_uid'=>$userProfile->identifier)));
Auth::login($user);
//here you can update the user details
return Redirect::to('dashboard')->with('user_social_linked',$provider->id);
}else{
//register user
$user = $this->createUser(array('email'=>$userProfile->email,'password'=>''));
$user->authentications()->save(new Authentication(array('provider'=>$provider->id, 'provider_uid'=>$userProfile->identifier)));
Auth::login($user);
//here you can set/update the user details
return Redirect::to('dashboard')->with('user_created',true);
}
}
}
private function createUser($credentials)
{
$user = new User();
$user->email = $credentials['email'];
$user->password = $credentials['password'];
$user->save();
Auth::login($user);
return $user;
}
}
The User model has also the following function
<?php
public function setPasswordAttribute($password){
//disable blank passwords => disables account login (eg login only with third party aka social providers)
if($password != ''){
$this->attributes['password'] = Hash::make($password);
}else{
$this->attributes['password'] = $password;
}
}
Now you can treat your own OAuth provider just as any other OAuth provider, eg by adding / configuring a hybridauth provider.
To your questions:
Should I do my OAuth authentication on my Android app before registering, then store some sort of credential along with user information when I call my API to create my local user account?
I would handle it the same as with any other OAuth provider: like in the example above one function to call (I only use login via the Laravel Auth, so I do not have the issues with expiring provider sessions and the user does not have to create another account).
Also, when logging a user in using OAuth, how do I know that the person who is authenticated by the provider, say Facebook, Is associated with a local user account on my servers? Is there some sort of token I can store?
This is done by the user_id column in the authentications table.
Hope that helps.

User authentication with SecurityServiceProvider via POST

What I'm looking to do is
Authenticate users on bar.com and
Post their credentials to foo.com/login and re-authenticate them without needing to log in again.
Currently, to GET secure pages on foo.com I'm using form-based access via the SecurityServiceProvider and a db-backed UserProvider to authenticate. Works great: any attempt to load a secured route is intercepted by the firewall and then redirected after successful authentication.
What I can't figure out is how to pass the POST variables (username and password) on to the provider instance and forward the user to the supplied route.
Stub POST route:
$app->post('/login', function(Request $req) use ($app) {
$route = $req->request->filter('route');
$username = $req->get('username');
$password = $req->get('password');
/* magic happens...? */
});
Here is an example of using the user provider to load a user check the password matches then setting the token in the security service. So if you put this code into a route you can get access to the Request for your username and password.
$userProvider = $app['security.user_provider.default'];
$user = null;
try {
$user = $userProvider->loadUserByUsername($username);
} catch (UsernameNotFoundException $e)
{
;
}
$encoder = $app['security.encoder_factory']->getEncoder($user);
// compute the encoded password
$encodedPassword = $encoder->encodePassword($password, $user->getSalt());
// compare passwords
if ($user->password == $encodedPassword)
{
// set security token into security
$token = new UsernamePasswordToken($user, $password, 'yourProviderKeyHere', array('ROLE_USER'));
$app['security']->setToken($token);
// redirect or give response here
} else {
// error feedback
}

Categories