Integrating the Auth Component with a bespoke 3rd party Single Sign-On Service in CakePHP
September 14, 2009 by Adam · Leave a Comment
Background
I recently had to develop a marketing portal for a large French car manufacturer. They have a number of internal sites, which are accessed by all internal staff, all sales people at individual dealers, and also external agencies (such as us). They have one central “portal”, which is where a user logs in, and then has links to all of the other portals they have access to. This provides the basis of their “Single Sign-On” Service.
The Single Sign-On Service
Basically, the Single Sign-On Service is all based around this initial “portal” site. Each of the links from this portal to the other sites contained a “token” in the URL, which is an encrypted string containing the userid for the central “users” table, and a timestamp, which is the time the link was generated.
There is then a file (which we don’t have access to) in the server’s include_path which decodes this token, checks the session is valid and looks the user up in the central users table, assuming it was a valid portal token, it then sets all of the user’s details in the session and hands control back to you.
Our task was to integrate this with Cake’s Auth component, so we can cache the users in our local database and do our own application level access control. Using Cake, this was surprisingly easy, once you get past one tiny configuration issue.
Implementation
I got the guys in the webservices department to point the portal link for our site to the domain root. This way we can let Cake handle things automatically…
The first thing we need to do is tell Cake to execute the code inside UsersController::login(). We do that using autoRedirect in the AppController::beforeFilter().
function beforeFilter() {
$this->Auth->autoRedirect = false;
}
This now means Cake won’t try to log you in automatically, but it still redirects you to UsersController::login(), so the token that we had passed gets lost. I initially tried saving this to the Session, but Cake appears to reset this when it redirects to the login function, so I decided to save it in a Cookie instead.
function beforeFilter() {
$this->Auth->autoRedirect = false;
if(!empty($_GET['TOKEN'])) {
$login = array(
'token' => $_GET['TOKEN']
);
$this->Cookie->write('login', $login, false, 60);
}
}
The cookie only lasts 60 sections, as we shouldn’t ever need it for any longer than that.
The next step was the login function itself. We didn’t need to worry about connecting to the central user table, decrypting the token, or worrying about validity, as the login script provided on the server would deal with that.
function login() {
if ($this->Auth->user() == null) {
App::import('vendor', 'do_login', 'do_login/do_login.php');
$user = $this->_register_user();
if($user) {
if($this->Auth->login($user)) {
$this->redirect($this->Auth->redirect());
} else {
// this shouldn't happen if do_login.php does it's job!
}
} else {
// this should only happen if there's an error with the db
}
} else {
$this->redirect($this->Auth->redirect());
}
}
The above code first checks to see if the user is current logged into Cake, if they are, it just allows them to continue back to where they were. This might happen if they accidently close the window, and go back to the central portal to login.
If the user isn’t logged in, the first thing we do is include the provided login script. We can safely assume that once this has run, we have a valid user in the system. Next we call our private _register_user() function, which does a little bit of Cake trickery:
function _register_user() {
$sso_data = $this->Session->read('sso');
$this->Session->del('sso');
if (!empty($sso_data['internal']) && $sso_data['internal'] == true) {
$group = $this->User->Group->findByDefaultFor('internals');
$group = $group['Group']['id'];
$username = $sso_data['ddb_id'] . ".int";
} elseif(!empty($sso_data['agency']) && $sso_data['agency'] == true) {
$group = $this->User->Group->findByDefaultFor('agencies');
$group = $group['Group']['id'];
$username = $sso_data['ddb_id'] . ".agy";
} else {
$group = $this->User->Group->findByDefaultFor('internals');
$group = $group['Group']['id'];
$username = $sso_data['ddb_id'] . ".dlr";
}
$user = array(
'User' => array(
'username' => $username,
'password' => Security::hash($sso_data['ddb_id']),
'firstname' => $sso_data['forename'],
'lastname' => $sso_data['surname'],
'email' => $sso_data['email'],
'group_id' => $group
)
);
if($this->User->findCount(array('User.username' => $username)) == 0) {
$this->User->create();
if($this->User->save($user)) {
return $user;
} else {
return false;
}
} else {
return $user;
}
}
Taking this step by step, the first thing we do is read the user’s details from the Session. We can then safely delete this, as we shouldn’t need it again and don’t really want it there.
The next few lines check to see if the user is a dealer, an internal, or an agency user, and assigns them the appropriate group. We also set them up with a username for use with our Cake App which we know will be constant (central user database record id), and append a string to visually identify what type of user they are (this is just for our ease of use when reading the db).
We can then setup the Cake user array, using our new username and group, and the details from the Single Sign-On service. We use the central user database record id again as the password, it doesn’t matter what this is as at this point we know we already have a valid user, we just want something we can pass to Cake to log the user in. After a quick check to see if the username currently exists, we either save the new user and return the User details, or, we just return the details as they are.
This should leave us with a valid Cake User record in $user, which we can then pass to $this->Auth->login(), as if we were doing an ajax login, and finally redirect the user back to where they were.
One final pitfall – if a Cake login request comes from a 3rd party site, and you have security set to medium or high in config.php, the login will fail. You have to set your security to low for this to work. This isn’t the ideal situation, obviously we would prefer a security setting of “high”, but as our application doesn’t need to check the validity of the user we thought this was acceptable (in this instance).
Related posts: