Recent Topics

1 Jul 25, 2006 17:39    

We use an integrated authentication system on our Intranet and would like to extend it to b2Evolution (it's PHP based). I have a working instance of this based on the old ldap plugin from the 0.9.2 file set, but I'm upgrading to 1.8 and would like to try to do away with alot of the hacks that the original version of the plugin required. Assuming that I am starting with a clean install of the latest release, what is the best way to go about implimenting such a plugin? (I'm looking for a high-level overview of the process of implimenting it, not a how-to for the plugin itself).

The following features apply:
[list]

  • Users must login to access the blogs

  • The auth system has it's own login page and users should be directed to that to login

  • Once authenticated, b2 should check to see if a user account exists already. If not, it should be created automagically.

  • We don't care about the b2evo user password, and b2evo need not know the users actual password.

  • [/list:u]

    Thanks in advance!

    2 Jul 25, 2006 18:20

    xangelusx wrote:

    The following features apply:

      [1]Users must login to access the blogs [2]The auth system has it's own login page and users should be directed to that to login [3]Once authenticated, b2 should check to see if a user account exists already. If not, it should be created automagically. [4]We don't care about the b2evo user password, and b2evo need not know the users actual password. [/list:u][/quote] Wow, these forums support lists! Never knew that.... Anyway,
        [1]With the BeforeSkinDisplay or whatever event, have if ( ! isset( $current_user ) ) die('log in'); should do the job. [2]Request a BeforeLoginDisplay or something event. And use a similiar way to the above. [3]Isn't this the default behavior? [4]Just in your custom login page, get rid of the password box, or have it hidden with a default password in it, then in a event that gets fired before the user is 'logged in', then do whatever password setting/getting whatever you need to do. [/list:u] Should be enough....

    3 Jul 25, 2006 18:27

    Can you tell me a bit more about the event triggers? Do they go inside the plugin and get registered, then get called from inside the b2evo source?

    Is there a good plugin tutorial anywhere?

    4 Jul 25, 2006 18:31

    Can you tell me a bit more about the event triggers? Do they go inside the plugin and get registered, then get called from inside the b2evo source?

    Is there a good plugin tutorial anywhere?

    I do not think there are any tutes yet, but if you know some OOP then shouldn't be a problem. Take a look at the 'Test Plugin' included in b2evo for the basic jist of things.

    Events are triggered when a event happens.... So if you need one that isn't there, then just request it in this forum. To get a complete list, open up your /blogs/inc/_misc/_plugin.class.php file (off my memory)...

    But how they work is, you stick in your event function inside your plugin, then it will be called automaticly.

    5 Jul 25, 2006 18:46

    1/ to your index.php (or stub.php) add "$login_required = true;"

    2/ to htsrv/login.php add header_redirect( 'real_login_page');

    3/ create a plugin that traps 'login_attempt' (or whatever it is) that checks your apps login table (and creates/updates the relevant [b2evo] user record)

    To see the events that a plugin can "trap" look at inc/_misc/_plugins.class.php ;)

    ¥

    6 Jul 25, 2006 18:50

    Yabba, Your 1 & 2 have nothing to do with plugins.... Which is how this man wants to implement the stuff...

    7 Jul 25, 2006 19:01

    I can live with that ;)

    ¥

    8 Jul 25, 2006 19:17

    balupton wrote:

    But how they work is, you stick in your event function inside your plugin, then it will be called automaticly.

    Great, thanks for that tip. That makes sense. Am I correct in assuming that a plugin has to be activated in order for it's events to be triggered? So for instance the ldap plugin that's included won't try to login as long as it's not active, right?

    Can I do all of this without hacking any of the core files? If not, then first: boo! and secondly I can hack my way through the rest. But if there is a way to do so without hacking, can you clarify these steps for me:

    in _main.inc.php the default login functionality starts on line 387. If I want the user to always login I can just set $login_required = true in my plugin (do I need to use the $_GLOBALS array) so that it is set by the time it is tested for on line 390, yes?

    Since the authentication system has it's own login page (not customizable and separate from the b2evo app) - where/how do I modify the login_action parameter so that we fall into the login code block at line 420 in _main.inc.php?

    Then my plugin just needs to wait for the LoginAttempt event to fire on line 430, do it's thing and then pass control back to _main.inc.php. Correct so far?

    Now that we got past line 430 - do I need to hack the user_pass_ok function so that b2evo ignores the password from the user profile? Or should I set every users b2evo password to some generic string and then spoof that string in the $_GET array? How can I pass the users login ID back to the global $login variable from inside my plugin?

    As I said in the original post - this all works now, it just required hacks to the _main.in.php file and some supporting files. I'm now trying to merge these changes with the new file set and it's a bear. I'd rather have this all done from inside a plugin if possible to ease future upgrades.

    Thanks for the help so far!

    9 Jul 25, 2006 19:19

    Thanks for the ideas Yabba, and thanks for sticking up for me balupton :)

    It sounds like I could maybe do what Yabba listed for steps 1 and 2 from inside the plugin file?

    10 Jul 25, 2006 19:38

    xangelusx wrote:

    Thanks for the ideas Yabba, and thanks for sticking up for me balupton :)

    It sounds like I could maybe do what Yabba listed for steps 1 and 2 from inside the plugin file?

    Maybe for 1... not that sure.

    But for 2 you need that to add the event before the login page is displayed, but there would be no point in doing a header redirect as you could just output your page then do a die instead...

    I'm not sure how correct your statements so far are, as i would need to look into how b2evo handles its login, but to me it goes somethings like;

    1. Loads the core.
    2. Checks if user is logged in.
    3. If user is required to login and is not, then display the login page and die.
    4. Display the skin.

    So what you would do is, find where you can add a event in Part 3, before the login page gets displayed.

    And then you would have something like this in your plugin;

     function BeforeLoginPageDisplay ( & params ) {
    include 'myloginpage.php';
    die;
    }

    Well i think that is right....

    Then you handle step 4 by giving every user the same password when they register their account, do this by in your login/register form, have the password field with display:none; but have the password in it... ( You must have a trustworthy community..... )

    Yeh think thats all....

    11 Jul 26, 2006 15:22

    Woot! I was able to get the plugin to work with NO modifications to the core files!! The plugin arcitecture for b2evo is sweet!!! Thanks!!!!

    Instead of using the LoginAttempt event (which would have required a bit of hacking to get the user_pass_ok() function to work), I used the under-documented AlternateAuthentication event. This event is fired after all normal login attempts fail, so there is nothing to hijack or spoof. Here's the code so far. I still need to modify the Profile form so that I can hide the password boxes (think I need to ask for a new DisplayProfileFormFieldset event), and add some code to disable the register and login forms. I'm sure I'll think of tons more that I can do now that I "get" the event and plugin stuffs. :) Thanks again!

    Edited 2006-07-26 @ 16:42 EST: Updated with latest version of plugin

    <?php
    /**
     * This file implements a custom portal authentication plugin for use with the
     * b2evolution project - {@link http://b2evolution.net/}.
     *
     * Plugin documentation can be found at {@link http://manual.b2evolution.net/Plugins}.
     *
     * Based on the LDAP plugin (c)2003-2006 by Francois PLANQUE - {@link http://fplanque.net/}
     */
    if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
    
    
    /**
     * Always require login
     */
    global $login_required; //in case we're inside a function
    $login_required = true;
    
    /**
     * portal authentication plugin.
     *
     * Handles user login events and creates a user locally if s/he
     * does not exist yet.
     */
    class portal_authentication_plugin extends Plugin
    {
    	var $code = 'evo_portal_auth';
    	var $priority = 50;
    	var $version = '1';
    	var $author = 'Chris Bloom';
    
    
    	/**
    	 * Constructor.
    	 */
    	function portal_authentication_plugin()
    	{
    		$this->name = T_('Intranet portal_authentication authentication');
    		$this->short_desc = T_('Creates users if they can be authenticated through portal authentication.');
    		#$this->long_desc = T_('');
    
    		require_once('path/to/portal/auth/class.php');
    		$this->auth = new AuthPortalLocal();
    	}
    
    
    	function GetDefaultSettings()
    	{
    		global $Settings;
    
    		return array(
    			'fallback_grp_ID' => array(
    				'label' => T_('Default group'),
    				'type' => 'select_group',
    				'note' => T_('The group to use as fallback, if we do not want to create a new group. "None" to not a create a new user in that case.' ),
    				'allow_none' => true,
    				'defaultvalue' => $Settings->get('newusers_grp_ID'),
    			),
    		);
    	}
    
    
    	/**
    	 * Event handler: allow alternate authentication through Plugin
    	 *
    	 * This function will check if the user exists and create it locally if it does
    	 * not exist yet. The plugin should attach the user to $Session
    	 *
    	 * @param NULL
    	 */
    	function AlternateAuthentication ( $params )
    	{
    		global $localtimenow, $login_required;
    		global $UserCache, $GroupCache, $Settings, $Hit, $Session;
    
    
    		if ( ! $login_required )
    		{
    			$this->debug_log( 'Login not required.' );
    			return false;
    		}
    
    
    		/**
    		 * Create instance of portal_authentication
    		 */
    		$this->auth->require_login();
    		$login = $this->auth->get_user_id();
    
    
    		/**
    		 * Test for user existance or create new account
    		 */
    		if( $local_User = & $UserCache->get_by_login( $login ) )
    		{ // User exist, do nothing
    			$this->debug_log( 'User already exists locally.' );
    		}
    		else
    		{ // create this user locally
    			$NewUser = new User();
    			$NewUser->set( 'login', $login );
    			$NewUser->set( 'nickname', $login );
    			$NewUser->set( 'idmode', 'login' );
    
    			//note that password doesn't matter because we are using portal_authentication to authenticate
    			$NewUser->set( 'pass', md5( uniqid( time() ) ) );
    			
    			$NewUser->set( 'validated', 1 ); // assume the user has been validated
    
    			$NewUser->set( 'firstname', '' );
    			$NewUser->set( 'lastname', '' );
    			$NewUser->set( 'email', '' );
    			$NewUser->set( 'locale', locale_from_httpaccept() ); // use the browser's locale
    			$NewUser->set( 'url', '' );
    			$NewUser->set( 'icq', 0 );
    			$NewUser->set( 'aim', '' );
    			$NewUser->set( 'msn', '' );
    			$NewUser->set( 'yim', '' );
    
    			$NewUser->set( 'ip', $Hit->IP );
    			$NewUser->set( 'domain', $Hit->get_remote_host( true ) );
    			$NewUser->set( 'browser', $Hit->user_agent );
    			$NewUser->set_datecreated( $localtimenow );
    			$NewUser->set( 'level', 1 );
    			$NewUser->set( 'notify', 0 );
    			$NewUser->set( 'showonline', 1 );
    
    			$assigned_group = false;
    			$users_Group = NULL;
    			$fallback_grp_ID = $this->Settings->get( 'fallback_grp_ID' );
    			if( empty($fallback_grp_ID) )
    			{
    				$this->debug_log( 'No default/fallback group given.' );
    			}
    			else
    			{
    				$users_Group = & $GroupCache->get_by_ID($fallback_grp_ID);
    				if( $users_Group )
    				{ // either $this->default_group_name is not given or wrong
    					$NewUser->setGroup( $users_Group );
    					$assigned_group = true;
    
    					$this->debug_log( 'Using default/fallback group ('.$users_Group->get('name').').' );
    				}
    				else
    				{
    					$this->debug_log( 'Default/fallback group not existing ('.$fallback_grp_ID.').' );
    				}
    			}
    
    			if( $assigned_group )
    			{
    				$NewUser->dbinsert();
    				$UserCache->add( $NewUser );
    
    				$this->debug_log( "Created user $login." );
    			}
    			else
    			{
    				$this->debug_log( "There was a problem trying to CREATE the blogger account $login. User is NOT logged in." );
    				bad_request_die( 'There was a problem trying to create your blogger account. Please contact the <a href="/webteam/contact/">Web Team</a> for assistance.' );
    			}
    
    			if( ! $local_User = & $UserCache->get_by_login( $login ) )
    			{
    				$this->debug_log( "There was a problem trying to RETRIEVE the blogger account $login. User is NOT logged in." );
    				bad_request_die( 'There was a problem trying to retrieve your new blogger account. Please contact the <a href="/webteam/contact/">Web Team</a> for assistance.' );
    			}
    		}
    
    		$this->debug_log( "User successfully logged in with username $login" );
    
    		// save the user for later hits
    		$Session->set_User( $local_User );
    
    		return true;
    	}
    
    		
    	/**
    	 * Event handler: called at the end of the login procedure for anonymous visitors.
    	 *
    	 * This function will kill any access attempts by anonymous users when login is required
    	 *
    	 * @param array EMPTY
    	 */
    	function AfterLoginAnonymousUser ( $params )
    	{
    		global $login_required;
    
    		if ( $login_required )
    		{
    			$this->debug_log( "Anonymous access denied. User is NOT logged in." );
    			bad_request_die( T_('Anonymous access denied. You must login to access these blogs.') );
    		}
    	}
    
    		
    	/**
    	 * Event handler: called at the end of the login procedure for registered users.
    	 *
    	 * This function will enforce any rules post login.
    	 *
    	 * @param array EMPTY
    	 */
    	function AfterLoginRegisteredUser ( $params )
    	{
    		global $current_User;
    		global $baseurl, $baseurlroot, $htsrv_url;
    		global $ReqURI, $Messages, $Session;
    
    		$_htsrv_url = strtolower( str_replace( $baseurlroot, '', $htsrv_url ) ); //should result in something like /blogs/htsrv/
    
    		//prevent access to restricted pages
    		switch ( strtolower( $ReqURI ) )
    		{
    			case $_htsrv_url.'login.php':
    				if ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] == 'logout' )
    				{
    					//substitute this custom logout function that takes into account the portal_authentication logout as well
    					global $model_path;
    					require_once $model_path.'users/_user.funcs.php';
    
    					logout();
    
    					//calling $this->auth->logout() directly would cause us to redirect right back to login.php?action=logout
    					//resulting in an endless loop. This--v isn't 100% future-proof, but it will have to do until the portal_authentication 
    					//logout function is modified to accept a target URL as an argument. 
    					$redirect_to = '/portal/logout.php?url='.urlencode($baseurl);
    
    					header_nocache();
    					header_redirect($redirect_to);
    					exit;
    				}
    				else
    				{
    					//no other login actions are allowed
    					//fall through to rest of swith block
    				}
    			case $_htsrv_url.'register.php':
    			case $_htsrv_url.'validate.php':
    				global $Messages;
    				
    				$Messages->add( T_('The registration, validation and login forms are disabled.'), 'error' );
    				$Session->set( 'Messages', $Messages );
    				$Session->dbsave(); // If we don't save now, we run the risk that the redirect goes faster than the PHP script shutdown.
    				
    				header_nocache();
    				header_redirect($baseurl);
    				exit;
    		}
    
    		if ( 
    			( 
    				! $current_User->email 
    				|| 
    				! strlen( $current_User->email ) 
    			) 
    			&& 
    			( 
    				strtolower( $ReqURI ) != strtolower( $_htsrv_url.'profile_update.php' ) 
    				&&
    				(
    					! isset( $_REQUEST['disp'] ) 
    					|| 
    					$_REQUEST['disp'] != 'profile' 
    				) 
    			)
    		)
    		{
    			//force new users to enter e-mail address
    			$location = $baseurl . '?blog=1&disp=profile&redirect_to=' . rawurlencode( $ReqURI ); //we need to include a blog param for this to work. Why???
    
    			//note that the profile form is part of each skin and is not garanteed to display messages.
    			$Messages->add( T_('Please enter your e-mail address.'), 'note' );
    			$Session->set( 'Messages', $Messages );
    			$Session->dbsave(); // If we don't save now, we run the risk that the redirect goes faster than the PHP script shutdown.
    
    			header_nocache();
    			header_redirect($location);
    		}
    	}
    }
    
    
    /*
     nolog */
    ?>

    12 Jul 26, 2006 17:09

    Ok cool. Plugins are really nice when you get into them :)

    But wouldn't the way you've done it, mean that the first login attempt always fails?
    And you haven't replaced the login form?

    Or are you going to do that later.

    13 Jul 26, 2006 18:10

    For 1 & 2 (without hacks) you'd probably want to look into the SessionLoaded event ;)

    ¥

    14 Jul 26, 2006 22:17

    Our portal authentication system has it's own login form that is presented whenever a login is required (it interrupts the original request, drops it's own login form inline, submits to itself, verifies the user against our active directory tree, all the while passing along any of the original GET/POST variables).

    The user will never see the default login form, at least not unless they specifically type in the URL; I'm working on a fix for that now. I'll post that code once it's working.

    If you follow the login logic within _main.inc.php (starting on line 387) you will see that since there is no login or password information the default login code is never processed and execution falls to the else block on line 472. There the AlternateAuthentication event is triggered and off we go to my plugin. So there's no place where a login would fail before getting to the plugin, and in order to get past my plug they have to login through the portal, and in order to do that they have to be a valid network user...

    15 Jul 26, 2006 22:18

    PS: My code changed quite a bit since I posted it, so I'm editing it to include the new bits and all of the updates. This is all without any changes to the original source files.

    16 Jul 26, 2006 22:53

    ¥åßßå wrote:

    For 1 & 2 (without hacks) you'd probably want to look into the SessionLoaded event ;)

    ¥

    I started using the SessionLoaded event, but that is triggered really early, before the user is loaded into the session and before all of the include files are included. I chose to go with the AfterLoginRegisteredUser event instead since it would always be fired and everything would be loaded by the time we got there. A bit more overhead, especially in the case where I'm kicking them off the page anyways, but less hoops to jump through overall.

    I also had a version that used the DisplayLoginFormFieldset, but since that only fires after the default fields have been printed to screen I had to rely on using javascript to hide the form, and it wasn't very effective is javascript was turned off. Here's that version:

    	/**
    	 * Event handler: called when displaying the "Login" form.
    	 *
    	 * This function will disable the login screen since it is not required
    	 * while using portal authentication
    	 *
    	 * @param array 'Form' => Form Object
    	 */
    	function cool_but_not_very_effective_when_js_is_disabled_version_of_DisplayLoginFormFieldset ( $params )
    	{
    		/**
    		 * Note that the DisplayLoginFormFieldset event is called after the
    		 * default fields have been displayed. It is intended to be used to
    		 * add custom fields, not remove or change existing ones. Therefor, in
    		 * order to disable the login form we will do the following:
    		 *   1. Add javascript to remove the form from the DOM
    		 *   2. Add javascript that will hide the form using CSS (in case the DOM methods aren't avail)
    		 *   3. Add text to explain why the form is disabled
    		 */
    
    		//return; //while debugging
    		//print "<pre>" . htmlspecialchars( print_r( $params, true ) ) . "</pre>";
    
    		global $baseurl, $rsc_url;
    		$_baseurl = htmlspecialchars( addslashes( $baseurl ) );
    		$_rsc_url = htmlspecialchars( $rsc_url );
    
    		print <<<EOT
    <script type="text/javascript" src="{$_rsc_url}js/functions.js">
    	<!-- 
    	/**
    	 * For the addEvent function.
    	 *
    	 * This resource file is not currently included by the original login file. If future versions
    	 * include it then we will need to remove it from being included here
    	 */
    	//-->
    </script>
    <script type="text/javascript" src="{$_rsc_url}js/form_extensions.js">
    	<!-- 
    	/**
    	 * For the get_form function.
    	 *
    	 * This resource file is not currently included by the original login file. If future versions
    	 * include it then we will need to remove it from being included here
    	 */
    	//-->
    </script>
    <script type="text/javascript">
    	<!-- 
    	if( typeof addEvent == "function" )
    	{
    		addEvent( window, 'load', DisplayLoginFormFieldset, false );
    	}
    	else
    	{
    		alert( 'Please do not use this form to login.');
    	}
    	
    	function DisplayLoginFormFieldset ( )
    	{
    		var oLogin = document.getElementById ( 'login' );
    		if ( oLogin )
    		{
    			oForm = get_form ( oLogin );
    			if ( ! oForm ) 
    			{
    				alert( 'Please do not use this form to login.');
    				return false;
    			}
    			else if ( oForm.parentNode )
    			{
    				oP = document.createElement( "P" );
    				oP.innerHTML = "Please do not use this form to login.";
    
    				oForm.parentNode.replaceChild( oP, oForm );
    				return true;
    			}
    			else
    			{
    				if ( oForm.style.display ) oForm.style.display = 'none';
    				else if ( oForm.display ) oForm.display = 'none';
    				else if ( oForm.style.visibility ) oForm.style.visibility = 'hidden';
    				else if ( oForm.visibility ) oForm.visibility = 'hidden';
    				else window.location.href = '{$_baseurl}';
    				return true;
    			}
    		}
    		return false;
    	}
    
    
    	if( typeof self.init_dynamicSelect == "undefined" )
    	{
    		function init_dynamicSelect ( )
    		{
    			//this function is added by the Form object, but isn't always defined
    			//so this is a fake instance to avoid js console errors
    		}
    	}
    	//-->
    </script>
    <noscript>
    	<p><strong>Please do not use this form to login</strong></p>
    </noscript>
    EOT;
    	}
    

    17 Jul 30, 2006 08:14

    Using the session loaded to detect if the user had just requested the login page (and redirecting if they have) means that you make the core do as little work as possible.

    Likewise using it to detect if you're on a page that needs $login_required set to true.

    ¥


    Form is loading...