Jump to content

Community

Developer Documentation

Creating a Username/Password Handler

To make a login handler where the user enters a username or email address and password you will add the \IPS\Login\Handler\UsernamePasswordHandler trait to your Login Handler class and implement the two methods it requires:

  • authenticateUsernamePassword() is called when a user enters a username or email address and password into the login form. You need to verify if it is valid in your login handler and return an \IPS\Member object if it is (including creating the account if there isn't one already and ensuring reauthentication happens if there is already an account but the user hasn't logged in with your handler before) or throw an \IPS\Login\Exception exception if not.
  • authenticatePasswordForMember() is called when a user is doing something sensitive and is being asked to reauthenticate by entering their password from your login handler again to confirm.

This is a basic skeleton of a username/password based Login Handler. This code will process a login for the a hardcoded username/email address and password. Obviously a real login handler wouldn't hardcode this data but would look it up in a database or through an external service.

class _MyLoginHandler extends \IPS\Login\Handler
{
	use \IPS\Login\Handler\UsernamePasswordHandler;
	
	/**
	 * @brief	Can we have multiple instances of this handler?
	 */
	public static $allowMultiple = FALSE;
		
	/**
	 * Get title
	 *
	 * @return	string
	 */
	public static function getTitle()
	{
		return 'my_cusom_login_handler'; // Create a langauge string for this
	}
	
	/**
	 * Authenticate
	 *
	 * @param	\IPS\Login	$login				The login object
	 * @param	string		$usernameOrEmail	The username or email address provided by the user
	 * @param	string		$password			The plaintext password provided by the user
	 * @return	\IPS\Member
	 * @throws	\IPS\Login\Exception
	 */
	public function authenticateUsernamePassword( \IPS\Login $login, $usernameOrEmail, $password )
	{
		/* Is this a user we can process? NOTE: Obviously a real login handler would look up $usernameOrEmail in some kind of database or external service */
		$authType = $this->authType(); // NOTE: The UsernamePasswordHandler trait has automatically provided a setting in this login method which allows the administrator to choose if the user will enter a username, email address, or either
		if ( ( $authType & \IPS\Login::AUTH_TYPE_USERNAME and $usernameOrEmail === 'example' ) or ( $authType & \IPS\Login::AUTH_TYPE_EMAIL and $usernameOrEmail === 'example@example.com' ) )
		{
			$userId = 1; // NOTE: This would be set to some kind of identifier for the user within that service. It doesn't have to be numeric, but does have to be unique.
			$name = 'example'; // NOTE: We will use this later to create an account if it doesn't exist. If your login handler doesn't store display names, set this to NULL (and the user will be asked to provide one)
			$email = 'example@example.com'; // NOTE: We will use this later to create an account if it doesn't exist. If your login handler doesn't store display names, set this to NULL (and the user will be asked to provide one)
		}
		else
		{
			switch ( $this->authType() )
			{
				case \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL:
					$type = 'username_or_email';
					break;
					
				case \IPS\Login::AUTH_TYPE_USERNAME:
					$type = 'username';
					break;
					
				case \IPS\Login::AUTH_TYPE_EMAIL:
					$type = 'email_address';
					break;
			}
			
			throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack( 'login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $type ) ) ) ), \IPS\Login\Exception::NO_ACCOUNT );
		}
		
		/* Find their local account if they have already logged in using this method in the past */
		$member = NULL;
		try
		{
			$link = \IPS\Db::i()->select( '*', 'core_login_links', array( 'token_login_method=? AND token_identifier=?', $this->id, $userId ) )->first();
			$member = \IPS\Member::load( $link['token_member'] );
			
			/* If the user never finished the linking process, or the account has been deleted, discard this access token */
			if ( !$link['token_linked'] or !$member->member_id )
			{
				\IPS\Db::i()->delete( 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $link['token_member'] ) );
				$member = NULL;
			}
		}
		catch ( \UnderflowException $e ) { }
		
		/* Is the password valid? NOTE: Obviously a real login handler would actually verify the password is correct for the member. This step is done
			AFTER looking up their local account so that their account can be locked if they provide multiple wrong passwords */
		if ( $password !== 'example' )
		{
			throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $member );
		}
		
		/* If we have a local account, go ahead and return it */
		if ( $member )
		{
			return $member;
		}
		
		/* Otherwise, we need to either create one or link it to an existing one */
		try
		{
			/* If the user is setting this up in the User CP, they are already logged in. Ask them to reauthenticate to link those accounts */
			if ( $login->type === \IPS\Login::LOGIN_UCP )
			{
				$exception = new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT );
				$exception->handler = $this;
				$exception->member = $login->reauthenticateAs;
				throw $exception;
			}
			
			/* Try to create one. NOTE: Invision Community will automatically throw an exception which we catch below if $email matches an existing account, if registration is disabled, or if Spam Defense blocks the account creation */			
			$member = $this->createAccount( $name, $email );
			
			/* If we're still here, a new account was created. Store something in core_login_links so that the next time this user logs in, we know they've used this method before */
			\IPS\Db::i()->insert( 'core_login_links', array(
				'token_login_method'	=> $this->id,
				'token_member'			=> $member->member_id,
				'token_identifier'		=> $userId,
				'token_linked'			=> 1,
			) );
			
			/* Log something in their history so we know that this login handler created their account */
			$member->logHistory( 'core', 'social_account', array(
				'service'		=> static::getTitle(),
				'handler'		=> $this->id,
				'account_id'	=> $userId,
				'account_name'	=> $name,
				'linked'		=> TRUE,
				'registered'	=> TRUE
			) );
			
			/* Set up syncing options. NOTE: See later steps of the documentation for more details - it is fine to just copy and paste this code */
			if ( $syncOptions = $this->syncOptions( $member, TRUE ) )
			{
				$profileSync = array();
				foreach ( $syncOptions as $option )
				{
					$profileSync[ $option ] = array( 'handler' => $this->id, 'ref' => NULL, 'error' => NULL );
				}
				$member->profilesync = $profileSync;
				$member->save();
			}
			
			return $member;
		}
		catch ( \IPS\Login\Exception $exception )
		{
			/* If the account creation was rejected because there is already an account with a matching email address
				make a note of it in core_login_links so that after the user reauthenticates they can be set as being
				allowed to use this login handler in future */
			if ( $exception->getCode() === \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT )
			{
				\IPS\Db::i()->insert( 'core_login_links', array(
					'token_login_method'	=> $this->id,
					'token_member'			=> $exception->member->member_id,
					'token_identifier'		=> $userId,
					'token_linked'			=> 0,
				) );
			}
			
			throw $exception;
		}
	}
	
	/**
	 * Authenticate
	 *
	 * @param	\IPS\Member	$member				The member
	 * @param	string		$password			The plaintext password provided by the user
	 * @return	bool
	 */
	public function authenticatePasswordForMember( \IPS\Member $member, $password )
	{
		// NOTE: Obviously a real login handler would actually verify the password is correct for the member. You may want to write a separate method
		// which can be called by both this method and authenticateUsernamePassword() so that the code isn't duplicated
		return $member->email === 'example@example.com' and $password === 'example';
	}
}

Note: The \IPS\Login\Handler\UsernamePasswordHandler trait will automatically add a setting to allow the administrator to choose if the user will provide a username or email address to sign in, and this value is returned to you by the authType() method. If this does not apply to your login handler, you will need to override it. For example, if your login handler can only accept email addresses, you will need to add this code within your class:

	/**
	 * ACP Settings Form
	 *
	 * @return	array	List of settings to save - settings will be stored to core_login_methods.login_settings DB field
	 * @code
	 	return array( 'savekey'	=> new \IPS\Helpers\Form\[Type]( ... ), ... );
	 * @endcode
	 */
	public function acpForm()
	{
		return array(); // Remove the setting set by the UsernamePasswordHandler trait
	}
	
	/**
	 * Get auth type
	 *
	 * @return	int
	 */
	public function authType()
	{
		return \IPS\Login::AUTH_TYPE_EMAIL;
	}
Edited by Mark

  Report Document


×
×
  • Create New...