Commit 546d5ec3 authored by Philip Frilling's avatar Philip Frilling Committed by Philip Frilling
Browse files

Issue #3030651 by jedihe, pfrilling: Support URL-initiated Log-in through Provider

parent 872c430d
Loading
Loading
Loading
Loading
+9 −1
Original line number Diff line number Diff line
@@ -60,7 +60,7 @@ openid_connect.redirect_controller_redirect:
    _controller: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::authenticate'
    _title: 'OpenID Connect redirect page'
  requirements:
    _custom_access: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::access'
    _custom_access: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::accessAuthenticate'
  options:
    _maintenance_access: TRUE

@@ -91,3 +91,11 @@ openid_connect.login:
    _user_is_logged_in: 'FALSE'
  options:
    _maintenance_access: TRUE

openid_connect.redirect_controller_redirect_iss:
  path: '/openid-connect/{openid_connect_client}/initiate'
  defaults:
    _controller: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::initiate'
    _title: 'OpenID Connect initiate redirect page'
  requirements:
    _custom_access: '\Drupal\openid_connect\Controller\OpenIDConnectRedirectController::accessInitiate'
+135 −4
Original line number Diff line number Diff line
@@ -17,11 +17,14 @@ use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\externalauth\AuthmapInterface;
use Drupal\openid_connect\OpenIDConnectClaims;
use Drupal\openid_connect\OpenIDConnect;
use Drupal\openid_connect\OpenIDConnectClientEntityInterface;
use Drupal\openid_connect\OpenIDConnectSessionInterface;
use Drupal\openid_connect\OpenIDConnectStateTokenInterface;
use Drupal\openid_connect\Plugin\OpenIDConnectClientInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
@@ -108,6 +111,13 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac
   */
  protected $entityTypeManager;

  /**
   * The OpenID Connect claims.
   *
   * @var \Drupal\openid_connect\OpenIDConnectClaims
   */
  protected $claims;

  /**
   * The constructor.
   *
@@ -131,8 +141,10 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac
   *   The language manager service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\openid_connect\OpenIDConnectClaims $claims
   *   The OpenID claims service.
   */
  public function __construct(OpenIDConnect $openid_connect, OpenIDConnectStateTokenInterface $state_token, RequestStack $request_stack, OpenIDConnectSessionInterface $session, ConfigFactoryInterface $config_factory, AuthmapInterface $authmap, AccountProxyInterface $current_user, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager) {
  public function __construct(OpenIDConnect $openid_connect, OpenIDConnectStateTokenInterface $state_token, RequestStack $request_stack, OpenIDConnectSessionInterface $session, ConfigFactoryInterface $config_factory, AuthmapInterface $authmap, AccountProxyInterface $current_user, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager, OpenIDConnectClaims $claims) {
    $this->openIDConnect = $openid_connect;
    $this->stateToken = $state_token;
    $this->requestStack = $request_stack;
@@ -143,6 +155,7 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac
    $this->moduleHandler = $module_handler;
    $this->languageManager = $language_manager;
    $this->entityTypeManager = $entity_type_manager;
    $this->claims = $claims;
  }

  /**
@@ -159,10 +172,48 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac
      $container->get('current_user'),
      $container->get('module_handler'),
      $container->get('language_manager'),
      $container->get('entity_type.manager')
      $container->get('entity_type.manager'),
      $container->get('openid_connect.claims')
    );
  }

  /**
   * Access callback: Initiate page.
   *
   * @param \Drupal\openid_connect\OpenIDConnectClientEntityInterface $openid_connect_client
   *   The client.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   Whether the acting user is allowed to initiate the authorization.
   */
  public function accessInitiate(OpenIDConnectClientEntityInterface $openid_connect_client): AccessResultInterface {
    // Only the anonymous user should be able to access redirects.
    if ($this->currentUser->isAuthenticated()) {
      return AccessResult::forbidden();
    }

    if (empty($openid_connect_client)) {
      return AccessResult::forbidden();
    }

    $query = $this->requestStack->getCurrentRequest()->query;

    // If the iss query parameter exists, the user is attempting SSO directly
    // from the IDP.
    $iss = $query->get('iss');
    if (!empty($iss)) {
      // If the iss domain is not in the allowed list, return forbidden.
      if (!$this->isValidRedirect($openid_connect_client, $iss)) {
        return AccessResult::forbidden();
      }

      return AccessResult::allowed();
    }

    // Default to forbidden.
    return AccessResult::forbidden();
  }

  /**
   * Access callback: Redirect page.
   *
@@ -170,7 +221,7 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac
   *   Whether the state token matches the previously created one that is stored
   *   in the session.
   */
  public function access(): AccessResultInterface {
  public function accessAuthenticate(): AccessResultInterface {
    // Confirm anti-forgery state token. This round-trip verification helps to
    // ensure that the user, not a malicious script, is making the request.
    $request = $this->requestStack->getCurrentRequest();
@@ -178,6 +229,8 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac
    if ($state_token && $this->stateToken->confirm($state_token)) {
      return AccessResult::allowed();
    }

    // Default to forbidden.
    return AccessResult::forbidden();
  }

@@ -264,7 +317,7 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac
    // The destination parameter should be a prepared uri and include any query
    // parameters or fragments already.
    //
    // @see \Drupal\openid_connect\OpenIDConnectSession::saveDestination()
    // @see \Drupal\openid_connect\OpenIDConnectSessionInterface::saveDestination()
    $session = $this->session->retrieveDestination();
    $destination = $session['destination'] ?: $this->configFactory->get('openid_connect.settings')->get('redirect_login');
    $langcode = $session['langcode'] ?: $this->languageManager->getCurrentLanguage()->getId();
@@ -274,6 +327,84 @@ class OpenIDConnectRedirectController implements ContainerInjectionInterface, Ac
    return new RedirectResponse($redirect);
  }

  /**
   * Initiate an SSO from the IDP.
   *
   * @param \Drupal\openid_connect\OpenIDConnectClientEntityInterface $openid_connect_client
   *   The client.
   *
   * @return \Symfony\Component\HttpFoundation\Response
   *   A redirect response back to the IDP.
   */
  public function initiate(OpenIDConnectClientEntityInterface $openid_connect_client): Response {
    if (empty($openid_connect_client)) {
      throw new AccessDeniedHttpException();
    }

    // Handle the ISS query parameter.
    $query = $this->requestStack->getCurrentRequest()->query;
    $iss = $query->get('iss');

    $additional_parameters = [];

    // ISS is required.
    if (empty($iss)) {
      throw new AccessDeniedHttpException();
    }

    // If the iss domain is not in the allowed list, return access denied.
    if (!$this->isValidRedirect($openid_connect_client, $iss)) {
      throw new AccessDeniedHttpException();
    }

    // Handle the login_hint parameter.
    $login_hint = $query->get('login_hint');
    if (!empty($login_hint)) {
      $additional_parameters['login_hint'] = $login_hint;
    }

    // Handle the target_link_uri parameter.
    if (!empty($query->get('target_link_uri'))) {
      $this->session->saveTargetLinkUri($query->get('target_link_uri'));
    }

    $scopes = $this->claims->getScopes();
    return $openid_connect_client->getPlugin()->authorize($scopes, $additional_parameters);
  }

  /**
   * Validate if the domain that initiated the SSO is valid.
   *
   * @param \Drupal\openid_connect\OpenIDConnectClientEntityInterface $client
   *   The client that was requested.
   * @param string $iss
   *   The iss url parameter the requested SSO.
   *
   * @return bool
   *   True if the domain is in the allowed list.
   */
  private function isValidRedirect(OpenIDConnectClientEntityInterface $client, string $iss): bool {
    $settings = $client->get('settings');
    if (empty($settings['iss_allowed_domains'])) {
      return FALSE;
    }

    // Get the domains that are allowed.
    $allowed_domains = explode("\r\n", $settings['iss_allowed_domains']);

    $url = parse_url($iss);

    if (empty($url['host'])) {
      return FALSE;
    }

    if (in_array($url['host'], $allowed_domains)) {
      return TRUE;
    }

    return FALSE;
  }

  /**
   * Redirect after logout.
   */
+19 −0
Original line number Diff line number Diff line
@@ -108,6 +108,25 @@ class OpenIDConnectSession implements OpenIDConnectSessionInterface {
    $this->session->set('openid_connect_langcode', $langcode);
  }

  /**
   * {@inheritDoc}
   */
  public function saveTargetLinkUri(string $target_link_uri): void {
    try {
      $uri = Url::fromUserInput($target_link_uri);
    }
    catch (\InvalidArgumentException $e) {
      // Invalid url, return.
      return;
    }

    // Make sure the uri is not external.
    if (!$uri->isExternal()) {
      // Save the path if it is safe.
      $this->session->set('openid_connect_destination', ltrim($target_link_uri, '/'));
    }
  }

  /**
   * {@inheritdoc}
   */
+13 −0
Original line number Diff line number Diff line
@@ -29,6 +29,19 @@ interface OpenIDConnectSessionInterface extends ContainerInjectionInterface {
   */
  public function saveDestination();

  /**
   * Save a target_link_uri as the redirect destination in the session.
   *
   * This will convert the user provided string to a \Drupal\Core\Url object
   * and will ensure it is not an external link before saving the destination
   * parameter. The string passed _must_ begin with a '/'.
   *
   * @param string $target_link_uri
   *   The internal url that the user should be redirected after login.
   *   The string must begin with a '/'.
   */
  public function saveTargetLinkUri(string $target_link_uri): void;

  /**
   * Get the operation details from the session.
   *
+1 −1
Original line number Diff line number Diff line
@@ -80,7 +80,7 @@ class OpenIDConnectFacebookClient extends OpenIDConnectClientBase {
  /**
   * {@inheritdoc}
   */
  public function authorize(string $scope = 'openid email'): Response {
  public function authorize(string $scope = 'openid email', array $additional_params = []): Response {
    // Use Facebook specific authorisations.
    return parent::authorize('public_profile email');
  }
Loading