Authentication process
Prerequisite: You must be familiar with the Syntax used in Tutorials and have already created an extension.
- learning:
- Allow connection to iTop using a different authentication method
- level:
- Advanced
- domains:
- PHP, Access rights
- min version:
- 2.7.0
Starting with version 2.7.0, it is now much easier to add new authentication methods in order to connect to iTop.
In this tutorial we will see:
-
How the login process is done in 2.7
-
What are the new interfaces (APIs) to extend the login process
-
How to extend the authentication process
Authentication Behavior
The authentication process of iTop 2.7 has been rewritten using a FSM (Finite State Machine) which is an automaton following different states.
General Algorithm
-
The automaton begins at the state
start
-
For each state all the relevant plugins are called
-
When all the plugins have been called for one state, and have all accepted to go on, the automaton advance to the next state.
Detailed automaton algorithm
The simplified pseudo-code for the login automaton is:
- LoginWebPage::Login()
-
$sLoginState = $_SESSION['login_state']; // defaulted to 'start' while (true) { // Select all the plugins corresponding to 'login_mode' and 'allowed_login_types' configuration $aLoginPlugins = self::GetLoginPluginList(); foreach ($aLoginPlugins as $oLoginFSMExtensionInstance) { $iResponse = $oLoginFSMExtensionInstance->LoginAction($sLoginState, $iErrorCode); if ($iResponse == self::LOGIN_FSM_RETURN_OK) { return self::EXIT_CODE_OK; // login OK, exit FSM } if ($iResponse == self::LOGIN_FSM_RETURN_ERROR) { $sLoginState = 'set_error'; // prepare to advance to 'error' state break; } } // Every plugin has nothing else to do in this state, go forward $sLoginState = self::AdvanceLoginFSMState($sLoginState); $_SESSION['login_state'] = $sLoginState; }
Variables
The login process uses the session to store the state and other information. The main variables used by the login process are:
-
state stored in
$_SESSION['login_state']
-
login mode stored in
$_SESSION['login_mode']
-
can logoff stored in
$_SESSION['can_logoff']
login_state
have to be managed only by iTop
framework,don't try to set it by yourself in your extension, it can lead to security issues
and weird login process behavior.
Login Plugin Selection
At every state of the login process, the list of login plugins
is refreshed by calling
LoginWebPage::GetLoginPluginList()
.
-
First the plugin list is filtered using the
login_mode
session variable, so only the plugin corresponding to the current login mode is active (or all the plugins iflogin_mode
is undefined). -
Plugins having the mode
before
andafter
are always selected, these are considered as “system” plugins having to be first or last in certain states. -
The resulting list is ordered using
before
thenallowed_login_types
configuration settings thenafter
.
Login states
start
-
Entry state.
-
This is the default state, either this is the first call for the user or it follows an error or a logout.
-
In this state some initialization can be done, but depending on the session, the plugins are not guaranteed to be called in this state, so it's better for a plugin to ignore this state.
login mode detection
-
Detect which login plugin to use.
-
The idea is to let some rare and specialized plugins shunt the normal order of detection if they identify the request as non ambiguously releving from their own auth mecanism. (ie: http basic auth)
-
If it is possible to decide from a non ambiguous manner that the mode is the one managed by the plugin, then the
login_mode
session variable should be set. -
If it is not possible to detect the current mode, do nothing here.
read credentials
-
Read the credentials.
-
The idea is to call each plugin in the configured order so that the first declared trigger its own login process.
-
the
login_mode
session variable should be set at this point. -
The goal of this state is to read the credentials either by displaying a form or redirecting an external authentication system (in case of SSO).
-
If redirected to an external authentication system, then the plugin will exit the PHP allowing the user to enter its credentials.
-
It is the responsibility of the authentication plugin to call back iTop in order to continue the login process (generally returning to
/pages/UI.php
). -
The concerned plugin will be called again with the same state (read credentials) in order to read the credentials.
-
The
auth_user
session variable is generally set at this point.
check credentials
-
Check if the credentials are valid.
-
When using an external authentication system, just controls that The
auth_user
session variable is set. this means that the external system gave back the info to iTop.
credentials ok
-
Check that the credentials correspond to a valid iTop User.
-
Call
LoginWebPage::CheckUser()
to check the validity of the iTop User corresponding to the credentials given. -
If the plugin supports this feature, User provisioning can be done at this point
-
Call
LoginWebPage::OnLoginSuccess()
if the User is valid, in order to initialize the session
user ok
-
Additional User check
-
At this point a valid iTop User has been selected.
-
Additional checks like 2 factor authentication can be done at this point.
connected
-
User connected
-
Call
LoginWebPage::CheckLoggedUser()
to verify that the user is always valid -
Session variable
can_logoff
must be set to true if the “log off” menu can be enabled
error
-
An error occurred, next state will be start
-
Some cleanup can be done at this point
-
An error form can also be displayed to the user by calling
LoginWebPage::DisplayLogoutPage()
Authentication interfaces
An API is provided to ease the creation of new authentication extensions.
interface iLoginFSMExtension
Low level interface for the plugin process. This interface contains 2 methods:
-
ListSupportedLoginModes()
-
Called to determine the list of supported login modes by this plugin
-
Return:
-
an array of supported login modes
-
-
-
LoginAction($sLoginState, &$iErrorCode)
-
Called for every state when the plugin is concerned
-
Parameters:
-
$sLoginState
The current state of the login process among:-
LoginWebPage::LOGIN_STATE_START
-
LoginWebPage::LOGIN_STATE_MODE_DETECTION
-
LoginWebPage::LOGIN_STATE_READ_CREDENTIALS
-
LoginWebPage::LOGIN_STATE_CHECK_CREDENTIALS
-
LoginWebPage::LOGIN_STATE_CREDENTIALS_OK
-
LoginWebPage::LOGIN_STATE_USER_OK
-
LoginWebPage::LOGIN_STATE_CONNECTED
-
LoginWebPage::LOGIN_STATE_ERROR
-
-
$iErrorCode
The error code returned in case of error-
LoginWebPage::EXIT_CODE_OK
-
LoginWebPage::EXIT_CODE_MISSINGLOGIN
-
LoginWebPage::EXIT_CODE_MISSINGPASSWORD
-
LoginWebPage::EXIT_CODE_WRONGCREDENTIALS
-
LoginWebPage::EXIT_CODE_MUSTBEADMIN
-
LoginWebPage::EXIT_CODE_PORTALUSERNOTAUTHORIZED
-
LoginWebPage::EXIT_CODE_NOTAUTHORIZED
-
-
-
Return:
-
LoginWebPage::LOGIN_FSM_RETURN_ERROR An error occurred → $iErrorCode must be set
-
LoginWebPage::LOGIN_FSM_RETURN_OK The login process is OK and terminated
-
LoginWebPage::LOGIN_FSM_RETURN_IGNORE The login process will proceed to next plugin or state
-
-
abstract class AbstractLoginFSMExtension
Abstract class used as an helper, implementing iLoginFSMExtension and providing callback functions for every state.
-
ListSupportedLoginModes()
-
see
iLoginFSMExtension
-
-
OnStart(&$iErrorCode)
-
OnModeDetection(&$iErrorCode)
-
OnReadCredentials(&$iErrorCode)
-
OnCheckCredentials(&$iErrorCode)
-
OnCredentialsOK(&$iErrorCode)
-
OnUsersOK(&$iErrorCode)
-
OnConnected(&$iErrorCode)
-
OnError(&$iErrorCode)
-
Callback functions corresponding to each login process state.
-
Same return than
LoginAction()
.
-
-
LoginAction($sLoginState, &$iErrorCode)
-
Not necessary to override this function if callbacks are used
-
see
iLoginFSMExtension
-
interface iLogoutExtension
Called when the user decides to logoff form the iTop screens.
-
ListSupportedLoginModes()
-
Called to determine the list of supported login modes by this plugin
-
Return:
-
an array of supported login modes
-
-
-
LogoutAction()
-
Called when the user decides to logout.
-
Extending the authentication process
In order to propose a new authentication method you'll have to:
-
Provide a PHP class extending
AbstractLoginFSMExtension
(prefered) or implementingiLoginFSMExtension
-
Optionally provide a PHP class implementing
iLogoutExtension
interface. -
configure in
allowed_login_types
the new mode corresponding to your extension
The login process will use the provided PHP class in the login process.
Login/logoff/password management screens can also be customized.
You can provide blocks to be inserted in the login screen, CSS to override or complete the current one. You can even replace completely the screens.
Login and User Provisioning API
The login and provisioning APIs are provided by LoginWebPage class.
Login API
/** * Login API: Check that credentials correspond to a valid user * Used only during login process when the password is known * * @api * * @param string $sAuthUser * @param string $sAuthPassword * @param string $sAuthentication ('internal' or 'external') * * @return bool (true if User OK) * */ public static function LoginWebPage::CheckUser($sAuthUser, $sAuthPassword='', $sAuthentication='external')
/** * Login API: Store User info in the session when connection is OK * * @api * * @param $sAuthUser * @param $sAuthentication * @param $sLoginMode * */ public static function LoginWebPage::OnLoginSuccess($sAuthUser, $sAuthentication, $sLoginMode)
/** * Login API: Check that an already logger User is still valid * * @api * * @param int $iErrorCode * * @return int LOGIN_FSM_RETURN_OK or LOGIN_FSM_RETURN_ERROR */ public static function LoginWebPage::CheckLoggedUser(&$iErrorCode)
User Provisioning
User provisioning is done in
AbstractLoginFSMExtension::OnCheckCredentials()
method. You can create a User and a Person if they are missing.
Example of pseudo-code for User provisioning:
private function DoUserProvisioning($sLogin, $sEmail, $sFirstName, $sLastName, $sOrganization, $aProfiles) { if (!Config::Get('synchronize_user')) { return; // Not configured } try { if (LoginWebPage::FindUser($sLogin, false)) { return; // Already a user, do nothing } $oPerson = LoginWebPage::FindPerson($sEmail); if ($oPerson == null) { if (!Config::Get('synchronize_contact')) { return; // Not configured } // Create the person $oPerson = LoginWebPage::ProvisionPerson($sFirstName, $sLastName, $sEmail, $sOrganization); } LoginWebPage::ProvisionUser($sLogin, $oPerson, $aProfiles); } catch (Exception $e) { IssueLog::Error($e->getMessage()); } }
User Provisioning API
API to use for User provisioning.
/** * Provisioning API: Find a User * * @api * * @param string $sAuthUser * @param bool $bMustBeValid * @param string $sType * * @return \User|null */ public static function LoginWebPage::FindUser($sAuthUser, $bMustBeValid = true, $sType = 'External')
/** * Provisioning API: Find a Person by email * * @api * * @param string $sEmail * * @return \Person|null */ public static function LoginWebPage::FindPerson($sEmail)
/** * Provisioning API: Create a person * * @api * * @param string $sFirstName * @param string $sLastName * @param string $sEmail * @param string $sOrganization * @param array $aAdditionalParams * * @return \Person */ public static function LoginWebPage::ProvisionPerson($sFirstName, $sLastName, $sEmail, $sOrganization, $aAdditionalParams = array())
/** * Provisioning API: Create a User * * @api * * @param string $sAuthUser * @param Person $oPerson * @param array $aRequestedProfiles profiles to add to the new user * * @return \UserExternal|null */ public static function LoginWebPage::ProvisionUser($sAuthUser, $oPerson, $aRequestedProfiles)
Login facility
Additional debug messages are available when setting the
configuration variable login_debug
to true.
Example of debug traces:
- Login debug logs
-
2019-08-21 09:53:52 | Info | --------------------------------- 2019-08-21 09:53:52 | Info | --> Entering Login FSM with state: [start] 2019-08-21 09:53:52 | Info | SESSION: 6nnga05fgqarfhe658rfjr50jq Array ( [itop_env] => production [login_state] => start ) 2019-08-21 09:53:52 | Info | Login: state: [start] call: LoginDefaultBefore 2019-08-21 09:53:52 | Info | Login: state: [start] call: Combodo\iTop\Cas\CASLoginExtension 2019-08-21 09:53:52 | Info | Login: state: [start] call: LoginForm 2019-08-21 09:53:52 | Info | Login: state: [start] call: LoginExternal 2019-08-21 09:53:52 | Info | Login: state: [start] call: LoginBasic 2019-08-21 09:53:52 | Info | Login: state: [start] call: LoginDefaultAfter 2019-08-21 09:53:52 | Info | SESSION: 6nnga05fgqarfhe658rfjr50jq Array ( [itop_env] => production [login_state] => login mode detection ) 2019-08-21 09:53:52 | Info | Login: state: [login mode detection] call: LoginDefaultBefore 2019-08-21 09:53:52 | Info | Login: state: [login mode detection] call: Combodo\iTop\Cas\CASLoginExtension 2019-08-21 09:53:52 | Info | Login: state: [login mode detection] call: LoginForm 2019-08-21 09:53:52 | Info | Login: state: [login mode detection] call: LoginExternal 2019-08-21 09:53:52 | Info | Login: state: [login mode detection] call: LoginBasic 2019-08-21 09:53:52 | Info | Login: state: [login mode detection] call: LoginDefaultAfter 2019-08-21 09:53:52 | Info | SESSION: 6nnga05fgqarfhe658rfjr50jq Array ( [itop_env] => production [login_state] => read credentials ) 2019-08-21 09:53:52 | Info | Login: state: [read credentials] call: LoginDefaultBefore 2019-08-21 09:53:52 | Info | Login: state: [read credentials] call: Combodo\iTop\Cas\CASLoginExtension 2019-08-21 09:53:52 | Info | --------------------------------- 2019-08-21 09:53:52 | Info | --> Entering Login FSM with state: [read credentials] 2019-08-21 09:53:52 | Info | SESSION: 6nnga05fgqarfhe658rfjr50jq Array ( [itop_env] => production [login_state] => read credentials [login_mode] => cas [cas_count] => 1 ) 2019-08-21 09:53:52 | Info | Login: state: [read credentials] call: LoginDefaultBefore 2019-08-21 09:53:52 | Info | Login: state: [read credentials] call: Combodo\iTop\Cas\CASLoginExtension 2019-08-21 09:53:53 | Info | --------------------------------- 2019-08-21 09:53:53 | Info | --> Entering Login FSM with state: [read credentials] 2019-08-21 09:53:53 | Info | SESSION: 6nnga05fgqarfhe658rfjr50jq Array ( [itop_env] => production [login_state] => read credentials [login_mode] => cas [cas_count] => 1 [phpCAS] => Array ( ... ) ) 2019-08-21 09:53:53 | Info | Login: state: [read credentials] call: LoginDefaultBefore 2019-08-21 09:53:53 | Info | Login: state: [read credentials] call: Combodo\iTop\Cas\CASLoginExtension 2019-08-21 09:53:53 | Info | SESSION: 6nnga05fgqarfhe658rfjr50jq Array ( [itop_env] => production [login_state] => read credentials [login_mode] => cas [cas_count] => 1 [phpCAS] => Array ( ... ) [auth_user] => agavalda ) 2019-08-21 09:53:53 | Info | Login: state: [read credentials] call: LoginDefaultAfter 2019-08-21 09:53:53 | Info | SESSION: 6nnga05fgqarfhe658rfjr50jq Array ( [itop_env] => production [login_state] => check credentials [login_mode] => cas [cas_count] => 1 [phpCAS] => Array ( ... ) [auth_user] => agavalda ) [...] 2019-08-21 09:53:53 | Info | SESSION: t6itov5mhsf6886clhenaj6q1f Array ( [itop_env] => production [login_state] => connected [login_mode] => cas [cas_count] => 1 [phpCAS] => Array ( ... ) [auth_user] => agavalda [archive_mode] => 0 [profile_list] => Array ( [1] => Administrator ) ) 2019-08-21 09:53:53 | Info | Login: state: [connected] call: LoginDefaultBefore 2019-08-21 09:53:53 | Info | Login: state: [connected] call: Combodo\iTop\Cas\CASLoginExtension
Screen customization
Authent-CAS
As an example, we will study the authent-cas
module.
The authent-cas module (located in
datamodels/2.x/authent-cas
) is used to integrate
Central
Authentication Service (CAS) authentication to iTop. You must
install it on your iTop server.
It mainly contains the class CASLoginExtension
extending AbstractLoginFSMExtension
and implementing
iLogoutExtension
interface.
ListSupportedLoginModes()
This extension provide “cas” login mode.
- CASLoginExtension::ListSupportedLoginModes()
-
public function ListSupportedLoginModes() { return array('cas'); }
OnStart()
This function only cleanup some session information if necessary, it is useful to remove unnecessary session variables when possible.
- CASLoginExtension::OnStart()
-
protected function OnStart(&$iErrorCode) { unset($_SESSION['phpCAS']); // generated by the CAS library return LoginWebPage::LOGIN_FSM_RETURN_CONTINUE; }
OnModeDetection()
This function is not implemented in this extension.
OnReadCredentials()
This function is used to read the credentials from the CAS
server. Note that the first time, the login_mode
session variable may not be set. In this case the login mode is set
for next state processing.
This function is called twice:
-
The first time the user is not yet authenticated, redirect to CAS for authentication and exit (
phpCAS::forceAuthentication();
). -
The second time the user is provided in
auth_user
session variable -
If this function is called a another time without being authenticated, an error is returned (see
cas_count
)
- CASLoginExtension::OnReadCredentials()
-
protected function OnReadCredentials(&$iErrorCode) { if (!isset($_SESSION['login_mode']) || ($_SESSION['login_mode'] == 'cas')) { $_SESSION['login_mode'] = 'cas'; static::InitCASClient(); if (phpCAS::isAuthenticated()) { $_SESSION['auth_user'] = phpCAS::getUser(); } else { if (!isset($_SESSION['cas_called'])) { $_SESSION['cas_called'] = 1; } else { // CAS has already been called, warn the user unset($_SESSION['cas_called']); $iErrorCode = LoginWebPage::EXIT_CODE_MISSINGLOGIN; return LoginWebPage::LOGIN_FSM_RETURN_ERROR; } phpCAS::forceAuthentication(); // Redirect to CAS and exit } } return LoginWebPage::LOGIN_FSM_RETURN_CONTINUE; }
OnCheckCredentials()
This function checks that auth_user
session
variable and if cas_user_synchro
configuration
variable is true, provision the User if possible.
- CASLoginExtension::OnCheckCredentials()
-
protected function OnCheckCredentials(&$iErrorCode) { if ($_SESSION['login_mode'] == 'cas') { if (!isset($_SESSION['auth_user'])) { $iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS; return LoginWebPage::LOGIN_FSM_RETURN_ERROR; } if (Config::Get('cas_user_synchro' )) { self::DoUserProvisioning($_SESSION['auth_user']); } } return LoginWebPage::LOGIN_FSM_RETURN_CONTINUE; }
OnCredentialsOK()
This function checks that the User in iTop is valid
- CASLoginExtension::OnCredentialsOK()
-
protected function OnCredentialsOK(&$iErrorCode) { if ($_SESSION['login_mode'] == 'cas') { $sAuthUser = $_SESSION['auth_user']; if (!LoginWebPage::CheckUser($sAuthUser, '', 'external')) { $iErrorCode = LoginWebPage::EXIT_CODE_NOTAUTHORIZED; return LoginWebPage::LOGIN_FSM_RETURN_ERROR; } LoginWebPage::OnLoginSuccess($sAuthUser, 'external', $_SESSION['login_mode']); } return LoginWebPage::LOGIN_FSM_RETURN_CONTINUE; }
OnConnected()
This function is called when the user is connected (for every itop page web or ajax). It checks that the User is still valid.
- CASLoginExtension::OnConnected()
-
protected function OnConnected(&$iErrorCode) { if ($_SESSION['login_mode'] == 'cas') { $_SESSION['can_logoff'] = true; return LoginWebPage::CheckLoggedUser($iErrorCode); } return LoginWebPage::LOGIN_FSM_RETURN_CONTINUE; }
OnError()
This function is called on error. It generally display a logout/error page with a link to reconnect.
- CASLoginExtension::OnError()
-
protected function OnError(&$iErrorCode) { if ($_SESSION['login_mode'] == 'cas') { unset($_SESSION['phpCAS']); // Generated by CAS library if ($iErrorCode != LoginWebPage::EXIT_CODE_MISSINGLOGIN) { $oLoginWebPage = new LoginWebPage(); $oLoginWebPage->DisplayLogoutPage(false, Dict::S('CAS:Error:UserNotAllowed')); } exit(); } return LoginWebPage::LOGIN_FSM_RETURN_CONTINUE; }
LogoutAction()
This function is part of the iLogoutExtension
interface. For this extension the user will also be disconnected
from the CAS server.
- CASLoginExtension::LogoutAction()
-
public function LogoutAction() { $sCASLogoutUrl = Config::Get('cas_logout_redirect_service'); if (empty($sCASLogoutUrl)) { $sCASLogoutUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php'; } static::InitCASClient(); phpCAS::logoutWithRedirectService($sCASLogoutUrl); // Redirects to the CAS logout page }