Commit cd1f125a authored by william roboly's avatar william roboly
Browse files

Issue #3321924: Keycloak locale param can be a problem for multilingual URIs

parent 12ef13b1
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
services:
  keycloak.keycloak:
    class: Drupal\keycloak\Service\KeycloakService
    arguments: ['@config.factory', '@plugin.manager.openid_connect_client.processor', '@language_manager', '@current_user', '@tempstore.private', '@logger.factory']
    arguments: ['@config.factory', '@plugin.manager.openid_connect_client', '@language_manager', '@current_user', '@tempstore.private', '@logger.factory']

  keycloak.route_subscriber:
    class: Drupal\keycloak\Routing\KeycloakRouteSubscriber
+2 −1
Original line number Diff line number Diff line
@@ -124,7 +124,8 @@ class KeycloakRequestSubscriber implements EventSubscriberInterface {
      }
      // Add parameter to request query, so the Keycloak login/register
      // pages will load using the right locale.
      $query['kc_locale'] = $langcode;
      $locale_param = $this->keycloak->getLocale();
      $query[$locale_param] = $langcode;
    }

    // Generate the endpoint URL including parameters.
+32 −85
Original line number Diff line number Diff line
@@ -2,22 +2,13 @@

namespace Drupal\keycloak\Plugin\OpenIDConnectClient;

use GuzzleHttp\ClientInterface;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Url;
use Drupal\openid_connect\Plugin\OpenIDConnectClientBase;
use Drupal\openid_connect\Plugin\OpenIDConnectClientInterface;
use Drupal\openid_connect\OpenIDConnectStateToken;
use Drupal\keycloak\Service\KeycloakServiceInterface;
use Drupal\keycloak\Service\KeycloakRoleMatcher;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;

/**
 * OpenID Connect client for Keycloak.
@@ -29,8 +20,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
 *   label = @Translation("Keycloak")
 * )
 */
class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInterface, ContainerFactoryPluginInterface {
  use DependencySerializationTrait;
class Keycloak extends OpenIDConnectClientBase {

  /**
   * The Keycloak service.
@@ -53,53 +43,6 @@ class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInt
   */
  protected $uuid;

  /**
   * Constructs an instance of the Keycloak client plugin.
   *
   * @param array $configuration
   *   The plugin configuration.
   * @param string $plugin_id
   *   The plugin identifier.
   * @param mixed $plugin_definition
   *   The plugin definition.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \GuzzleHttp\ClientInterface $http_client
   *   The http client.
   * @param \Drupal\keycloak\Service\KeycloakServiceInterface $keycloak
   *   The Keycloak service.
   * @param \Drupal\keycloak\Service\KeycloakRoleMatcher $role_matcher
   *   The Keycloak role manager service.
   * @param \Drupal\Component\Uuid\UuidInterface $uuid
   *   The UUID service.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger factory.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    RequestStack $request_stack,
    ClientInterface $http_client,
    KeycloakServiceInterface $keycloak,
    KeycloakRoleMatcher $role_matcher,
    UuidInterface $uuid,
    LoggerChannelFactoryInterface $logger_factory
  ) {
    parent::__construct(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $request_stack,
      $http_client,
      $logger_factory
    );

    $this->keycloak = $keycloak;
    $this->roleMatcher = $role_matcher;
    $this->uuid = $uuid;
  }

  /**
   * {@inheritdoc}
   */
@@ -109,26 +52,20 @@ class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInt
    $plugin_id,
    $plugin_definition
  ) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('request_stack'),
      $container->get('http_client'),
      $container->get('keycloak.keycloak'),
      $container->get('keycloak.role_matcher'),
      $container->get('uuid'),
      $container->get('logger.factory')
    );
    $static = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $static->keycloak = $container->get('keycloak.keycloak');
    $static->roleMatcher = $container->get('keycloak.role_matcher');
    $static->uuid = $container->get('uuid');

    return $static;
  }

  /**
   * {@inheritdoc}
   */
  public function authorize($scope = 'openid email') {
    $language_manager = \Drupal::languageManager();
    $language_none = $language_manager
      ->getLanguage(LanguageInterface::LANGCODE_NOT_APPLICABLE);
  public function authorize(string $scope = 'openid email'): Response {
    $language_none = $this->languageManager->getLanguage(LanguageInterface::LANGCODE_NOT_APPLICABLE);

    $redirect_uri = Url::fromRoute(
      'openid_connect.redirect_controller_redirect',
      [
@@ -146,14 +83,14 @@ class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInt
        'response_type' => 'code',
        'scope' => $scope,
        'redirect_uri' => $redirect_uri->getGeneratedUrl(),
        'state' => OpenIDConnectStateToken::create(),
        'state' => $this->stateToken->generateToken(),
      ],
    ];

    // Whether to add language parameter.
    if ($this->keycloak->isI18nEnabled()) {
      // Get current language.
      $langcode = $language_manager->getCurrentLanguage()->getId();
      $langcode = $this->languageManager->getCurrentLanguage()->getId();
      // Map Drupal language code to Keycloak language identifier.
      // This is required for some languages, as Drupal uses IETF
      // script codes, while Keycloak may use IETF region codes.
@@ -163,7 +100,7 @@ class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInt
      }
      // Add parameter to request query, so the Keycloak login/register
      // pages will load using the right locale.
      $url_options['query']['kc_locale'] = $langcode;
      $url_options['query'][$this->configuration['keycloak_locale_param']] = $langcode;
    }

    $endpoints = $this->getEndpoints();
@@ -183,7 +120,7 @@ class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInt
  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
    $form = parent::buildConfigurationForm($form, $form_state);
    $form_state->setCached(FALSE);

@@ -211,8 +148,7 @@ class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInt
    ];

    // Enable/disable i18n support and map language codes to Keycloak locales.
    $language_manager = \Drupal::languageManager();
    if ($language_manager->isMultilingual()) {
    if ($this->languageManager->isMultilingual()) {
      $form['keycloak_i18n_enabled'] = [
        '#title' => $this->t('Enable multi-language support'),
        '#type' => 'checkbox',
@@ -295,7 +231,7 @@ class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInt
      '#step' => 1,
      '#size' => 5,
      '#field_suffix' => $this->t('seconds'),
      '#default_value' => !isset($this->configuration['check_session']['interval']) ? $this->configuration['check_session']['interval'] : 2,
      '#default_value' => isset($this->configuration['check_session']['interval']) ? $this->configuration['check_session']['interval'] : 2,
    ];

    $form['keycloak_groups_enabled'] = [
@@ -349,6 +285,17 @@ class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInt
      '#markup' => sprintf('<strong>%s</strong>', $this->t('Mapping rules')),
    ];

    $form['keycloak_locale_param'] = [
      '#title' => $this->t('Locale parameter variable name'),
      '#type' => 'select',
      '#description' => $this->t('Depending on the version of KeyCloak used a specific localization parameter may need to be used. Select between the choices given. The default is <code>kc_locale</code>, but if you find your system is not behaving the way you expect it with multilingual setups, cross-reference the API documentation, you may need to use <code>ui_locale</code> instead.'),
      '#options' => [
        'kc_locale' => 'kc_locale',
        'ui_locale' => 'ui_locale',
      ],
      '#default_value' => !empty($this->configuration['keycloak_locale_param']) ? $this->configuration['keycloak_locale_param'] : 'kc_locale',
    ];

    $form = array_merge_recursive($form, $this->getGroupRuleTable($form_state));

    return $form;
@@ -357,14 +304,14 @@ class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInt
  /**
   * {@inheritdoc}
   */
  public function getEndpoints() {
  public function getEndpoints(): array {
    return $this->keycloak->getEndpoints();
  }

  /**
   * {@inheritdoc}
   */
  public function retrieveUserInfo($access_token) {
  public function retrieveUserInfo(string $access_token): ?array {
    $userinfo = parent::retrieveUserInfo($access_token);

    // Synchronize email addresses with Keycloak. This is safe as long as
@@ -373,7 +320,7 @@ class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInt
    if (
      $this->configuration['userinfo_update_email'] == 1 &&
      is_array($userinfo) &&
      $sub = openid_connect_extract_sub([], $userinfo)
      $sub = \Drupal::service('openid_connect.openid_connect')->extractSub([], $userinfo)
    ) {
      // Try finding a connected user profile.
      $authmap = \Drupal::service('openid_connect.authmap');
@@ -404,7 +351,7 @@ class Keycloak extends OpenIDConnectClientBase implements OpenIDConnectClientInt
              '@email' => $userinfo['email'],
            ]
          ));
          return FALSE;
          return NULL;
        }

        // Only change the email, if no validation error occurred.
+7 −0
Original line number Diff line number Diff line
@@ -124,6 +124,13 @@ class KeycloakService implements KeycloakServiceInterface {
    return $this->config->get('settings.keycloak_realm');
  }

  /**
   * {@inheritdoc}
   */
  public function getLocale() {
    return $this->config->get('settings.keycloak_locale_param');
  }

  /**
   * {@inheritdoc}
   */
+8 −0
Original line number Diff line number Diff line
@@ -93,6 +93,14 @@ interface KeycloakServiceInterface {
   */
  public function getRealm();

  /**
   * Return the Keycloak locale param.
   *
   * @return string
   *   Keycloak locale param value.
   */
  public function getLocale();

  /**
   * Return the available Keycloak endpoints.
   *