ExpressionEngine CMS
Open, Free, Amazing

Thread

This is an archived forum and the content is probably no longer relevant, but is provided here for posterity.

The active forums are here.

Single Sign On - phpCAS and LDAP - Great Success!

March 20, 2009 3:55pm

Subscribe [8]
  • #1 / Mar 20, 2009 3:55pm

    Jason Hung

    6 posts

    I have EE integrated with phpCAS and LDAP. Would anyone be interested in this code? It’s not a simple EE hookable code since some of the hooks do not exist for EE as it stands right now.  It is fairly easy to install and implement, assuming you can get your LDAP server to work.  It really just entails modifying cp/cp.login.php and has been working great for us so far.

    Based on the Active Directory membership, it will generate and/or update the user based on LDAP information and associate it to the right membership level and then login the user.

    Even though we have CAS to run authentication, CASified apps are still responsible for making sure access rights are available., we have EE checking our LDAP server (Active Directory) to make sure the user is a member of the CMS group to enter new items. We do this by running sAMAccountName filter and the group:

    e.g,

    $result = ldap_search($ad, "DC=ad,DC=companyname,DC=com", "(&(objectClass=person)(sAMAccountName=$username)(memberOf=CN=Company CMS,OU=Web Services,OU=Groups,DC=ad,DC=companyname,DC=com))");

    CAS is called “Central Authentication Service,” which enables single sign-in and sign-out by issuing tickets similar to Kerberos.  Many universities and companies are using to reduce login overhead as well as implement safer security.

    The major benefit for us is that if an employee’s relationship with the company is terminated, our accounting system will disable the AD account and the former employee no longer has access to anything, making authentication simple.

    It will also enforce single sign off, so if you sign off from any one service, the CAS server will issue callbacks to log all other sessions out. We’ve been using the rubycas module for our Salesperson frontend database (we don’t want them to have direct posting access to our accounting system, so we built a web-based Sales Order Processing interface). It is also hookable with e-mail servers such as Zimbra.  We have CASified Highrise (internal Highrise URL + OpenID), Zimbra, Dynamics GP, Wiki, as well as a few other internal apps.

    The other huge benefit with CAS is that if your company operates multiple domain names in a totally different namespace (e.g, abc.com and def.com) and cannot have cookie sharing, CAS makes it easy for you to make authentication seamless.  So-called unified authentication systems are incapable of maintaining single sign-on through different namespaces. CAS requires either Java or Ruby. No implementation is available in PHP yet. Chances are, if your company doesn’t run Java, you probably don’t need it yet.  If you have Peoplesoft apps like we do, you probably will have access to Java EE.

    I’ll make this code available under BSD. Let me know what you think and if you think it would be useful for this kind of thing to be maintained. I’ll also see if I can make it hookable as a plugin or something.

    Jason

    Moved to HowTo by Moderator

  • #2 / Mar 20, 2009 4:01pm

    Sue Crocker

    26054 posts

    Welcome to the ExpressionEngine forums, Jason.

    I’m going to move this to the General forum, so that others in the community can comment.

  • #3 / Mar 22, 2009 11:01pm

    Michael Rog

    179 posts

    Yes, I’d be interested in seeing your code! I’m trying to get an installation of EE tied in to our university CAS.

  • #4 / Mar 22, 2009 11:06pm

    Michael Rog

    179 posts

    Also, what keeps this from being integrated as an extension using the cp_member_login and cp_member_logout hooks?

  • #5 / Mar 23, 2009 1:16am

    Jason Hung

    6 posts

    Michael,

    The problem is that I’m really not sure how to turn this into a module yet… I haven’t developed much code for EE yet, so you are more than welcome to try… I will also warn you that this is a hack and is not Don’t Repeat Yourself (DRY) ... so if you’re a coder who likes cleanliness, well ... PLEASE IMPROVE 😉 ... The reason I’m not sure you’ll be able to change this without directly hacking the code is because you want to change the default case action to do the CAS auth instead of showing a login form, I’ve only figured out how to do it this way.

    Download CAS 1.0.1 and install to system_directory/cp/.

    In cp.login.php, in function Login()

    Find:

    switch($IN->GBL('M'))
            {
                case 'auth'            : $this->authenticate();
                    break;

    Insert below:

    case 'auth_sso'      : $this->authenticate_sso();
                    break;

    Change:

    default                : $this->login_form();
                    break;

    to:

    default                : $this->authenticate_sso();
                    break;

    Add the following code below function authenticate():

  • #6 / Mar 23, 2009 1:18am

    Jason Hung

    6 posts

    ADD THE FOLLOWING PART 1:

    [code]
        function authenticate_sso()
        {
            global $IN, $DSP, $LANG, $SESS, $PREFS, $OUT, $LOC, $FNS, $REGX, $LOG, $DB, $EXT, $STAT;
    
            /** ----------------------------------------
            /**  No username/password?  Bounce them…
            /** ----------------------------------------*/
            
            include_once('CAS-1.0.1/CAS-1.0.1/CAS.php');
    
            phpCAS::setDebug();
            phpCAS::client(CAS_VERSION_2_0,'CASLOGIN.COMPANYNAME.COM',443,'');
            phpCAS::setNoCasServerValidation();
            phpCAS::setFixedServiceURL    ("https://www.companyname.com/system_secret/index.php?S=0&C=login&M=auth_sso");
            phpCAS::handleLogoutRequests(true, array("CASLOGINSERVER1.COMPANYNAME.COM")); // if you want Single Sign Out
            phpCAS::forceAuthentication();
            phpCAS::isAuthenticated();
            phpCAS::checkAuthentication();
            
            /** ----------------------------------------
            /**  Check Active Directory for User
            /** ----------------------------------------*/
            
            $ad = ldap_connect("ldap://ACTIVEDIRECTORY.COMPANYNAME.COM");
            
            ldap_set_option($ad, LDAP_OPT_PROTOCOL_VERSION, 3);
            ldap_set_option($ad, LDAP_OPT_REFERRALS, 0);
            
            $bd = ldap_bind($ad,"[email protected]","CASPASSWORD")
                or die("Couldn't bind to AD!");
            $username = phpCAS::getUser();
    
            $result = ldap_search($ad, "DC=ACTIVEDIRECTORY,DC=COMPANYNAME,DC=COM", "(&(objectClass=person)(sAMAccountName=$username)(memberOf=CN=CAS Admins,OU=Web Services,OU=Groups,DC=ACTIVEDIRECTORY,DC=COMPANYNAME,DC=COM))");
        
            $entries = ldap_get_entries($ad, $result);
    
            if ($entries["count"] == 1) {
                
                $valid_user = TRUE;
                
                $sql = "SELECT exp_members.password, exp_members.unique_id, exp_members.member_id, exp_members.group_id, exp_member_groups.can_access_cp
                        FROM   exp_members, exp_member_groups
                        WHERE  username = '".$DB->escape_str(phpCAS::getUser())."'
                        AND    exp_member_groups.site_id = '".$DB->escape_str($PREFS->ini('site_id'))."'
                        AND    exp_members.group_id = exp_member_groups.group_id";
    
                $query = $DB->query($sql);
    
                if ($query->num_rows == 0)
                {
                    $entry = ldap_first_entry($ad, $result);
    
                    $first_name = ldap_get_values($ad, $entry, "givenName");
                    $first_name = $first_name[0];
                    
                    $last_name = ldap_get_values($ad, $entry, "sn");
                    $last_name = $last_name[0];
                    
                    $mail = ldap_get_values($ad, $entry, "mail");
                    $mail = $mail[0];
                    
                    $data['username']            = phpCAS::getUser();
                    $data['password']            = $FNS->hash(stripslashes('SECRETSECRETSECRET'));
                    $data['group_id']            = "6";
                    $data['language']            = "english";
                    $data['timezone']            = "UM7";
                    $data['time_format']        = "us";
                    $data['daylight_savings']    = "n";
                    $data['ip_address']            = $IN->IP;
                    $data['join_date']            = $LOC->now;
                    $data['email']               = $mail;
                    $data['unique_id']             = $FNS->random('encrypt');
                    $data['screen_name']         = $first_name . " " . $last_name;
                    
                    $DB->query($DB->insert_string('exp_members', $data));
                    $member_id = $DB->insert_id;
                    
                    // Create a record in the custom field table
    
                    $DB->query($DB->insert_string('exp_member_data', array('member_id' => $member_id)));
    
                    // Create a record in the member homepage table
    
                    $DB->query($DB->insert_string('exp_member_homepage', array('member_id' => $member_id)));
            
                    $STAT->update_member_stats();
                        
                }
            }
            
            if (! isset($valid_user)) {
                    return $this->login_form($LANG->line('unauthorized_request') . ". Please make sure that you are <a >subscribed</a> to the service before using. "); // tell users to signup for the service by going to self subscribe. we'll add this service onto self subscribe and require that they they're in the marketing department.
            }
            
            /* -------------------------------------------
            /* 'login_authenticate_start' hook.
            /*  - Take control of CP authentication routine
            /*  - Added EE 1.4.2
            */
                $edata = $EXT->call_extension('login_authenticate_start');
                if ($EXT->end_script === TRUE) return;
            /*
            /* -------------------------------------------*/
  • #7 / Mar 23, 2009 1:18am

    Jason Hung

    6 posts

    ADD THE FOLLOWING PART 2:

    /** ----------------------------------------
            /**  Fetch member data
            /** ----------------------------------------*/
    
            $sql = "SELECT exp_members.password, exp_members.unique_id, exp_members.member_id, exp_members.group_id, exp_member_groups.can_access_cp
                    FROM   exp_members, exp_member_groups
                    WHERE  username = '".$DB->escape_str($username)."'
                    AND    exp_member_groups.site_id = '".$DB->escape_str($PREFS->ini('site_id'))."'
                    AND    exp_members.group_id = exp_member_groups.group_id";
                    
            $query = $DB->query($sql);
            
            
            /** ----------------------------------------
            /**  Invalid Username
            /** ----------------------------------------*/
    
            if ($query->num_rows == 0)
            {
                $SESS->save_password_lockout();
            
                return $this->login_form($LANG->line('no_username'));
            }
            
            
            /** ----------------------------------------
            /**  Is the user banned?
            /** ----------------------------------------*/
            
            // Super Admins can't be banned
            
            if ($query->row['group_id'] != 1)
            {
                if ($SESS->ban_check())
                {
                    return $OUT->fatal_error($LANG->line('not_authorized'));
                }
            }
            
            /** ----------------------------------------
            /**  Is user allowed to access the CP?
            /** ----------------------------------------*/
            
            if ($query->row['can_access_cp'] != 'y')
            {
                return $this->login_form($LANG->line('not_authorized'));        
            }
            
            /** --------------------------------------------------
            /**  Do we allow multiple logins on the same account?
            /** --------------------------------------------------*/
            
            if ($PREFS->ini('allow_multi_logins') == 'n')
            {
                // Kill old sessions first
            
                $SESS->gc_probability = 100;
                
                $SESS->delete_old_sessions();
            
                $expire = time() - $SESS->session_length;
                
                // See if there is a current session
    
                $result = $DB->query("SELECT ip_address, user_agent 
                                      FROM   exp_sessions 
                                      WHERE  member_id  = '".$query->row['member_id']."'
                                      AND    last_activity > $expire");
                                    
                // If a session exists, trigger the error message
                                   
                if ($result->num_rows == 1)
                {
                    if ($SESS->userdata['ip_address'] != $result->row['ip_address'] || 
                        $SESS->userdata['user_agent'] != $result->row['user_agent'] )
                    {
                        return $this->login_form($LANG->line('multi_login_warning'));                            
                    }               
                } 
            }  
            
            
            /** ----------------------------------------
            /**  Set cookies
            /** ----------------------------------------*/
            $expire = 0;
            $password = $FNS->hash('SECRETSECRETSECRET'); // I WAS PULLING MY HAIR OFF ON THIS ONE. APPARENTLY, you also have to set the hashed cookie value as a cookie.
            
                $FNS->set_cookie($SESS->c_expire , time()+$expire, $expire);
                $FNS->set_cookie($SESS->c_uniqueid , $query->row['unique_id'], $expire);       
                $FNS->set_cookie($SESS->c_password , $password,  $expire);   
                $FNS->set_cookie($SESS->c_anon , 1,  $expire);
            
                $FNS->set_cookie('cp_last_site_id', '1', 0);
            
            /** ----------------------------------------
            /**  Create a new session
            /** ----------------------------------------*/
    
            $session_id = $SESS->create_new_session($query->row['member_id'], TRUE);
            
            // -------------------------------------------
            // 'cp_member_login' hook.
            //  - Additional processing when a member is logging into CP
            //
                $edata = $EXT->call_extension('cp_member_login', $query->row);
                if ($EXT->end_script === TRUE) return;
            //
            // -------------------------------------------
                   
            /** ----------------------------------------
            /**  Log the login
            /** ----------------------------------------*/
            
            // We'll manually add the username to the Session array so
            // the LOG class can use it.
            $SESS->userdata['username']  = $username;
            
            $LOG->log_action($LANG->line('member_logged_in'));
            
            /** ----------------------------------------
            /**  Delete old password lockouts
            /** ----------------------------------------*/
            
            $SESS->delete_password_lockout();
    
            /** ----------------------------------------
            /**  Redirect the user to the CP home page
            /** ----------------------------------------*/
    
            $return_path = $REGX->decode_qstr('index.php'.'?S='.$session_id);
            
            $_SESSION["ee_session_id"] = $session_id;
            
            $_SESSION['isLoggedIn'] = true; // for LG File Manager
            $_SESSION['user'] = phpCAS::getUser(); // for LG File Manager
            
            $FNS->redirect($return_path);
            exit;    
        }
        /* END */
  • #8 / Mar 23, 2009 1:19am

    Jason Hung

    6 posts

    For Single Sign-Out to work properly, you will need to modify one file in CAS-1.0.1/CAS-1.0.1/CAS/client.php:

    Find:

    // Extract the ticket from the SAML Request
            preg_match("|<samlp:SessionIndex>(.*)</samlp:SessionIndex>|", $_POST['logoutRequest'], $tick, PREG_OFFSET_CAPTURE, 3);
            $wrappedSamlSessionIndex = preg_replace('|<samlp:SessionIndex>|','',$tick[0][0]);
            $ticket2logout = preg_replace('|</samlp:SessionIndex>|','',$wrappedSamlSessionIndex);
            phpCAS::log("Ticket to logout: ".$ticket2logout);
            $session_id = preg_replace('/[^\w]/','',$ticket2logout);
            phpCAS::log("Session id: ".$session_id);
    
                    
            // fix New session ID
            session_id($session_id);
            $_COOKIE[session_name()]=$session_id;
            $_GET[session_name()]=$session_id;
            
            // Overwrite session
            session_start();

    Add Below:

    phpCAS::log("EE Session id: ". $_SESSION["ee_session_id"]);
            
            $conn = mysql_connect("localhost", "eedatabase", "password");
            mysql_select_db("ee_database");
    
            $result = mysql_query("DELETE FROM exp_sessions WHERE session_id = '". $_SESSION["ee_session_id"] ."'");

    Then, single sign-out should work beautifully. So if your users sign-out from Peoplesoft, you should also be booted off from everything you logged into through CAS. Make sure cookies expires as required by your organization or it’ll be pointless to have CAS integrated.

    Hopefully this helps. If you can turn this into a module, be my guest.  Otherwise, this has worked for me.

.(JavaScript must be enabled to view this email address)

ExpressionEngine News!

#eecms, #events, #releases