Custom Login Handlers (*) > Invisionize.eu - IP.Board (IPB) News
Kanał Rss Kanał Rss
Kanał Atom Kanał Atom

Custom Login Handlers (*)

wersja drukowalna wersja Microsoft Word wersja HTML

IPS Community Suite comes with a number of different methods to allow users to log in to the community, called "login handlers". Generally speaking, there are two types of login handlers:

 

Getting Started

Both types are implemented by creating a PHP file in the system/Login folder containing class that extends IPSLoginLoginAbstract and inserting a record into the core_login_handlers database table. If you are creating a 3rd party login handler for distribution, you will need to create a plugin to insert that record, and distribute it with your login class.

When inserting the record into core_login_handlers, set login_key to the name of the class without the namespace (for example, if your class is IPSLoginExample, set login_key to "Example").

Note that your PHP class will be prefixed with an underscore. This is a technicality in how code hooks are facilitated in the IPS Community Suite.

 

Standard Login Handlers

Here is a basic skeleton for a standard login handler:

namespace IPSLogin;

class _Example extends LoginAbstract
{
	/**
	 * @brief	Authentication types
	 */
	public $authTypes = IPSLogin::AUTH_TYPE_USERNAME;
		
	/**
	 * Authenticate
	 *
	 * @param	array	$values	Values from from
	 * @return	IPSMember
	 * @throws	IPSLoginException
	 */
	public function authenticate( $values )
	{
		/* Init */
		$username	= $values['auth'];	// Depending on the value of $authTypes this may be an email instead
		$password	= $values['password'];
		
		/* Find member */
		try
		{
			$member = IPSMember::load( $username );
		}
		catch ( OutOfRangeException $e )
		{
			throw new IPSLoginException( IPSMember::loggedIn()->language()->addToStack('login_err_no_account', FALSE, array( 'sprintf' => array( IPSMember::loggedIn()->language()->addToStack('username') ) ) ), IPSLoginException::NO_ACCOUNT );
		}
		
		/* Check password */
		if ( $password !== 'the-correct-password' ) // Implement correct check here
		{
			throw new IPSLoginException( 'login_err_bad_password', IPSLoginException::BAD_PASSWORD, NULL, $member );
		}
		
		/* Return member */
		return $member;
	}

    /**
     * ACP Settings Form
     *
     * @param    string    $url    URL to redirect user to after successful submission
     * @return    array    List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
     * @code
         return array( 'savekey'    => new IPSHelpersForm[Type]( ... ), ... );
     * @endcode
     */
    public function acpForm()
    {
        return array();
    }
    
     /**
     * Can a member change their email/password with this login handler?
     *
     * @param    string        $type    'username' or 'email' or 'password'
     * @param    IPSMember    $member    The member
     * @return    bool
     */
    public function canChange( $type, IPSMember $member )
    {
        return TRUE;
    }
}

The $authTypes property defines whether your login handler expects a username or email address or either. It is a bitwise field, and the acceptable values are:

public $authTypes = IPSLogin::AUTH_TYPE_USERNAME;									// Username
public $authTypes = IPSLogin::AUTH_TYPE_EMAIL;									// Email address
public $authTypes = IPSLogin::AUTH_TYPE_USERNAME + IPSLogin::AUTH_TYPE_EMAIL;	// Username or email address

If you want to base this off a setting, or do any other setup for your login handler, you can implement an init() method.

The authenticate() function receives the values from the form (the username/email address and password) and can either return an IPSMember object if the login was successful, or throw an IPSLoginException object if it wasn't. If throwing an IPSLoginException object, the message is displayed to the user, and the code should be one of the following values:

throw new IPSLoginException( "Login Failed.", IPSLoginException::INTERNAL_ERROR ); // Something went wrong with the login handler which wasn't the user's fault.
throw new IPSLoginException( "Login Failed.", IPSLoginException::BAD_PASSWORD ); // The password the user provided was incorrect.
throw new IPSLoginException( "Login Failed.", IPSLoginException::NO_ACCOUNT ); // The username or email address the user provided did not match any account.
throw new IPSLoginException( "Login Failed.", IPSLoginException::MERGE_SOCIAL_ACCOUNT ); // The username or email address matches an existing account but which has not been used by this login handler before and an account merge is required (see below)

If your login handler needs to create an account for a user, and it is appropriate to do that, you can do that in the authenticate() method. For example:

    public function authenticate( $values )
	{
		/* Init */
		$username	= $values['auth'];	// Depending on the value of $authTypes this may be an email instead
		$password	= $values['password'];
		
		/* Find member */
		try
		{
			$member = IPSMember::load( $username );
		}
		catch ( OutOfRangeException $e )
		{
			$member = new IPSMember;
			$member->member_group_id = IPSSettings::i()->member_group;
			$member->name = $username;
			$member->email = '...'; // You'll need to get the email from your login handler's database
			// You may want to set additional properties here
			$member->save();
		}
		
		/* Check password */
		if ( $password !== 'the-correct-password' ) // Implement correct check here
		{
			throw new IPSLoginException( 'login_err_bad_password', IPSLoginException::BAD_PASSWORD, NULL, $member );
		}
		
		/* Return member */
		return $member;
	}

The acpForm() and canChange() methods are discussed below.

 

Other Login Handlers

Here is a basic skeleton for an OAuth-based login handler:

namespace IPSLogin;

class _Example extends LoginAbstract
{
	/** 
	 * @brief	Icon
	 */
	public static $icon = 'lock';
	
	/**
	 * Get Form
	 *
	 * @param	IPSHttpUrl	$url	The URL for the login page
	 * @param	bool			$ucp	If this is being done from the User CP
	 * @return	string
	 */
	public function loginForm( $url, $ucp=FALSE )
	{
		$redirectUrl = IPSHttpUrl::internal( 'applications/core/interface/example/auth.php', 'none' );
		$oauthUrl = IPSHttpUrl::external( "https://www.example.com/oauth" )->setQueryString( array(
			'client_id'		=> 'xxx',
			'redirect_uri'	=> (string) $redirectUrl
		) );
		
		return "<a href='{$oauthUrl}'>Login</a>";
	}
	
	/**
	 * Authenticate
	 *
	 * @param	string			$url	The URL for the login page
	 * @param	IPSMember		$member	If we want to integrate this login method with an existing member, provide the member object
	 * @return	IPSMember
	 * @throws	IPSLoginException
	 */
	public function authenticate( $url, $member=NULL )
	{
		/* Get user details from service */
		$userData = IPSHttpUrl::external( "https://www.example.com/userData" )->setQueryString( 'token', IPSRequest::i()->token )->request()->get()->decodeJson();
		
		/* Get or create member */
		if ( $member === NULL )
		{			
			/* Try to find member */
			$member = IPSMember::load( $userData['id'], 'my_custom_id' );
			
			/* If we don't have one, create one */
			if ( !$member->member_id )
			{
				/* If a member already exists with this email, prompt them to merge */
				$existingEmail = IPSMember::load( $userData['email'], 'email' );
				if ( $existingEmail->member_id )
				{
					$exception = new IPSLoginException( 'generic_error', IPSLoginException::MERGE_SOCIAL_ACCOUNT );
					$exception->handler = 'Example';
					$exception->member = $existingEmail;
					$exception->details = IPSRequest::i()->token;
					throw $exception;
				}
				
				/* Create member */
				$member = new IPSMember;
				$member->member_group_id = IPSSettings::i()->member_group;
				
				/* Is a user doesn't exist with this username, set it (if it does, the user will automatically be prompted) */
				$existingUsername = IPSMember::load( $userData['name'], 'name' );
				if ( !$existingUsername->member_id )
				{
					$member->name = $userData['name'];
				}
				
				/* Set validating if necessary */
				if ( IPSSettings::i()->reg_auth_type == 'admin' or IPSSettings::i()->reg_auth_type == 'admin_user' )
				{
					$member->members_bitoptions['validating'] = TRUE;
				}
			}
		}
					
		/* Set service ID */
		$member->my_custom_id = $userData['id'];
		$member->save();
				
		/* Return */
		return $member;
	}
	
	/**
	 * Link Account
	 *
	 * @param	IPSMember	$member		The member
	 * @param	mixed		$details	Details as they were passed to the exception thrown in authenticate()
	 * @return	void
	 */
	public static function link( IPSMember $member, $details )
	{
		$userData = IPSHttpUrl::external( "https://www.example.com/userData" )->setQueryString( 'token', $details )->request()->get()->decodeJson();
		$member->my_custom_id = $userData['id'];
		$member->save();
	}
	
    /**
     * ACP Settings Form
     *
     * @param    string    $url    URL to redirect user to after successful submission
     * @return    array    List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
     * @code
         return array( 'savekey'    => new IPSHelpersForm[Type]( ... ), ... );
     * @endcode
     */
    public function acpForm()
    {
        return array();
    }
    
	/**
	 * Can a member change their email/password with this login handler?
	 *
	 * @param	string		$type	'email' or 'password'
	 * @param	IPSMember	$member	The member
	 * @return	bool
	 */
	public function canChange( $type, IPSMember $member )
	{
		return FALSE;
	}
}

The $icon parameter should be the name of a FontAwesome icon which is used on some login screens.

The loginForm() method is used to display the HTML you need for the form. For an OAuth-based handler, this will usually just return the appropriate login button. You can alternatively return an IPSHelpersForm object.

The authenticate() method is where the bulk of your login code will go. If your loginForm() method returns an IPSHelpersForm object it will be passed an array of values from that form (just like standard login handlers). If your loginForm() method returns raw HTML, it is your responsibility to ultimately redirect the user back to the same URL that was passed as $url to loginForm with the "loginProcess" set to the key for your login handler. Most OAuth providers do this with a gateway script in the interface directory.

Your authenticate() method needs to return an IPSMember object or throw an IPSLoginException object, just as described above for standard login handlers.

The acpForm(), link() and changeSettings() methods are described below.

 

Creating settings for your login handler

You will likely need to create settings for your login handler so when an admin sets it up they can provide keys, etc. There are two methods to assist with this: 

acpForm() can return an array of form fields allowing you to specify these settings, and testSettings() allows you to check the settings are correct. For example, to define a client ID setting you might do something like this:

	/**
	 * ACP Settings Form
	 *
	 * @param	string	$url	URL to redirect user to after successful submission
	 * @return	array	List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
	 * @code
	 	return array( 'savekey'	=> new IPSHelpersForm[Type]( ... ), ... );
	 * @endcode
	 */
	public function acpForm()
	{
		return array(
			'example_client_id'	=> new IPSHelpersFormText( 'example_client_id', ( isset( $this->settings['example_client_id'] ) ) ? $this->settings['example_client_id'] : '', TRUE )
		);
	}
	
	/**
	 * Test Settings
	 *
	 * @return	bool
	 * @throws	InvalidArgumentException
	 */
	public function testSettings()
	{
		if ( $this->settings['example_client_id'] == 'invalid id' )
		{
			throw new InvalidArgumentException("The Client ID isn't correct.");
		}
		return TRUE;
	}

And then you can simply access it's value elsewhere using $this->settings['example_client_id'].

You can of course use custom validation callbacks for fields if appropriate, but often you will need testSettings() where there are multiple settings which work together.

 

Merging Accounts

With some login handlers, particularly those which are OAuth-based, you may need to merge accounts. For example, imagine a user is registered on your community, and then they try to log in using Facebook. In this situation, you don't want to create a new account, but rather prompt the user to link their Facebook account with their existing account. In this case, throw an exception in your authenticate() method:

$exception = new IPSLoginException( 'generic_error', IPSLoginException::MERGE_SOCIAL_ACCOUNT );
$exception->handler = 'Example';
$exception->member = $existingAccount;
$exception->details = $token;
throw $exception;

Set $handler to the key for your login handler, $member to the existing account and $details to any details you need to link the accounts together, such as the access token.

Then implement a link() method:

	/**
	 * Link Account
	 *
	 * @param	IPSMember	$member		The member
	 * @param	mixed		$details	Details as they were passed to the exception thrown in authenticate()
	 * @return	void
	 */
	public static function link( IPSMember $member, $details )
	{
		$userData = IPSHttpUrl::external( "https://www.example.com/userData" )->setQueryString( 'token', $details )->request()->get()->decodeJson();
		$member->my_custom_id = $userData['id'];
		$member->save();
	}

Your link() method is called after the user has provided their password and it is safe to link the accounts together. Do whatever is necessary so that on subsequent logins, you can log the user in without intervention. Note that link() is static and cannot use any settings from the login handler.

 

Checking if email/username is in use

When a user registers an account on the community, your handler can check if the email address or username is acceptable. This is useful if you want your login handler to provide close integration such as is provided by the LDAP and IPS Connect handlers. The methods are emailIsInUse() and usernameIsInUse() - see the signatures in LoginAbstract for information on how to override these.

 

Changing Details and additional callbacks

When a user changes their email, password or username on the community, your handler can be notified of these changes and update their databases. You need to implement the canChange() method to let the User CP controllers know you support this functionality, and then the methods are changeEmail(), changePassword() and changeUsername() - see the signatures in LoginAbstract for information on how to override these.

Additional callbacks are also available - logoutAccount(), createAccount(), validateAccount(), deleteAccount(), banAccount() and mergeAccounts() - see the signatures in LoginAbstract for information on how to override these.

czw, 01 wrzesień 2016