/** * Proxy authenticates to a target service. * * Returns cookies from the proxied service in a * CookieJar object for use when later accessing resources. * * @param string $target_service * The service to be proxied. * * @return \GuzzleHttp\Cookie\CookieJar * A CookieJar object (array storage) containing cookies from the * proxied service. * * @throws CasProxyException */ public function proxyAuthenticate($target_service) { // Check to see if we have proxied this application already. if (isset($_SESSION['cas_proxy_helper'][$target_service])) { $cookies = array(); foreach ($_SESSION['cas_proxy_helper'][$target_service] as $cookie) { $cookies[$cookie['Name']] = $cookie['Value']; } $domain = $cookie['Domain']; $jar = CookieJar::fromArray($cookies, $domain); $this->casHelper->log("{$target_service} already proxied. Returning information from session."); return $jar; } if (!($this->casHelper->isProxy() && isset($_SESSION['cas_pgt']))) { // We can't perform proxy authentication in this state. throw new CasProxyException("Session state not sufficient for proxying."); } // Make request to CAS server to retrieve a proxy ticket for this service. $cas_url = $this->getServerProxyURL($target_service); try { $this->casHelper->log("Retrieving proxy ticket from: {$cas_url}"); $response = $this->httpClient->get($cas_url); $this->casHelper->log("Received: " . htmlspecialchars($response->getBody()->__toString())); } catch (ClientException $e) { throw new CasProxyException($e->getMessage()); } $proxy_ticket = $this->parseProxyTicket($response->getBody()); $this->casHelper->log("Extracted proxy ticket: {$proxy_ticket}"); // Make request to target service with our new proxy ticket. // The target service will validate this ticket against the CAS server // and set a cookie that grants authentication for further resource calls. $params['ticket'] = $proxy_ticket; $service_url = $target_service . "?" . UrlHelper::buildQuery($params); $cookie_jar = new CookieJar(); try { $this->casHelper->log("Contacting service: {$service_url}"); $this->httpClient->get($service_url, ['cookies' => $cookie_jar]); } catch (ClientException $e) { throw new CasProxyException($e->getMessage()); } // Store in session storage for later reuse. $_SESSION['cas_proxy_helper'][$target_service] = $cookie_jar->toArray(); $this->casHelper->log("Stored cookies from {$target_service} in session."); return $cookie_jar; }
/** * Handles a request to either validate a user login or log a user out. * * The path that this controller/action handle are always set to the "service" * when authenticating with the CAS server, so CAS server communicates back to * the Drupal site using this controller. */ public function handle() { $request = $this->requestStack->getCurrentRequest(); // First, check if this is a single-log-out (SLO) request from the server. if ($request->request->has('logoutRequest')) { $this->casHelper->log("Logout request: passing to casLogout::handleSlo"); $this->casLogout->handleSlo($request->request->get('logoutRequest')); // Always return a 200 code. CAS Server doesn’t care either way what // happens here, since it is a fire-and-forget approach taken. return Response::create('', 200); } // Our CAS Subscriber, which implements forced redirect and gateway, will // set this query string param which indicates we should disable the // subscriber on the next redirect. This prevents an infinite redirect loop. if ($request->query->has('cas_temp_disable')) { $this->casHelper->log("Temp disable flag set, set session flag."); $_SESSION['cas_temp_disable'] = TRUE; } // Check if there is a ticket parameter. If there isn't, we could be // returning from a gateway request and the user may not be logged into CAS. // Just redirect away from here. if (!$request->query->has('ticket')) { $this->casHelper->log("No ticket detected, move along."); $this->handleReturnToParameter($request); return RedirectResponse::create($this->urlGenerator->generate('<front>')); } $ticket = $request->query->get('ticket'); // Our CAS service will need to reconstruct the original service URL // when validating the ticket. We always know what the base URL for // the service URL (it's this page), but there may be some query params // attached as well (like a destination param) that we need to pass in // as well. So, detach the ticket param, and pass the rest off. $service_params = $request->query->all(); unset($service_params['ticket']); $cas_version = $this->casHelper->getCasProtocolVersion(); $this->casHelper->log("Configured to use CAS protocol version: {$cas_version}"); try { $cas_validation_info = $this->casValidator->validateTicket($cas_version, $ticket, $service_params); } catch (CasValidateException $e) { // Validation failed, redirect to homepage and set message. $this->setMessage(t('There was a problem validating your login, please contact a site administrator.'), 'error'); $this->handleReturnToParameter($request); return RedirectResponse::create($this->urlGenerator->generate('<front>')); } try { $this->casLogin->loginToDrupal($cas_validation_info, $ticket); if ($this->casHelper->isProxy() && $cas_validation_info->getPgt()) { $this->casHelper->log("Storing PGT information for this session."); $this->casHelper->storePGTSession($cas_validation_info->getPgt()); } $this->setMessage(t('You have been logged in.')); } catch (CasLoginException $e) { $this->setMessage(t('There was a problem logging in, please contact a site administrator.'), 'error'); } $this->handleReturnToParameter($request); return RedirectResponse::create($this->urlGenerator->generate('<front>')); }
/** * Validation of a service ticket for Version 2 of the CAS protocol. * * @param string $data * The raw validation response data from CAS server. * * @return array * An array containing validation result data from the CAS server. * @throws CasValidateException */ private function validateVersion2($data) { $dom = new \DOMDocument(); $dom->preserveWhiteSpace = FALSE; $dom->encoding = "utf-8"; // Suppress errors from this function, as we intend to throw our own // exception. 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) { // Failed validation, extract the message and toss exception. $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) { // All responses should have either an authenticationFailure // or authenticationSuccess node. throw new CasValidateException("XML from CAS server is not valid."); } // There should only be one success element, grab it and extract username. $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("Extracted user: {$username}"); $property_bag = new CasPropertyBag($username); // If the server provided any attributes, parse them out into the property // bag. $attribute_elements = $dom->getElementsByTagName("attributes"); if ($attribute_elements->length > 0) { $property_bag->setAttributes($this->parseAttributes($attribute_elements)); } // Look for a proxy chain, and if it exists, validate it against config. $proxy_chain = $success_element->getElementsByTagName("proxy"); if ($this->casHelper->canBeProxied() && $proxy_chain->length > 0) { $this->verifyProxyChain($proxy_chain); } if ($this->casHelper->isProxy()) { // Extract the PGTIOU from the XML. $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("Extracted PGT: {$pgt}"); $property_bag->setPgt($pgt); } return $property_bag; }
/** * Main point of communication between CAS server and the Drupal site. * * The path that this controller/action handle are always set to the "service" * url when authenticating with the CAS server, so CAS server communicates * back to the Drupal site using this controller action. That's why there's * so much going on in here - it needs to process a few different types of * requests. */ public function handle() { $request = $this->requestStack->getCurrentRequest(); // First, check if this is a single-log-out (SLO) request from the server. if ($request->request->has('logoutRequest')) { try { $this->casLogout->handleSlo($request->request->get('logoutRequest')); } catch (CasSloException $e) { $this->casHelper->log($e->getMessage()); } // Always return a 200 code. CAS Server doesn’t care either way what // happens here, since it is a fire-and-forget approach taken. return Response::create('', 200); } // We will be redirecting the user below. To prevent the CasSubscriber from // initiating an automatic authentiation on the that request (like forced // auth or gateway auth) and potentially creating an authentication loop, // we set a session variable instructing the CasSubscriber skip auto auth // for that request. $request->getSession()->set('cas_temp_disable_auto_auth', TRUE); /* If there is no ticket parameter on the request, the browser either: * (a) is returning from a gateway request to the CAS server in which * the user was not already authenticated to CAS, so there is no * service ticket to validate and nothing to do. * (b) has hit this URL for some other reason (crawler, curiosity, etc) * and there is nothing to do. * In either case, we just want to redirect them away from this controller. */ if (!$request->query->has('ticket')) { $this->casHelper->log("No ticket detected, move along."); $this->handleReturnToParameter($request); return RedirectResponse::create($this->urlGenerator->generate('<front>')); } // There is a ticket present, meaning CAS server has returned the browser // to the Drupal site so we can authenticate the user locally using the // ticket. $ticket = $request->query->get('ticket'); // Our CAS service will need to reconstruct the original service URL // when validating the ticket. We always know what the base URL for // the service URL (it's this page), but there may be some query params // attached as well (like a destination param) that we need to pass in // as well. So, detach the ticket param, and pass the rest off. $service_params = $request->query->all(); unset($service_params['ticket']); try { $cas_validation_info = $this->casValidator->validateTicket($ticket, $service_params); } catch (CasValidateException $e) { // Validation failed, redirect to homepage and set message. $this->casHelper->log($e->getMessage()); $this->setMessage($this->t('There was a problem validating your login, please contact a site administrator.'), 'error'); $this->handleReturnToParameter($request); return RedirectResponse::create($this->urlGenerator->generate('<front>')); } // Now that the ticket has been validated, we can use the information from // validation request to authenticate the user locally on the Drupal site. try { $this->casLogin->loginToDrupal($cas_validation_info, $ticket); if ($this->casHelper->isProxy() && $cas_validation_info->getPgt()) { $this->casHelper->log("Storing PGT information for this session."); $this->casHelper->storePgtSession($cas_validation_info->getPgt()); } $this->setMessage($this->t('You have been logged in.')); } catch (CasLoginException $e) { $this->casHelper->log($e->getMessage()); $this->setMessage($this->t('There was a problem logging in, please contact a site administrator.'), 'error'); } // And finally redirect the user to the homepage, or so a specific // destination found in the destination param (like the page they were on // prior to initiating authentication). $this->handleReturnToParameter($request); return RedirectResponse::create($this->urlGenerator->generate('<front>')); }
/** * Test getting the 'act as proxy' configuration. * * @covers ::isProxy * @covers ::__construct */ public function testIsProxy() { $config_factory = $this->getConfigFactoryStub(array('cas.settings' => array('server.hostname' => 'example.com', 'server.port' => 443, 'server.path' => '/cas', 'proxy.initialize' => TRUE))); $cas_helper = new CasHelper($config_factory, $this->urlGenerator, $this->connection, $this->loggerFactory, $this->session); $this->assertEquals(TRUE, $cas_helper->isProxy()); }