Thursday, July 23, 2009

CAS Authentication in CakePHP

Simple CakePHP Plugin-Component for replacing the standard Cake Auth component with CAS authentication

Jasig's Central Authentication Service (CAS) is a fantastic open source project providing enterprise single sign-on services. We use it here at Pascal Metrics and in order to tie our CakePHP prototypes into it for authentication, I threw together this replacement for the standard CakePHP Auth component.

Usage:
  1. Create a vendors/plugins/cas/controllers/components directory structure in your project.
  2. Paste the code below into the new components directory as a new file named cas.php
  3. Instead of loading the 'Auth' component, load this plugin component as 'Cas.Cas'
  4. Replace all references to the 'Auth' instance in your project with 'Cas'

<?php
include_once('CAS.php');
App::import('Component', 'Auth');
/**
* CAS Component for CakePHP
* @author Eric Simmerman <eric.simmerman @nospam pascalmetrics.com>
* @copyright Pascal Metrics
* @link http://www.cakephp.org CakePHP
* @link http://www.pascalmetrics.com Pascal Metrics, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
class CasComponent extends AuthComponent {

function initialize(&$controller) {
parent::initialize($controller);
//phpCAS::setDebug();
phpCAS::client(CAS_VERSION_2_0,'secure.yourdomain.com',443,'/cas/',false);
phpCAS::setNoCasServerValidation();
}

function logout() {
phpCAS::logout();
parent::logout();
}

function startup(&$controller) {
$methods = array_flip($controller->methods);
$isErrorOrTests = (
strtolower($controller->name) == 'cakeerror' ||
(strtolower($controller->name) == 'tests' && Configure::read() > 0)
);
if ($isErrorOrTests) {
return true;
}

$isMissingAction = (
$controller->scaffold === false &&
!isset($methods[strtolower($controller->params['action'])])
);

if ($isMissingAction) {
return true;
}

if (!$this->__setDefaults()) {
return false;
}

$this->data = $controller->data = $this->hashPasswords($controller->data);
$url = '';

if (isset($controller->params['url']['url'])) {
$url = $controller->params['url']['url'];
}
$url = Router::normalize($url);
$loginAction = Router::normalize($this->loginAction);

$isAllowed = (
$this->allowedActions == array('*') ||
in_array($controller->params['action'], $this->allowedActions)
);

if ($loginAction != $url && $isAllowed) {
return true;
}

if (!$this->user()) { //We are processing a secured page and Cake does not hold Authenticated Session
if(phpCAS::isAuthenticated()){ // CAS does hold authenticated session - use it to authenticate for Cake
$username = phpCAS::getUser();
$model =& $this->getModel();
$conditions = array( $this->userModel.'.'.$this->fields['username'] => $username );
pr($conditions);
$data = $model->find($conditions, null, null, 0);
if (empty($data)) {
// CAS return authenticated true, but username [{$username}] was not found. Load dummy user
$data = $model->findById(-1);
}
$this->Session->write($this->sessionKey, $data[$this->userModel]);
$this->_loggedIn = true;
}else{ // Force CAS authentication
phpCAS::forceAuthentication();
}
}


if (!$this->authorize) {
return true;
}

extract($this->__authType());
switch ($type) {
case 'controller':
$this->object =& $controller;
break;
case 'crud':
case 'actions':
if (isset($controller->Acl)) {
$this->Acl =& $controller->Acl;
} else {
$err = 'Could not find AclComponent. Please include Acl in ';
$err .= 'Controller::$components.';
trigger_error(__($err, true), E_USER_WARNING);
}
break;
case 'model':
if (!isset($object)) {
$hasModel = (
isset($controller->{$controller->modelClass}) &&
is_object($controller->{$controller->modelClass})
);
$isUses = (
!empty($controller->uses) && isset($controller->{$controller->uses[0]}) &&
is_object($controller->{$controller->uses[0]})
);

if ($hasModel) {
$object = $controller->modelClass;
} elseif ($isUses) {
$object = $controller->uses[0];
}
}
$type = array('model' => $object);
break;
}

if ($this->isAuthorized($type)) {
return true;
}

$this->Session->setFlash($this->authError, 'default', array(), 'auth');
$controller->redirect($controller->referer(), null, true);
return false;
}
}
?>

Notes:
  • This implementation is a little ugly because I had to copy/paste a good portion of the startup method from the parent class.

4 comments:

  1. Hi,

    Where did you put the CAS.php and CAS folder? As when I put it under the same folder as the file and I do include_once("CAS.php");

    I get syntax error, unexpected T_INCLUDE_ONCE, expecting T_FUNCTION

    Thanks.

    ReplyDelete
  2. 3en, if you install CAS using pear then it'll be setup in the proper location. Try the following:

    pear install http://www.ja-sig.org/downloads/cas-clients/php/1.0.1/CAS-1.0.1.tgz

    ReplyDelete
  3. Hi Eric
    I am new to cakephp. Could you please explain me i detail how to incorporate CAS code into CakePHP. I mean after including cas.php what are all the changes to be done in cakephp?
    Could you pls. help me?

    ReplyDelete
  4. Eric,

    Thanks for the code. I've CakePHP throws a missing controller when I navigate to a controller that doesn't exist, however, it doesn't throw the typical missing view error when I navigate to a missing view, it just echos a CAS error. Any ideas on how to fix it?

    - Wes

    ReplyDelete