You are here

class Kitchen in Bakery Single Sign-On System 8.2

Hierarchy

Expanded class hierarchy of Kitchen

10 files declare their use of Kitchen
BakeryUncrumbleForm.php in src/Forms/BakeryUncrumbleForm.php
BootSubscriber.php in src/EventSubscriber/BootSubscriber.php
For Boot event subscribe.
BrowserCookieTrait.php in src/Cookies/BrowserCookieTrait.php
BrowserCookieTraitTest.php in tests/src/Unit/Cookies/BrowserCookieTraitTest.php
ChildController.php in src/Controller/ChildController.php

... See full list

1 string reference to 'Kitchen'
bakery.services.yml in ./bakery.services.yml
bakery.services.yml
1 service uses Kitchen
bakery.kitchen in ./bakery.services.yml
Drupal\bakery\Kitchen

File

src/Kitchen.php, line 19

Namespace

Drupal\bakery
View source
class Kitchen {
  use MessengerTrait;
  use LoggerChannelTrait;

  /**
   * Main browser cookie used for authentication.
   */
  const CHOCOLATE_CHIP = 'CHOCOLATECHIP';

  /**
   * Secondary browser cookie used for account information.
   */
  const OATMEAL = 'OATMEAL';

  /**
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $time;

  /**
   * @var \Drupal\Core\Config\ImmutableConfig
   */
  protected $config;

  /**
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * @var \Symfony\Component\HttpFoundation\ParameterBag
   */
  protected $cookieJar;
  public function __construct(TimeInterface $time, ConfigFactoryInterface $config_factory, AccountProxyInterface $current_user, ParameterBag $cookie_jar) {
    $this->time = $time;
    $this->config = $config_factory
      ->get('bakery.settings');
    $this->currentUser = $current_user;
    $this->cookieJar = $cookie_jar;
  }

  /**
   * Set a cookie.
   *
   * @param \Drupal\bakery\Cookies\CookieInterface $cookie
   *   The cookie data.
   *
   * @throws \Drupal\bakery\Exception\MissingKeyException
   *   Thrown if the site key isn't configured yet.
   */
  public function bake(CookieInterface $cookie) {
    $this->cookieJar
      ->set($cookie::getName(), $this
      ->bakeData($cookie));
  }

  /**
   * Encrypt and sign data for Bakery transfer.
   *
   * @param \Drupal\bakery\Cookies\CookieInterface $cookie
   *   The cookie data.
   *
   * @return string
   *   String of signed and encrypted data, url safe.
   */
  public function bakeData(CookieInterface $cookie) {
    $key = $this->config
      ->get('bakery_key');
    if (empty($key)) {
      throw new MissingKeyException();
    }
    $data = $cookie
      ->toData();
    $data['type'] = $cookie::getName();
    $data['timestamp'] = $this->time
      ->getRequestTime();
    $data = $this
      ->encrypt(serialize($data));
    $signature = hash_hmac('sha256', $data, $key);
    return base64_encode($signature . $data);
  }

  /**
   * Re-bake a chocolate chip cookie.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   */
  public function reBakeChocolateChipCookie(AccountInterface $account) {
    try {
      $this
        ->bake(new ChocolateChip($account
        ->getAccountName(), $account
        ->getEmail(), $this
        ->generateInitField($account
        ->id()), $this->config
        ->get('bakery_is_master')));
    } catch (MissingKeyException $exception) {

      // Fail quietly. This could be happening during boot and allowing to
      // bubble could make the site unreachable.
    }
  }

  /**
   * Build internal init url (without scheme).
   */
  public function generateInitField($uid) {
    $url = $this->config
      ->get('bakery_master');
    $scheme = parse_url($url, PHP_URL_SCHEME);
    return str_replace($scheme . '://', '', $url) . 'user/' . $uid . '/edit';
  }

  /**
   * Check that the given cookie exists and doesn't taste funny.
   *
   * @param string $type
   *   Optional string defining the type of data this is.
   * @param \Symfony\Component\HttpFoundation\ParameterBag|null $cookies
   *   Optional list of cookies from the request.
   *
   * @return array|bool
   *   Unserialized data or FALSE if invalid.
   */
  public function taste(string $type, ParameterBag $cookies = NULL) {
    $cookies = $cookies ?? \Drupal::request()->cookies;
    $cookie_name = $this
      ->cookieName($type);
    if (!$cookies
      ->has($cookie_name)) {
      return FALSE;
    }

    // Shouldn't this be part of baking not tasting?
    if (!$this->config
      ->get('bakery_domain')) {
      return FALSE;
    }
    return $this
      ->tasteData($cookies
      ->get($cookie_name), $cookie_name);
  }

  /**
   * Validate signature and decrypt data.
   *
   * @param string $data
   *   String of Bakery data, base64 encoded.
   * @param string|null $type
   *   Optional string defining the type of data this is.
   *
   * @return array|bool
   *   Unserialized data or FALSE if invalid.
   *
   * @throws \Drupal\bakery\Exception\MissingKeyException
   *   Thrown if the site key isn't configured yet.
   */
  public function tasteData(string $data, string $type = NULL) {
    $key = $this->config
      ->get('bakery_key');
    if (empty($key)) {
      throw new MissingKeyException();
    }
    $data = base64_decode($data);
    $signature = substr($data, 0, 64);
    $encrypted_data = substr($data, 64);
    if ($signature !== hash_hmac('sha256', $encrypted_data, $key)) {
      return FALSE;
    }
    $decrypted_data = unserialize($this
      ->decrypt($encrypted_data));

    // Prevent one cookie being used in place of another.
    if ($type !== NULL && $decrypted_data['type'] !== $type) {
      return FALSE;
    }
    if ($decrypted_data['timestamp'] + (int) $this->config
      ->get('bakery_freshness') >= $this->time
      ->getRequestTime()) {
      return $decrypted_data;
    }
    return FALSE;
  }

  /**
   * Nom Nom Nom Nom.
   *
   * Destroys browser cookies.
   *
   * @param string $type
   *   Cookie type
   */
  public function eat(string $type) : void {
    $this->cookieJar
      ->set($this
      ->cookieName($type), '');
  }

  /**
   * Ship a cookie to child sites.
   *
   * @param \Drupal\bakery\Cookies\RemoteCookieInterface $cookie
   *   The cookie data.
   *
   * @return \Psr\Http\Message\ResponseInterface|bool
   */
  public function ship(RemoteCookieInterface $cookie) {
    $path = $cookie
      ->getPath();
    $data = $this
      ->bakeData($cookie);
    if ($this->config
      ->get('bakery_is_master')) {
      $children = $this->config
        ->get('bakery_slaves') ?: [];
      foreach ($children as $child) {
        $result = $this
          ->shipItGood($child . $path, $cookie::getName(), $data);
        if (!$result) {
          if ($this->currentUser
            ->hasPermission('administer bakery')) {
            $this
              ->messenger()
              ->addError(t('Error occurred talking to the site at %url', [
              '%url' => $child,
            ]));
          }
        }
        else {
          if ($this->currentUser
            ->hasPermission('administer bakery')) {
            $this
              ->messenger()
              ->addMessage((string) $result
              ->getBody());
          }
        }
      }
      return TRUE;
    }

    // From child site, send to master.
    $master = $this->config
      ->get('bakery_master');
    return $this
      ->shipItGood($master . $path, $cookie::getName(), $data);
  }

  /**
   * Helper method to ship specific cookie to the site.
   *
   * @param string $uri
   *   The uri to recieve the cookie.
   * @param string $cookie_name
   *   The name of the cookie.
   * @param string $data
   *   The encrypted cookie data.
   *
   * @return false|\Psr\Http\Message\ResponseInterface
   */
  protected function shipItGood(string $uri, string $cookie_name, string $data) {

    // @phpstan-ignore-next-line
    $client = \Drupal::httpClient();
    try {
      return $client
        ->post($uri, [
        'form_params' => [
          $cookie_name => $data,
        ],
      ]);
    } catch (BadResponseException $exception) {
      $this
        ->getLogger('bakery')
        ->error('Failed to fetch file due to HTTP error "%error"', [
        '%error' => $exception
          ->getMessage(),
      ]);
      return FALSE;
    } catch (RequestException $exception) {
      $response = $exception
        ->getResponse();
      $this
        ->getLogger('bakery')
        ->error('Failed to fetch file due to error "%error"', [
        '%error' => $exception
          ->getMessage(),
      ]);
      return FALSE;
    }
  }

  /**
   * Name for cookie including session.cookie_secure and variable extension.
   *
   * @param string $type
   *   CHOCOLATECHIP or OATMEAL.
   *
   * @return string
   *   The cookie name for this environment.
   */
  public function cookieName(string $type) : string {

    // Use different names for HTTPS and HTTP to prevent a cookie collision.
    if (ini_get('session.cookie_secure')) {
      $type .= 'SSL';
    }

    // Allow installation to modify the cookie name.
    $extension = $this->config
      ->get('bakery_cookie_extension') ?: '';
    return $type . $extension;
  }

  /**
   * Encryption handler.
   *
   * @param string $text
   *   The text to be encrypted.
   *
   * @return bool|string
   *   Encrypted text.
   */
  public function encrypt(string $text) {
    $td = phpseclib_mcrypt_module_open('rijndael-128', '', 'ecb', '');
    $iv = phpseclib_mcrypt_create_iv(phpseclib_mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
    $key = substr($this->config
      ->get('bakery_key'), 0, phpseclib_mcrypt_enc_get_key_size($td));
    phpseclib_mcrypt_generic_init($td, $key, $iv);
    $data = phpseclib_mcrypt_generic($td, $text);
    phpseclib_mcrypt_generic_deinit($td);
    phpseclib_mcrypt_module_close($td);
    return $data;
  }

  /**
   * Decryption handler.
   *
   * @param string $text
   *   The data to be decrypted.
   *
   * @return string
   *   Decrypted text.
   */
  public function decrypt(string $text) : string {
    $td = phpseclib_mcrypt_module_open('rijndael-128', '', 'ecb', '');
    $iv = phpseclib_mcrypt_create_iv(phpseclib_mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
    $key = substr($this->config
      ->get('bakery_key'), 0, phpseclib_mcrypt_enc_get_key_size($td));
    phpseclib_mcrypt_generic_init($td, $key, $iv);
    $data = phpseclib_mdecrypt_generic($td, $text);
    phpseclib_mcrypt_generic_deinit($td);
    phpseclib_mcrypt_module_close($td);
    return $data;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
Kitchen::$config protected property
Kitchen::$cookieJar protected property
Kitchen::$currentUser protected property
Kitchen::$time protected property
Kitchen::bake public function Set a cookie.
Kitchen::bakeData public function Encrypt and sign data for Bakery transfer.
Kitchen::CHOCOLATE_CHIP constant Main browser cookie used for authentication.
Kitchen::cookieName public function Name for cookie including session.cookie_secure and variable extension.
Kitchen::decrypt public function Decryption handler.
Kitchen::eat public function Nom Nom Nom Nom.
Kitchen::encrypt public function Encryption handler.
Kitchen::generateInitField public function Build internal init url (without scheme).
Kitchen::OATMEAL constant Secondary browser cookie used for account information.
Kitchen::reBakeChocolateChipCookie public function Re-bake a chocolate chip cookie.
Kitchen::ship public function Ship a cookie to child sites.
Kitchen::shipItGood protected function Helper method to ship specific cookie to the site.
Kitchen::taste public function Check that the given cookie exists and doesn't taste funny.
Kitchen::tasteData public function Validate signature and decrypt data.
Kitchen::__construct public function
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.