View source
<?php
namespace Drupal\cas\Service;
use Drupal\cas\CasServerConfig;
use Drupal\cas\Event\CasPostValidateEvent;
use Drupal\cas\Event\CasPreValidateEvent;
use Drupal\cas\Event\CasPreValidateServerConfigEvent;
use Drupal\cas\Exception\CasValidateException;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Drupal\cas\CasPropertyBag;
use Psr\Log\LogLevel;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class CasValidator {
protected $httpClient;
protected $casHelper;
protected $eventDispatcher;
protected $settings;
protected $urlGenerator;
public function __construct(Client $http_client, CasHelper $cas_helper, ConfigFactoryInterface $config_factory, UrlGeneratorInterface $url_generator, EventDispatcherInterface $event_dispatcher) {
$this->httpClient = $http_client;
$this->casHelper = $cas_helper;
$this->settings = $config_factory
->get('cas.settings');
$this->urlGenerator = $url_generator;
$this->eventDispatcher = $event_dispatcher;
}
public function validateTicket($ticket, array $service_params = []) {
$casServerConfig = CasServerConfig::createFromModuleConfig($this->settings);
$event = new CasPreValidateServerConfigEvent($casServerConfig);
$this->eventDispatcher
->dispatch(CasHelper::EVENT_PRE_VALIDATE_SERVER_CONFIG, $event);
$path = '';
switch ($casServerConfig
->getProtocolVerison()) {
case "1.0":
$path = 'validate';
break;
case "2.0":
if ($this->settings
->get('proxy.can_be_proxied')) {
$path = 'proxyValidate';
}
else {
$path = 'serviceValidate';
}
break;
case "3.0":
if ($this->settings
->get('proxy.can_be_proxied')) {
$path = 'p3/proxyValidate';
}
else {
$path = 'p3/serviceValidate';
}
break;
}
$params = [];
$params['service'] = $this->urlGenerator
->generate('cas.service', $service_params, UrlGeneratorInterface::ABSOLUTE_URL);
$params['ticket'] = $ticket;
if ($this->settings
->get('proxy.initialize')) {
$params['pgtUrl'] = $this
->formatProxyCallbackUrl();
}
$pre_validate_event = new CasPreValidateEvent($path, $params);
$this->eventDispatcher
->dispatch(CasHelper::EVENT_PRE_VALIDATE, $pre_validate_event);
$validate_url = $casServerConfig
->getServerBaseUrl() . $pre_validate_event
->getValidationPath();
if (!empty($pre_validate_event
->getParameters())) {
$validate_url .= '?' . UrlHelper::buildQuery($pre_validate_event
->getParameters());
}
$this->casHelper
->log(LogLevel::DEBUG, 'Attempting to validate service ticket %ticket by making request to URL %url', [
'%ticket' => $ticket,
'%url' => $validate_url,
]);
try {
$response = $this->httpClient
->get($validate_url, $casServerConfig
->getCasServerGuzzleConnectionOptions());
$response_data = $response
->getBody()
->__toString();
$this->casHelper
->log(LogLevel::DEBUG, "Validation response received from CAS server: %data", [
'%data' => $response_data,
]);
} catch (RequestException $e) {
throw new CasValidateException("Error with request to validate ticket: " . $e
->getMessage());
}
$protocol_version = $casServerConfig
->getProtocolVerison();
switch ($protocol_version) {
case "1.0":
$cas_property_bag = $this
->validateVersion1($response_data);
break;
case "2.0":
case "3.0":
$cas_property_bag = $this
->validateVersion2($response_data);
break;
}
if (empty($cas_property_bag)) {
throw new CasValidateException('Unknown CAS protocol version specified: ' . $protocol_version);
}
$event = new CasPostValidateEvent($response_data, $cas_property_bag);
$this->eventDispatcher
->dispatch(CasHelper::EVENT_POST_VALIDATE, $event);
return $event
->getCasPropertyBag();
}
private function validateVersion1($data) {
if (preg_match('/^no\\n/', $data)) {
throw new CasValidateException("Ticket did not pass validation.");
}
elseif (!preg_match('/^yes\\n/', $data)) {
throw new CasValidateException("Malformed response from CAS server.");
}
$arr = preg_split('/\\n/', $data);
$user = trim($arr[1]);
$this->casHelper
->log(LogLevel::DEBUG, "Extracted username %user from validation response data.", [
'%user' => $user,
]);
return new CasPropertyBag($user);
}
private function validateVersion2($data) {
$dom = new \DOMDocument();
$dom->preserveWhiteSpace = FALSE;
$dom->encoding = "utf-8";
if (@$dom
->loadXML($data) === FALSE) {
throw new CasValidateException("XML from CAS server is not valid.");
}
$failure_elements = $dom
->getElementsByTagName('authenticationFailure');
if ($failure_elements->length > 0) {
$failure_element = $failure_elements
->item(0);
$error_code = $failure_element
->getAttribute('code');
$error_msg = $failure_element->nodeValue;
throw new CasValidateException("Error Code " . trim($error_code) . ": " . trim($error_msg));
}
$success_elements = $dom
->getElementsByTagName("authenticationSuccess");
if ($success_elements->length === 0) {
throw new CasValidateException("XML from CAS server is not valid.");
}
$success_element = $success_elements
->item(0);
$user_element = $success_element
->getElementsByTagName("user");
if ($user_element->length == 0) {
throw new CasValidateException("No user found in ticket validation response.");
}
$username = $user_element
->item(0)->nodeValue;
$this->casHelper
->log(LogLevel::DEBUG, "Extracted username %user from validation response.", [
'%user' => $username,
]);
$property_bag = new CasPropertyBag($username);
$attribute_elements = $dom
->getElementsByTagName("attributes");
if ($attribute_elements->length > 0) {
$property_bag
->setAttributes($this
->parseAttributes($attribute_elements));
}
$proxy_chain = $success_element
->getElementsByTagName("proxy");
if ($this->settings
->get('proxy.can_be_proxied') && $proxy_chain->length > 0) {
$this
->verifyProxyChain($proxy_chain);
}
if ($this->settings
->get('proxy.initialize')) {
$pgt_element = $success_element
->getElementsByTagName("proxyGrantingTicket");
if ($pgt_element->length == 0) {
throw new CasValidateException("Proxy initialized, but no PGTIOU provided in response.");
}
$pgt = $pgt_element
->item(0)->nodeValue;
$this->casHelper
->log(LogLevel::DEBUG, "Extracted PGT %pgt from validation response.", [
'%pgt' => $pgt,
]);
$property_bag
->setPgt($pgt);
}
return $property_bag;
}
private function verifyProxyChain(\DOMNodeList $proxy_chain) {
$allowed_proxy_chains_raw = $this->settings
->get('proxy.proxy_chains');
$allowed_proxy_chains = $this
->parseAllowedProxyChains($allowed_proxy_chains_raw);
$server_chain = $this
->parseServerProxyChain($proxy_chain);
$this->casHelper
->log(LogLevel::DEBUG, "Attempting to verify supplied proxy chain: %chain", [
'%chain' => print_r($server_chain, TRUE),
]);
foreach ($allowed_proxy_chains as $chain) {
if (count($chain) != count($server_chain)) {
continue;
}
$flag = TRUE;
foreach ($chain as $index => $regex) {
if (preg_match('/^\\/.*\\/[ixASUXu]*$/s', $regex)) {
if (!preg_match($regex, $server_chain[$index])) {
$flag = FALSE;
$this->casHelper
->log(LogLevel::DEBUG, "Failed to match %regex with supplied %chain", [
'%regex' => $regex,
'%chain' => $server_chain[$index],
]);
break;
}
}
else {
if (!(strncasecmp($regex, $server_chain[$index], strlen($regex)) == 0)) {
$flag = FALSE;
$this->casHelper
->log(LogLevel::DEBUG, "Failed to match %regex with supplied %chain", [
'%regex' => $regex,
'%chain' => $server_chain[$index],
]);
break;
}
}
}
if ($flag == TRUE) {
$this->casHelper
->log(LogLevel::DEBUG, "Matched allowed chain: %chain", [
'%chain' => print_r($chain, TRUE),
]);
return;
}
}
throw new CasValidateException("Proxy chain did not match allowed list.");
}
private function parseAllowedProxyChains($proxy_chains) {
$chain_list = [];
$chains = preg_split('/\\v/', $proxy_chains, NULL, PREG_SPLIT_NO_EMPTY);
foreach ($chains as $chain) {
$list = preg_split('/\\s/', $chain, NULL, PREG_SPLIT_NO_EMPTY);
$chain_list[] = $list;
}
return $chain_list;
}
private function parseServerProxyChain(\DOMNodeList $xml_list) {
$proxies = [];
foreach ($xml_list as $node) {
$proxies[] = $node->nodeValue;
}
return $proxies;
}
private function parseAttributes(\DOMNodeList $xml_list) {
$attributes = [];
$node = $xml_list
->item(0);
foreach ($node->childNodes as $child) {
$name = $child->localName;
$value = $child->nodeValue;
$attributes[$name][] = $value;
}
$this->casHelper
->log(LogLevel::DEBUG, "Parsed the following attributes from the validation response: %attributes", [
'%attributes' => print_r($attributes, TRUE),
]);
return $attributes;
}
private function formatProxyCallbackUrl() {
return str_replace('http://', 'https://', $this->urlGenerator
->generateFromRoute('cas.proxyCallback', [], [
'absolute' => TRUE,
]));
}
}