Skocz do zawartości
Zaloguj się, aby obserwować  
Invisionize.eu

[Dokumentacja IPS4] Custom Login Handlers

Polecane posty

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:

  • "Standard" login handlers which take a username and/or email address and password. For example, the default login handler, LDAP and IPS Connect.
  • Login handlers which use a custom form, which are usually (though not necessarily) OAuth based. For example, Facebook, Twitter and LinkedIn login.

 

Getting Started

Both types are implemented by creating a PHP file in the system/Login folder containing class that extends \IPS\Login\LoginAbstract 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 \IPS\Login\Example, 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 IPS\Login;

class _Example extends LoginAbstract
{
	/**
	 * @brief	Authentication types
	 */
	public $authTypes = \IPS\Login::AUTH_TYPE_USERNAME;
		
	/**
	 * Authenticate
	 *
	 * @param	array	$values	Values from from
	 * @return	\IPS\Member
	 * @throws	\IPS\Login\Exception
	 */
	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 = \IPS\Member::load( $username );
		}
		catch ( \OutOfRangeException $e )
		{
			throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack('login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('username') ) ) ), \IPS\Login\Exception::NO_ACCOUNT );
		}
		
		/* Check password */
		if ( $password !== 'the-correct-password' ) // Implement correct check here
		{
			throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::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 \IPS\Helpers\Form\[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    \IPS\Member    $member    The member
     * @return    bool
     */
    public function canChange( $type, \IPS\Member $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 = \IPS\Login::AUTH_TYPE_USERNAME;									// Username
public $authTypes = \IPS\Login::AUTH_TYPE_EMAIL;									// Email address
public $authTypes = \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::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 \IPS\Member object if the login was successful, or throw an \IPS\Login\Exception object if it wasn't. If throwing an \IPS\Login\Exception object, the message is displayed to the user, and the code should be one of the following values:

throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::INTERNAL_ERROR ); // Something went wrong with the login handler which wasn't the user's fault.
throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::BAD_PASSWORD ); // The password the user provided was incorrect.
throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::NO_ACCOUNT ); // The username or email address the user provided did not match any account.
throw new \IPS\Login\Exception( "Login Failed.", \IPS\Login\Exception::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 = \IPS\Member::load( $username );
		}
		catch ( \OutOfRangeException $e )
		{
			$member = new \IPS\Member;
			$member->member_group_id = \IPS\Settings::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 \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::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 IPS\Login;

class _Example extends LoginAbstract
{
	/** 
	 * @brief	Icon
	 */
	public static $icon = 'lock';
	
	/**
	 * Get Form
	 *
	 * @param	\IPS\Http\Url	$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 = \IPS\Http\Url::internal( 'applications/core/interface/example/auth.php', 'none' );
		$oauthUrl = \IPS\Http\Url::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	\IPS\Member		$member	If we want to integrate this login method with an existing member, provide the member object
	 * @return	\IPS\Member
	 * @throws	\IPS\Login\Exception
	 */
	public function authenticate( $url, $member=NULL )
	{
		/* Get user details from service */
		$userData = \IPS\Http\Url::external( "https://www.example.com/userData" )->setQueryString( 'token', \IPS\Request::i()->token )->request()->get()->decodeJson();
		
		/* Get or create member */
		if ( $member === NULL )
		{			
			/* Try to find member */
			$member = \IPS\Member::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 = \IPS\Member::load( $userData['email'], 'email' );
				if ( $existingEmail->member_id )
				{
					$exception = new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT );
					$exception->handler = 'Example';
					$exception->member = $existingEmail;
					$exception->details = \IPS\Request::i()->token;
					throw $exception;
				}
				
				/* Create member */
				$member = new \IPS\Member;
				$member->member_group_id = \IPS\Settings::i()->member_group;
				
				/* Is a user doesn't exist with this username, set it (if it does, the user will automatically be prompted) */
				$existingUsername = \IPS\Member::load( $userData['name'], 'name' );
				if ( !$existingUsername->member_id )
				{
					$member->name = $userData['name'];
				}
				
				/* Set validating if necessary */
				if ( \IPS\Settings::i()->reg_auth_type == 'admin' or \IPS\Settings::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	\IPS\Member	$member		The member
	 * @param	mixed		$details	Details as they were passed to the exception thrown in authenticate()
	 * @return	void
	 */
	public static function link( \IPS\Member $member, $details )
	{
		$userData = \IPS\Http\Url::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 \IPS\Helpers\Form\[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	\IPS\Member	$member	The member
	 * @return	bool
	 */
	public function canChange( $type, \IPS\Member $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 \IPS\Helpers\Form object.

The authenticate() method is where the bulk of your login code will go. If your loginForm() method returns an \IPS\Helpers\Form 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 \IPS\Member object or throw an \IPS\Login\Exception 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 \IPS\Helpers\Form\[Type]( ... ), ... );
	 * @endcode
	 */
	public function acpForm()
	{
		return array(
			'example_client_id'	=> new \IPS\Helpers\Form\Text( '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 \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::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	\IPS\Member	$member		The member
	 * @param	mixed		$details	Details as they were passed to the exception thrown in authenticate()
	 * @return	void
	 */
	public static function link( \IPS\Member $member, $details )
	{
		$userData = \IPS\Http\Url::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.

Wyświetl pełny artykuł

Udostępnij ten post


Link to postu
Udostępnij na innych stronach

Bądź aktywny! Zaloguj się lub utwórz konto

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Zaloguj się, aby obserwować  

  • Kto przegląda   0 użytkowników

    Brak zalogowanych użytkowników przeglądających tę stronę.

×

Ważne informacje

Kontynuując przeglądanie strony, wyrażasz zgodę na używanie przez nas plików cookies.