University of La Verne Single-SignOn Project
How this Single-SignOn thing is built, the requirements, and all the gotchas.
Kenny Katzgrau, August 25, 2008 Contents:
Pre-requisites
Overview of ULV Project
How is this project set up?
o What is SimpleSAMLphp?
o What is Zend?
o What does LTech’s code do?
o Major Function documentation o The LTech Hook/Patch
o Where are actual page layouts are kept?
The flow of application usage
Configuring
Debugging
Server Prerequisites
PHP gotchas
Prerequisites:
Before you keep reading, it’s pretty important to have a good grip on what single sign-on is, and also what SAML is all about. Because this document is specifically about the ULV project, I won’t get into it here. You can read more about SSO at:
http://www.ltech.com/google-apps-solutions/single-sign-on
http://code.google.com/apis/apps/sso/saml_reference_implementation.html
Here is a quick diagram of the ULV SSO/Sync Application. Number 3 is what we deal with in this document.
Overview of the University of La Verne Project:
When Google sends our SSO application a SignOn request, it comes with a SAML request. In a regular SSO application, the app would ask the user to enter their login information, the app would authenticate them with the client’s LDAP directory, and then the app would sign the SAML request and send them back to Google, and they would be logged in.
For ULV, things happen differently. When the user enters their information, the app is required to:
1. Check to see if the user exists
2. Check to see if their password is expired ( hasn’t been changed in 180 days). If it is expired, force them to change it (We supply that functionality too).
3. Check to see that the user hasn’t made 3 consecutive failed logins (if they did, lock them out). If they are locked out, keep them out for 30 minutes.
4. Check to see if the User/Password “Binds” (Logs In) to the LDAP directory.
If the user passes all these checks, we sign their SAML request, and they mosey along to Google.
If in the event of something else occurring:
The username doesn’t even exist:
Load up the login page, and just display the “Username/Password combination does not exist”
message.
The password is expired:
Checking if the password is expired requires looking up the username (which we now confirmed DOES exist), and checking the appropriate LDAP attribute to see if this user needs to change their password. If they need to, redirect them to “Change Password” Page.
To change the password, make the user enter their old password and new password. Then make sure the new password passes the rules required by La Verne:
It has a number in it
It has an upper-case letter in it
It has a lower-case letter in it
It is not one of their last 10 passwords
Checking the first 3 rules is easy, checking the last requires an LDAP lookup of that user, getting the appropriate attribute, and checking for matches.
When we pass all the requirements, we change the password in both LDAP, and Google Apps.
This is discussed in a bit.
The user is locked out:
If the user fails to log in, log that timestamp in the appropriate attribute in their LDAP record, and increment a “fail count” in their record as well.
If the user has made 3 or more consecutive failed login attempts, display message telling them they have been locked out for 30 minutes.
If 30 minutes has passed since their last failed login, allow an additional login attempt. If they succeed, reset the failed logins count and delete the attribute which holds the failed login time.
Read this for the official requirements from ULV:
https://ltechconsulting.projectpath.com/projects/2277415/file/18666507/ULV%20-
%20Requirements.doc
Overview of the University of La Verne Project:
Before I get into the flow of the project, let’s talk about the major code bases in it.
What is SimpleSAMLphp?
SimpleSAML php is a library whose whole purpose is to provide a SingleSignOn service. It natively supports talking to an LDAP directory, authenticating the user, and sending them back.
Because of the extended requirements, there is a lot more needed from LDAP than simple authentication. Not only does the user need to be authenticated, but we have to do everything
mentioned in the previous sections. For this reason, we handle the authentication procedure instead. This is discussed later.
SimpleSAMLphp’s core usefulness is signing the SAML request, but it also acts as the web application. At this point, I want to make it clear that LTech’s code exists to handle custom requirements from ULV.
It’s also important to note that SimpleSAMLphp’s code is half-procedural, half Object-Oriented oriented framework. Modifying SimpleSAML’s core code is not recommended, and doing is not required by any custom requirements. SimpleSAMLphp is recommended by Google, and is the standard for PHP Single Sign on apps. SimpleSAML provides a pure-script alternative to installing a number of dependencies, preventing a portability nightmare
It is extremely unlikely that you will ever have to modify the core SimpleSAMLphp code, except for that mentioned in this document. Everything in the root of the application, except the ULV, LTech, and ChangePassword.php folders/files is SimpleSAMLphp’s. This is its default layout.
There are also several requirements you need to meet to make SimpleSAMLphp work correctly on any server. That is discussed in the Server Prerequisites section.
What is Zend?
Zend is the name of a company that created a very helpful library named Zend_GData. This library can be used to modify the settings of a user account in Google Apps. As long as you have the administrator account name and password, you can use this library to modify any account under his domain. In this SSO App, this library is only used to change a user’s password. You cannot retrieve a password, only change it.
What does LTech’s code do?
LTech’s code is the API that performs all of the important stuff mentioned above. All the
necessary tasks needed for the SSO application have been created. For instance, if you wanted to check whether a user exists, there is a function just to do that. It is unlikely that you will ever need to talk to LDAP directly (although I have tried to make it as easy as possible if you want to).
Major Function Documentation:
LTech’s code is also completely Object-Oriented. There are 3 classes which perform all of the functionality needed at the time of this writing. They are (In order of abstraction):
Class Name File Description
ULV_LdapGdataManager /ULV/LdapGdataManager.php Contains two major functions: Login, and Change Password.
Login: Does all necessary checking of rules on
passwords, and changes passwords in LDAP and Google Apps. One-Stop shopping for performing a full login. Returns a
constant depending on what the status was (define in the class).
ULV_UserDirectory /ULV/UserDirectory Contains all the major functions that
ULV_LdapGdataManager uses. You use
ULV_UserDirectory to do any interaction with LDAP.
It has many functions, which are documented in a bit.
LTech_Ldap /LTech/Ldap.php Is the actual object used by
ULV_UserDirectory to talk to LDAP. This class is completely reusable in any PHP-Ldap application. It is highly recommended to use this Kenny Katzgrau
innovation instead of the low level PHP Ldap API.
There are also 2 utility classes used:
Class Name File Description
ULV_Utilities /ULV/Utilities.php Contains utility functions such as
Encryption/Decryption, validating passwords, redirecting users to pages, converting LDAP
Timestamps, loading templates, etc. Depends on LTech_Utilities.
LTech_Utilities_String /LTech/Utilities/String.php Contains reusable functions for dealing with strings that aren’t built in to PHP. For example, does this string have a number in it? Used by ULV_Utilities.
There is 1 Configuration File:
Class Name File Description
ULV_Config /ULV/Config Contains all the
configuration constants used by the application.
This file is one-stop shopping for configuration changes. Look it over and see all the awesome stuff you can change on the fly.
As you may have been able to tell, both Zend and LTech’s code follow a very useful naming convention. Class names are somewhat of a C# namespace. If a class is kept in
“/Path/To/Class.php”, the actual class name is made Path_To_Class. This is important, because in a PHP app, it isn’t always obvious where a class is coming from.
Another readability design used is naming functions and variables with occasionally long, but highly descriptive names. Take the ULV_UserDirectories’ doesUserExist( $username ) function.
Can you guess what it does?
LTech’s Code performs the following functions, in the following files (only major functions are mentioned).
ULV_LdapGdataManager
Function Description
ChangePassword
( $userId , $newPassword,
$oldPassword )
Checks the new password for validity, changes the Ldap
password, and then changes the Google apps password. Returns a status constant:
Password_Changed
Password_UnChanged_Invalid_Login Password_UnChanged_NoUser Password_UnChanged_Reqs Password_UnChanged_History Password_UnChanged_Fail Login( $username,
$password)
Authenticates the user against LDAP, returns a status constant:
Login_User_Failed Login_Expired Login_Locked
Login_Password_Failed ULV_UserDirectory
Function Description
__construct Creates a new UserDirectory object, all ready to talk to LDAP.
LDAP connection info is set in the config.
addToUserPasswordHistory(
$username, $password )
Adds a passwords to the specified user’s history of used passwords. (That attribute is in the config)
changeUserPassword(
$username, $password )
Changes a user’s LDAP password, with no checking, validation, or anything.
getUserInfoByUsername(
$username )
Gets a user’s entire LDAP record.
getUserAttribute( $username,
$attribute, $indexValue = -1 )
Gets a specific attribute from a user’s LDAP record.
getFirstUserAttributeValue(
$username, $attribute )
Gets the first value of a potentially multivalued attribute in a user’s LDAP record.
getLastKnownUserPassword(
$username )
Gets the last user password changed in our system. If the
password was modified outside of our App, we don’t know that.
isCorrectUserPass(
$username, $password )
Attempts a bind against the LDAP directory with the specified username and password. Returns true or false depending on if the user/pass worked.
isPasswordExpired(
$username )
Checks to see is the user’s password is expired beyond the number of days set in the config.
isUserLockedOut (
$username )
Checks to see if the user has failed a login more than the number of times set in the config.
logUserLoginFail(
$username )
Increments the failed login count, sets the lockout time, and adds a timestamp to a failed login date list.
makeDnFromUsername (
$username )
Creates a user’s DN from the dn “template” in the config.
resetFailedLoginCount(
$username )
Guess.
saveAttribute( $username,
$attributeName,
$attributeValue )
Saves an attribute in a user’s directory.
saveAttributes( $username,
$attributesArray )
Saves a user’s attribute, which is specified in a name/value pair array.
doesUserExist( $username ) Checks to see if that user has an entry in the LDAP directory.
getInstance() Gets a running instance of the UserDirectory object. Use this to maintain and use one connection in several different parts of a script.
The LTech Hook/Patch
So where does LTech’s code get included in SimpleSAMLphp?
In /auth/login-auto.php, around line 70.
This is where we got rid of SimpleSAML’s authentication process, and used our own. If everything goes well, and the user was authenticated in LDAP, we get their record in an
$attributes array.
We then create a pseudo record of the form that SimpleSAMLphp was expecting before we made our path, and start to insert the correct information into it, which comes out of our own
$attributes array. This takes place around line 140. The attribute that SimpleSAMLphp uses as the GoogleApp ID is ‘uid’. This can be set in the config.
At this point, I should mention that a user logs into with a username such as “msmith”.
This is the username we use when talking to LDAP. When we talk to Google Apps, we refer to the user as something like Mark.Smith. This name is stored in an attribute. At the time of this writing, the CN attribute of a user is multivalued. CN[0] == “msmith”, and CN[1]
== “Mark.Smith”. This, although, is expected to change due to ULV’s schema change.
After we create our pseudo array, we load it into SimpleSAMLphp, and let it take care of the rest.
Where are the actual page layouts/designs kept?
All page designs and layouts are kept in /templates/default. This is not our choice directory structure, it is SimpleSAML php’s. These files are shown when needed in files such as login- auto.php, around line 70.
The flow of application usage:
1. User browses to http://mail.google.com/a/laverne.edu
2. User is redirected to Laverne’s server. (Which we’ll call mail.laverne.edu/sso) They are sent by Google to mail.laverne.edu/sso/SSOService.php.
3. SimpleSAMLPHP stores information about the SAML request in the session, and forwards the user to mail.laverne.edu/sso/auth/login-auto.php with some crazy
parameters. At this points, our code was not executed yet, because no username/password had been submitted.
4. The user enters his username and password, and submits.
5. login-auto.php is loaded once again. It sees there is a password, and then the LTech patch is executed, calling the ULV_LdapGdataManager::Login( $username, $password)
function. Depending on what that function returns, different things can happen. If the function returns:
a. ULV_LdapGdataManager::Login_User_Failed: Show the page again with a error message.
b. ULV_LdapGdataManager::Login_Expired: Direct the user to the change password page.
c. ULV_LdapGdataManager::Login_Locked: Tell the user they’re locked out.
d. ULV_LdapGdataManager::Login_Password_Failed: Show the page again with an error message. (User_Failed means the user didn’t exist, and this means that the user existed, but the password failed. – Don’t tell them this, though).
If we got this far, then the function must have returned an attribute array for the user (hooray for duck typing!). Now we can proceed with the login procedure mentioned above.
There she is, the whole application in 5 steps, and 7 pages. But there’s more!
Configuring:
All configurations for this SSO application are performed through the ULV_Config class, located in /ULV/ULV_Config.php.
Debugging:
Debugging in PHP is a lot like ASP debugging: Print out lines to the page to see what is happening at a given time.
There are a few very helpful things to do when debugging this app:
1. If is seems that you aren’t even connecting the ldap server, the LTech_Ldap class keeps an internal message queue that contains error messages, and stuff like that. If you want to see that status of the ldap object, you can use the PHP print_r function, to print out this array. Print_r prints out the nicely formatted contents of any PHP object. Example:
print_r( $My_User_Directory->ldap->MessageQueue). By the way, the ldap object the ULV_UserDirectory uses is a public property.
2. There are a few “test” that I use to test functions of the ULV_UserDirectory Class. In /tests there is an ldapTest.php file. Open it up, and see the SSO API in action. You may have to change the require_once files at the top to the correct path.
Server Prerequisites:
Here are the required extensions and libraries needed by this app. Claudio knows about installing these, and making them work:
Required - PHP Version >= 5.1.2.
Required - Hashing function Required - ZLib
Required - OpenSSL Required - SimpleXML Required - XML DOM Required - RegEx support Required - LDAP Extension Required - Radius Extension Required - Mcrypt
And https (however that is done)
PHP Gotchas:
Here are some things in PHP that aren’t quite obvious.
PHP uses duck-typing, meaning variables can be treated like string one minute, objects the next, and numbers the next. In fact: 0 == false == “” == null.
If you require a file at the top of a script, keep in mind that PHP always uses the path relative to the originally executing script. If you want to include a file relative to the file you are editing, use: Require_once dirname(__FILE__) . ‘/path/to/file’; __FILE__ is a global php constant containing the name of the current file.
If you have an instantiated object in a variable $myObject, the way to call a function on that object is $myObject->function. This is a lot like C++. If you want to call a static function, you have to use the class name and ::. Example: If I wanted to call
ULV_Utility’s static convertLDAPTimestamp( $value ) function, I would call:
$timestamp = ULV_Utility::convertLDAPTimestamp( $timestamp );. Constants are also treated this way. You will see a lot of ULV_Config::[ConstantName] throughout the app.