I did it another way. I created SSO.php in modules/Users/ which is essentially the same thing as Authenticate.php, except modified. Below are the steps that I recount for integrating SSO/CAS authentication. It works best if you have a SOAP service or an LDAP directory to check for access rights and fill in employee fields. I'm thinking of changing it so that it calls our SOAP service to pull the results of the LDAP, to fill in user details every time and stay accurate every time. We are afraid of opening our LDAP server to the public, so we have built a SOAP service that gives us poor man's LDAP.
First, I added an entry for SSO.php in include/MVC/SugarApplication.php so that it would be allowed to run before an authenticated session had been established: PHP Code:
$allowed_actions = array('Authenticate', 'Login', 'SSO');
changed any redirects to Login action to SSO action in include/MVC/SugarApplication.php PHP Code:
SugarApplication::redirect('index.php?action=SSO&module=Users');
copied module/Users/Authenticate.php into module/Users/SSO.php and placed the following code immediately after the GPL license PHP Code:
require_once('CAS/CAS.php');
phpCAS::client(CAS_VERSION_2_0,'xxx.xxx.xxx.xxx', 443, '');
phpCAS::forceAuthentication();
$authController->login_sso(phpCAS::getUser());
modified module/Users/authentication/AuthenticationController.php by adding the login_sso function (customized login function) PHP Code:
function login_sso($username, $PARAMS = array ()) {
//kbrill bug #13225
$_SESSION['loginAttempts'] = (isset($_fSESSION['loginAttempts']))? $_SESSION['loginAttempts'] + 1: 1;
unset($GLOBALS['login_error']);
if($this->loggedIn)return $this->loginSuccess;
$this->loginSuccess = $this->authController->loginAuthenticate_sso($username, $PARAMS);
$this->loggedIn = true;
if($this->loginSuccess){
//Ensure the user is authorized
checkAuthUserStatus();
loginLicense();
if(!empty($GLOBALS['login_error'])){
session_unregister('authenticated_user_id');
$GLOBALS['log']->fatal('FAILED LOGIN: potential hack attempt');
$this->loginSuccess = false;
return false;
}
$ut = $GLOBALS['current_user']->getPreference('ut');
if(empty($ut) && $_REQUEST['action'] != 'SaveTimezone') {
$GLOBALS['module'] = 'Users';
$GLOBALS['action'] = 'SetTimezone';
ob_clean();
header("Location: index.php?module=Users&action=SetTimezone");
sugar_cleanup(true);
}
//call business logic hook
if(isset($GLOBALS['current_user']))
$GLOBALS['current_user']->call_custom_logic('after_login');
}else{
//kbrill bug #13225
LogicHook::initialize();
$GLOBALS['logic_hook']->call_custom_logic('Users', 'login_failed');
$GLOBALS['log']->fatal('FAILED LOGIN:attempts[' .$_SESSION['loginAttempts'] .'] - '. $username);
}
return $this->loginSuccess;
}
customized module/Users/authentication/LDAPAuthenticate.php to have function loginAuthenticate_sso($username) PHP Code:
function loginAuthenticate_sso($username){
global $mod_strings;
session_unregister('login_error');
if ($this->userAuthenticate->loadUserOnSSO($username) {
return $this->postLoginAuthenticate();
}
if(strtolower(get_class($this)) != 'sugarauthenticate'){
$sa = new SugarAuthenticate();
$error = (!empty($_SESSION['login_error']))?$_SESSION['login_error']:'';
if($sa->loginAuthenticate($username, $password)){
return true;
}
$_SESSION['login_error'] = $error;
}
$_SESSION['login_user_name'] = $username;
$_SESSION['login_password'] = $password;
if(empty($_SESSION['login_error'])){
$_SESSION['login_error'] = $mod_strings['ERR_INVALID_PASSWORD'];
}
return false;
}
customized the file above with loadUserOnSSO (with segments from loadUser) PHP Code:
function loadUserOnSSO($name) {
global $mod_strings;
// Check if the LDAP extensions are loaded
#if(!function_exists('ldap_connect')) {
# $error = $mod_strings['LBL_LDAP_EXTENSION_ERROR'];
# $GLOBALS['log']->fatal($error);
# $_SESSION['login_error'] = $error;
# return false;
#}
global $login_error;
$GLOBALS['ldap_config'] = new Administration();
$GLOBALS['ldap_config']->retrieveSettings('ldap');
$GLOBALS['log']->debug("Starting user load for ". $name);
if(empty($name)) return false;
checkAuthUserStatus();
$user_id = $this->authenticateSSOUser($name);
if(empty($user_id)) {
//check if the user can login as a normal sugar user
$GLOBALS['log']->fatal('SECURITY: User authentication for '.$name.' failed');
return false;
}
$this->loadUserOnSession($user_id);
return true;
}
copied function authenticateUser from modules/Users/authentication/LDAPAuthenticate/LDAPAuthenticateUser.php to authenticateUserSSO PHP Code:
function authenticateUser($name) {
// Same code from authenticateUser function, except no password checking.
// We customized the code here to access a secret SOAP service available only
// to the server's IP and webapp key.
// If user has access rights to the CRM, createUser would automatically provision
// accounts for users with access rights to the CRM.
// We can't post the contents of the SOAP service function because of the sensitivity
// of the information. SOAP isn't the most secure way of doing it ;).
}
as well as customized createUser to work with SOAP data instead of LDAP data (it's still getting data through a SOAP service, which acts an intermediary between our LDAP services and webapps) PHP Code:
function createUser($name){
$user = new User();
$user->user_name = $name;
foreach($this->ldapUserInfo as $key=>$value){
$user->$key = $value;
}
$user->employee_status = 'Active';
$user->status = 'Active';
$user->is_admin = 0;
$user->save();
$a = "UPDATE users SET email1 = '". $this->ldapUserInfo["email1"] . "' WHERE id = '" . $user->id . "'";
$dbresult = $GLOBALS['db']->query($a);
$a = "UPDATE users SET ldap_guid = '". $this->ldapUserInfo["guid"] . "' WHERE id = '" . $user->id . "'";
$dbresult = $GLOBALS['db']->query($a);
return $user->id;
}
That wasn't so bad, was it? If you use this implementation, you'll want to keep Login.php around to throw error messages at users (since they are already authenticated with CAS by the time they can see Login). We removed the username and password fields and replaced the Submit button with a Logout button for users who have attempted to access the CRM but do not have access to it under the LDAP and therefore need a way to logout so other users can enter it. In our implementation, if a user does not have access rights to the CRM the system will throw back the error message from the SOAP service, i.e. ERROR 4KZ: USER '%user%' (%FirstName% %M.% %LastName%) not authorized to access CRM service. Please contact Human Resources for help.
One other thing to note about our implementation is that we are tracking and creating users by GUID instead of the username. While it's highly unlikely that usernames will change in the LDAP, if they do change due to marriage (or court order), we will modify the LDAP username to correspond with the changes that HR requests. GUID is guaranteed to stay the same unless the user is deleted, so it's a much more reliable way of linking users to their CRM profile.
Oh yeah, you'll want to modify modules/Users/Logout.php to redirect you to the CAS logout page
... You will put this after session_destroy(); PHP Code:
require_once("CAS/CAS.php");
phpCAS::client(CAS_VERSION_2_0,'xxx.xxx.xxx.xxx', 443,'');
phpCAS::setDebug();
phpCAS::logout("http%3A%2F%2Fcrm.somecompany.com%3A80%2Findex.php%3Faction%3DSSO%26module%3DUsers");
Bookmarks