diff --git a/modules/cas/default-disable b/modules/cas/default-disable new file mode 100644 index 0000000000000000000000000000000000000000..fa0bd82e2df7bd79d57593d35bc53c1f9d3ef71f --- /dev/null +++ b/modules/cas/default-disable @@ -0,0 +1,3 @@ +This file indicates that the default state of this module +is disabled. To enable, create a file named enable in the +same directory as this file. diff --git a/modules/cas/docs/cas.txt b/modules/cas/docs/cas.txt new file mode 100644 index 0000000000000000000000000000000000000000..c928cb7f3418b849282cff7493b0d1044a65f484 --- /dev/null +++ b/modules/cas/docs/cas.txt @@ -0,0 +1,36 @@ +Using the CAS authentication source with simpleSAMLphp +========================================================== + +This is completely based on the original cas authentication, +the only diffrence is this is authentication module and not a script. + +Setting up the CAS authentication module +---------------------------------- + +The first thing you need to do is to enable the cas module: + + touch modules/cas/enable + +Adding a authentication source + +example authsource.php +---------------------------------- + + 'example-cas' => array( + 'cas:CAS', + 'cas' => array( + 'login' => 'https://cas.example.com/login', + 'validate' => 'https://cas.example.com/validate', + 'logout' => 'https://cas.example.com/logout' + ), + 'ldap' => array( + 'servers' => 'ldaps://ldaps.example.be:636/', + 'enable_tls' => true, + 'searchbase' => 'ou=people,dc=org,dc=com', + 'searchattributes' => 'uid', + 'attributes' => array('uid','cn'), + 'priv_user_dn' => 'cn=simplesamlphp,ou=applications,dc=org,dc=com', + 'priv_user_pw' => 'password', + + ), + ), diff --git a/modules/cas/lib/Auth/Source/CAS.php b/modules/cas/lib/Auth/Source/CAS.php new file mode 100644 index 0000000000000000000000000000000000000000..f19a9c484d39b57b7bcdd507031c96688c46b952 --- /dev/null +++ b/modules/cas/lib/Auth/Source/CAS.php @@ -0,0 +1,236 @@ +<?php + +/** + * Authenticate using CAS. + * + * Based on www/auth/login-cas.php by Mads Freek, RUC. + * + * @author Danny Bollaert, UGent. + * @package simpleSAMLphp + * @version $Id$ + */ +class sspmod_cas_Auth_Source_CAS extends SimpleSAML_Auth_Source { + + /** + * The string used to identify our states. + */ + const STAGE_INIT = 'sspmod_cas_Auth_Source_CAS.state'; + + /** + * The key of the AuthId field in the state. + */ + const AUTHID = 'sspmod_cas_Auth_Source_CAS.AuthId'; + + + /** + * @var array with ldap configuration + */ + private $_ldapConfig; + + /** + * @var cas configuration + */ + private $_casConfig; + + /** + * @var cas chosen validation method + */ + private $_validationMethod; + /** + * @var cas login method + */ + private $_loginMethod; + + + /** + * Constructor for this authentication source. + * + * @param array $info Information about this authentication source. + * @param array $config Configuration. + */ + public function __construct($info, $config) { + assert('is_array($info)'); + assert('is_array($config)'); + + /* Call the parent constructor first, as required by the interface. */ + parent::__construct($info, $config); + + if (!array_key_exists('cas', $config)){ + throw new Exception('cas authentication source is not properly configured: missing [cas]'); + } + + if (!array_key_exists('ldap', $config)){ + throw new Exception('ldap authentication source is not properly configured: missing [ldap]'); + } + + $this->_casConfig = $config['cas']; + $this->_ldapConfig = $config['ldap']; + + if(isset($this->_casConfig['serviceValidate'])){ + $this->_validationMethod = 'serviceValidate'; + }elseif(isset($this->_casConfig['validate'])){ + $this->_validationMethod = 'validate'; + }else{ + throw new Exception("validate or serviceValidate not specified"); + } + + if(isset($this->_casConfig['login'])){ + $this->_loginMethod = $this->_casConfig['login']; + }else{ + throw new Exception("cas login url not specified"); + } + } + + + /** + * This the most simple version of validating, this provides only authentication validation + * + * @param string $ticket + * @param string $service + * @return list username and attributes + */ + private function casValidate($ticket, $service){ + $url = SimpleSAML_Utilities::addURLparameter($this->_casConfig['validate'], array( + 'ticket' => $ticket, + 'service' => $service, + )); + $result = file_get_contents($url); + $res = preg_split("/\r?\n/",$result); + + if (strcmp($res[0], "yes") == 0) { + return array($res[1], array()); + } else { + throw new Exception("Failed to validate CAS service ticket: $ticket"); + } + } + + + /** + * Uses the cas service validate, this provides additional attributes + * + * @param string $ticket + * @param string $service + * @return list username and attributes + */ + private function casServiceValidate($ticket, $service){ + $url = SimpleSAML_Utilities::addURLparameter($this->_casConfig['serviceValidate'], array( + 'ticket' => $ticket, + 'service' => $service, + )); + $result = file_get_contents($url); + + $dom = DOMDocument::loadXML($result); + $xPath = new DOMXpath($dom); + $xPath->registerNamespace("cas", 'http://www.yale.edu/tp/cas'); + $success = $xPath->query("/cas:serviceResponse/cas:authenticationSuccess/cas:user"); + if ($success->length == 0) { + $failure = $xPath->evaluate("/cas:serviceResponse/cas:authenticationFailure"); + throw new Exception("Error when validating CAS service ticket: " . $failure->item(0)->textContent); + } else { + + $attributes = array(); + if ($casattributes = $this->_casConfig['attributes']) { # some has attributes in the xml - attributes is a list of XPath expressions to get them + foreach ($casattributes as $name => $query) { + $attrs = $xPath->query($query); + foreach ($attrs as $attrvalue) $attributes[$name][] = $attrvalue->textContent; + } + } + $casusername = $success->item(0)->textContent; + + return array($casusername, $attributes); + + } + } + + + /** + * Main validation method, redirects to correct method + * (keeps finalStep clean) + * + * @param string $ticket + * @param string $service + * @return list username and attributes + */ + private function casValidation($ticket, $service){ + switch($this->_validationMethod){ + case 'validate': + return $this->casValidate($ticket, $service); + break; + case 'serviceValidate': + return $this->casServiceValidate($ticket, $service); + break; + default: + throw new Exception("validate or serviceValidate not specified"); + } + } + + + /** + * Called by linkback, to finish validate/ finish logging in. + * @param state $state + * @return list username, casattributes/ldap attributes + */ + public function finalStep(&$state) { + + + $ticket = $state['cas:ticket']; + $stateID = SimpleSAML_Auth_State::saveState($state, self::STAGE_INIT); + $service = SimpleSAML_Module::getModuleURL('cas/linkback.php', array('stateID' => $stateID)); + list($username, $casattributes) = $this->casValidation($ticket, $service); + $ldapattributes = array(); + if ($this->_ldapConfig['servers']) { + $ldap = new SimpleSAML_Auth_LDAP($this->_ldapConfig['servers'], $this->_ldapConfig['enable_tls']); + $ldapattributes = $ldap->validate($this->_ldapConfig, $username); + } + $attributes = array_merge_recursive($casattributes, $ldapattributes); + $state['Attributes'] = $attributes; + + SimpleSAML_Auth_Source::completeAuth($state); + } + + + /** + * Log-in using cas + * + * @param array &$state Information about the current authentication. + */ + public function authenticate(&$state) { + assert('is_array($state)'); + + /* We are going to need the authId in order to retrieve this authentication source later. */ + $state[self::AUTHID] = $this->authId; + + $stateID = SimpleSAML_Auth_State::saveState($state, self::STAGE_INIT); + + + + $serviceUrl = SimpleSAML_Module::getModuleURL('cas/linkback.php', array('stateID' => $stateID)); + + SimpleSAML_Utilities::redirect($this->_loginMethod, array( + 'service' => $serviceUrl)); + } + + + /** + * Log out from this authentication source. + * + * This function should be overridden if the authentication source requires special + * steps to complete a logout operation. + * + * If the logout process requires a redirect, the state should be saved. Once the + * logout operation is completed, the state should be restored, and completeLogout + * should be called with the state. If this operation can be completed without + * showing the user a page, or redirecting, this function should return. + * + * @param array &$state Information about the current logout operation. + */ + public function logout(&$state) { + assert('is_array($state)'); + $logoutUrl = $this->_casConfig['logout']; + + SimpleSAML_Auth_State::deleteState($state); + // we want cas to log us out + SimpleSAML_Utilities::redirect($logoutUrl, array()); + } + +} diff --git a/modules/cas/www/linkback.php b/modules/cas/www/linkback.php new file mode 100644 index 0000000000000000000000000000000000000000..1e6740c0bdcc6ba9d2190c1656a0642aa821ce85 --- /dev/null +++ b/modules/cas/www/linkback.php @@ -0,0 +1,30 @@ +<?php + +/** + * Handle linkback() response from CAS. + */ + +if (!isset($_GET['stateID'])) { + throw new SimpleSAML_Error_BadRequest('Missing stateID parameter.'); +} +$stateId = (string)$_GET['stateID']; + +if (!isset($_GET['ticket'])) { + throw new SimpleSAML_Error_BadRequest('Missing ticket parameter.'); +} + +$state = SimpleSAML_Auth_State::loadState($stateId, sspmod_cas_Auth_Source_CAS::STAGE_INIT); +$state['cas:ticket'] = (string)$_GET['ticket']; + +/* Find authentication source. */ +assert('array_key_exists(sspmod_cas_Auth_Source_CAS::AUTHID, $state)'); +$sourceId = $state[sspmod_cas_Auth_Source_CAS::AUTHID]; + +$source = SimpleSAML_Auth_Source::getById($sourceId); +if ($source === NULL) { + throw new Exception('Could not find authentication source with id ' . $sourceId); +} + +$source->finalStep($state); + +