SamlController.php in SAML Authentication 8.2
Same filename and directory in other branches
Namespace
Drupal\samlauth\ControllerFile
src/Controller/SamlController.phpView source
<?php
namespace Drupal\samlauth\Controller;
use Exception;
use Drupal\samlauth\SamlService;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Url;
use Drupal\Core\Utility\Error;
use Drupal\Core\Utility\Token;
use OneLogin_Saml2_Utils;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
/**
* Returns responses for samlauth module routes.
*/
class SamlController extends ControllerBase {
/**
* The samlauth SAML service.
*
* @var \Drupal\samlauth\SamlService
*/
protected $saml;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* A configuration object containing samlauth settings.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* The PathValidator service.
*
* @var \Drupal\Core\Path\PathValidatorInterface
*/
protected $pathValidator;
/**
* The token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* Constructor for Drupal\samlauth\Controller\SamlController.
*
* @param \Drupal\samlauth\SamlService $saml
* The samlauth SAML service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Path\PathValidatorInterface $path_validator
* The PathValidator service.
* @param \Drupal\Core\Utility\Token $token
* The token service.
*/
public function __construct(SamlService $saml, RequestStack $request_stack, ConfigFactoryInterface $config_factory, PathValidatorInterface $path_validator, Token $token) {
$this->saml = $saml;
$this->requestStack = $request_stack;
$this->config = $config_factory
->get('samlauth.authentication');
$this->pathValidator = $path_validator;
$this->token = $token;
}
/**
* Factory method for dependency injection container.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
*
* @return static
*/
public static function create(ContainerInterface $container) {
return new static($container
->get('samlauth.saml'), $container
->get('request_stack'), $container
->get('config.factory'), $container
->get('path.validator'), $container
->get('token'));
}
/**
* Initiates a SAML2 authentication flow.
*
* This should redirect to the Login service on the IDP and then to our ACS.
* It does not actually log us in (yet).
*/
public function login() {
try {
$url = $this->saml
->login($this
->getUrlFromDestination());
} catch (Exception $e) {
$this
->handleException($e, 'initiating SAML login');
$url = Url::fromRoute('<front>');
}
return $this
->createRedirectResponse($url);
}
/**
* Initiate a SAML2 logout flow.
*
* This should redirect to the SLS service on the IDP and then to our SLS.
* It does not actually log us out (yet).
*/
public function logout() {
try {
$url = $this->saml
->logout($this
->getUrlFromDestination());
} catch (Exception $e) {
$this
->handleException($e, 'initiating SAML logout');
$url = Url::fromRoute('<front>');
}
return $this
->createRedirectResponse($url);
}
/**
* Displays service provider metadata XML for iDP autoconfiguration.
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function metadata() {
try {
$metadata = $this->saml
->getMetadata();
} catch (Exception $e) {
$this
->handleException($e, 'processing SAML SP metadata');
return $this
->createRedirectResponse(Url::fromRoute('<front>'));
}
return new Response($metadata, 200, [
'Content-Type' => 'text/xml',
]);
}
/**
* Attribute Consumer Service.
*
* This is usually the second step in the authentication flow; the Login
* service on the IDP should redirect (or: execute a POST request to) here.
*
* @return \Drupal\Core\Routing\TrustedRedirectResponse
*/
public function acs() {
try {
$this->saml
->acs();
$url = $this
->getRedirectUrlAfterProcessing(TRUE);
} catch (Exception $e) {
$this
->handleException($e, 'processing SAML authentication response');
$url = Url::fromRoute('<front>');
}
return $this
->createRedirectResponse($url);
}
/**
* Single Logout Service.
*
* This is usually the second step in the logout flow; the SLS service on the
* IDP should redirect here.
*
* @return \Drupal\Core\Routing\TrustedRedirectResponse
*/
public function sls() {
try {
$url = $this->saml
->sls();
if (!$url) {
$url = $this
->getRedirectUrlAfterProcessing();
}
} catch (Exception $e) {
$this
->handleException($e, 'processing SAML single-logout response');
$url = Url::fromRoute('<front>');
}
return $this
->createRedirectResponse($url);
}
/**
* Change password redirector.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function changepw() {
$url = $this->config
->get('idp_change_password_service');
return $this
->createRedirectResponse($url);
}
/**
* Constructs a full URL from the 'destination' parameter.
*
* @return string|null
* The full absolute URL (i.e. leading back to ourselves), or NULL if no
* destination parameter was given. This value is tuned to what login() /
* logout() expect for an input argument.
*
* @throws \RuntimeException
* If the destination is disallowed.
*/
protected function getUrlFromDestination() {
$destination_url = NULL;
$destination = $this->requestStack
->getCurrentRequest()->query
->get('destination');
$this->requestStack
->getCurrentRequest()->query
->remove('destination');
if ($destination) {
if (UrlHelper::isExternal($destination)) {
// Prevent authenticating and then redirecting somewhere else.
throw new \RuntimeException("Destination URL query parameter must not be external: {$destination}");
}
// The destination parameter is relative by convention but fromUserInput()
// requires it to start with '/'. (Note '#' and '?' don't make sense here
// because that would be expanded to the current URL, which is saml/*.)
if (strpos($destination, '/') !== 0) {
$destination = "/{$destination}";
}
$destination_url = Url::fromUserInput($destination)
->setAbsolute()
->toString(TRUE)
->getGeneratedUrl();
}
return $destination_url;
}
/**
* Returns a URL to redirect to.
*
* This should be called only after successfully processing an ACS/logout
* response.
*
* @param bool $logged_in
* (optional) TRUE if an ACS request was just processed.
*
* @return \Drupal\Core\Url
* The URL to redirect to.
*/
protected function getRedirectUrlAfterProcessing($logged_in = FALSE) {
if (isset($_REQUEST['RelayState'])) {
// We should be able to trust the RelayState parameter at this point
// because the response from the IDP was verified. Only validate general
// syntax.
if (!UrlHelper::isValid($_REQUEST['RelayState'], TRUE)) {
$this
->getLogger('samlauth')
->error('Invalid RelayState parameter found in request: @relaystate', [
'@relaystate' => $_REQUEST['RelayState'],
]);
}
elseif (strpos($_REQUEST['RelayState'], OneLogin_Saml2_Utils::getSelfURLhost() . '/saml/') !== 0) {
$url = $_REQUEST['RelayState'];
}
}
if (empty($url)) {
// If no url was specified, we check if it was configured.
$url = $this->config
->get($logged_in ? 'login_redirect_url' : 'logout_redirect_url');
}
if ($url) {
$url = $this->token
->replace($url);
// We don't check access here. If a URL was explicitly specified, we
// prefer returning a 403 over silently redirecting somewhere else.
$url_object = $this->pathValidator
->getUrlIfValidWithoutAccessCheck($url);
if (empty($url_object)) {
$type = $logged_in ? 'Login' : 'Logout';
$this
->getLogger('samlauth')
->warning("The {$type} Redirect URL is not a valid path; falling back to default.");
}
}
if (empty($url_object)) {
// If no url was configured, fall back to a hardcoded route.
$url_object = Url::fromRoute($logged_in ? 'user.page' : '<front>');
}
return $url_object;
}
/**
* Converts a URL to a response object that is suitable for this controller.
*
* @param string|\Drupal\Core\Url $url
* A URL to redirect to, either as a string or a Drupal URL object. Strings
* may only be used by callbacks that are configured in routing.yml as not
* being cacheable. (Which, in our case, is most callbacks.)
*
* @return \Drupal\Core\Routing\TrustedRedirectResponse
* A response object representing a redirect.
*/
protected function createRedirectResponse($url) {
if (is_object($url)) {
// If toString() is used without arguments, this influences requirements
// for passing cacheability metadata into the response object, which can
// lead to bugs (see #2630808 short description). We pass TRUE to get
// cacheability metadata passed back in a GeneratedUrl object instead.
$generated_url = $url
->toString(TRUE);
$url = $generated_url
->getGeneratedUrl();
}
// Also when having returned from the IDP, we might redirect to an external
// url (at least in theory), so we always return a TrustedRedirectResponse.
$response = new TrustedRedirectResponse($url);
if (isset($generated_url)) {
// We shouldn't have to add cacheability metadata to our response object
// when the route is configured to not cache responses in our routing.yml.
// Do it anyway to prevent future obscure bugs with new routes.
$response
->addCacheableDependency($generated_url);
}
return $response;
}
/**
* Displays error message and logs full exception.
*
* @param $exception
* The exception thrown.
* @param string $while
* A description of when the error was encountered.
*/
protected function handleException($exception, $while = '') {
if ($while) {
$while = " {$while}";
}
// We use the same format for logging as Drupal's ExceptionLoggingSubscriber
// except we also specify where the error was encountered. (The options are
// limited, so we make this part of the message, not a context parameter.)
$error = Error::decodeException($exception);
unset($error['severity_level']);
$this
->getLogger('samlauth')
->critical("%type encountered while {$while}: @message in %function (line %line of %file).", $error);
// Don't expose the error to prevent information leakage; the user probably
// can't do much with it anyway. But hint that more details are available.
drupal_set_message("Error {$while}; details have been logged.", 'error');
}
}
Classes
Name | Description |
---|---|
SamlController | Returns responses for samlauth module routes. |