You are here

bakery.module in Bakery Single Sign-On System 7.4

File

bakery.module
View source
<?php

use Lcobucci\JWT\Builder;
use Lcobucci\JWT\ValidationData;
use Lcobucci\JWT\Signer\Hmac\Sha512;
use Lcobucci\JWT\Parser;

/**
 * Implements hook_menu().
 */
function bakery_menu() {
  $items = array();
  $items['admin/config/system/bakery'] = array(
    'title' => 'Bakery',
    'access arguments' => array(
      'administer bakery',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'bakery_settings',
    ),
    'description' => 'Infrastructure-wide single-sign-on system options.',
  );
  if (variable_get('bakery_is_master', 0)) {
    $items['bakery'] = array(
      'title' => 'Register',
      'access callback' => 'user_is_anonymous',
      'page callback' => 'bakery_register',
      'type' => MENU_CALLBACK,
    );
    $items['bakery/login'] = array(
      'title' => 'Login',
      'access callback' => 'user_is_anonymous',
      'page callback' => 'bakery_login',
      'type' => MENU_CALLBACK,
    );
  }
  return $items;
}

/**
 * Implements hook_translated_menu_link_alter().
 */
function bakery_translated_menu_link_alter(&$item, $map) {
  if ($item['href'] == 'bakery') {
    $destination = drupal_get_destination();
    $item['localized_options']['query'] = $destination;
  }
}

/**
 * Implements hook_permission().
 */
function bakery_permission() {
  return array(
    'administer bakery' => array(
      'title' => t('Administer Bakery'),
    ),
    'bypass bakery' => array(
      'title' => t('Bypass Bakery'),
      'description' => t('Bypass SSO enforcement policy and allow a user to log in without a valid SSO cookie'),
      'restrict access' => TRUE,
    ),
  );
}

/**
 * Implements hook_user_login().
 */
function bakery_user_login(&$edit, $account) {
  if (variable_get('bakery_is_master', 0) && isset($account->uid)) {
    $init = _bakery_init_field($account->uid);
    _bakery_bake_chocolatechip_cookie($account->name, $account->mail, $init);
  }
}

/**
 * Implements hook_user_logout().
 */
function bakery_user_logout($account) {
  _bakery_destroy_cookie();

  // Destroy session cookie.
  _bakery_destroy_cookie(session_name());
}

/**
 * Implements hook_user_view().
 */
function bakery_user_view($account, $view_mode, $langcode) {
  if (!variable_get('bakery_is_master', 0)) {
    $master = variable_get('bakery_master', 'https://drupal.org/');
    $init_url = _bakery_init_field_url($account->init);
    if (parse_url($master, PHP_URL_HOST) == parse_url($init_url, PHP_URL_HOST)) {
      $account->content['summary']['master_profile'] = array(
        '#type' => 'user_profile_item',
        '#title' => t('Primary profile'),
        '#markup' => l(t('Profile on primary site'), substr($init_url, 0, strlen($init_url) - 5)),
        // Take everything up to '/edit'.
        '#access' => user_access('access user profiles'),
      );
    }
  }
}

/**
 * Implements hook_boot().
 */
function bakery_boot() {

  // Directly include the composer autoload since we need
  // it in this early hook.
  $loader = @(include_once __DIR__ . '/vendor/autoload.php');
  if (!$loader) {

    // Fail and don't try to run bakery code.
    return;
  }
  _bakery_taste_chocolatechip_cookie();
}

/**
 * Implements hook_boot().
 */
function bakery_init() {
  if (isset($GLOBALS['bakery_exception'])) {
    watchdog_exception('bakery', $GLOBALS['bakery_exception']);
    unset($GLOBALS['bakery_exception']);
  }
}

/**
 * Implements hook_form_alter().
 *
 */
function bakery_form_alter(&$form, $form_state, $form_id) {
  switch ($form_id) {
    case 'user_profile_form':
    case 'user_edit_form':
      if (!variable_get('bakery_is_master', 0) && !user_access('administer users')) {
        $init_url = _bakery_init_field_url($form['#user']->init);
        $index = key($form);
        if (isset($form['account'])) {
          drupal_set_message(t('You can change the name, mail, and password <a href="!url">at the master site</a>.', array(
            '!url' => check_url($init_url),
          )), 'status', FALSE);
          $form['account']['#access'] = FALSE;
          $form['account']['name']['#access'] = FALSE;
          $form['account']['pass']['#access'] = FALSE;
          $form['account']['mail']['#access'] = FALSE;
        }
        foreach (variable_get('bakery_supported_fields', array(
          'mail' => 'mail',
          'name' => 'name',
        )) as $type => $value) {
          if ($value) {
            switch ($type) {
              case 'mail':
              case 'name':
                break;
              case 'picture':
                if (isset($form['picture'])) {
                  $form['picture']['picture_delete']['#access'] = FALSE;
                  $form['picture']['picture_upload']['#access'] = FALSE;
                  $form['picture']['#description'] = t('You can change the image <a href="!url">at the master site</a>.', array(
                    '!url' => check_url($init_url),
                  ));
                }
                break;
              case 'language':
                if (isset($form['locale'][$type])) {
                  $form['locale'][$type]['#disabled'] = TRUE;
                  $form['locale'][$type]['#description'] .= ' ' . t('You can change the language setting <a href="!url">at the master site</a>.', array(
                    '!url' => check_url($init_url),
                  ));
                }
                break;
              case 'signature':
                if (isset($form['signature_settings'][$type])) {
                  $form['signature_settings'][$type]['#disabled'] = TRUE;
                  $form['signature_settings'][$type]['#description'] .= ' ' . t('You can change the signature <a href="!url">at the master site</a>.', array(
                    '!url' => check_url($init_url),
                  ));
                }
                break;
              default:
                if (isset($form[$type])) {
                  $form[$type]['#disabled'] = TRUE;
                }
                if (isset($form[$type][$type])) {
                  $form[$type][$type]['#disabled'] = TRUE;
                  $form[$type][$type]['#description'] .= ' ' . t('You can change this setting <a href="!url">at the master site</a>.', array(
                    '!url' => check_url($init_url),
                  ));
                }
                break;
            }
          }
        }
      }
      break;
    case 'user_register_form':

      // Provide register ability on the slave sites.
      if (!variable_get('bakery_is_master', FALSE)) {
        if (arg(0) == 'admin') {

          // Admin create user form. Add a note about account synchronization.
          $form['account']['bakery_help'] = array(
            '#value' => t('<strong>Note:</strong> Only use this form to create accounts for users who exist on <a href="!url">@master</a> and not on this site. Be sure to use the exact same username and e-mail for the account here that they have on @master.', array(
              '!url' => variable_get('bakery_master', 'https://drupal.org'),
              '@master' => variable_get('bakery_master', 'https://drupal.org'),
            )),
            '#weight' => -100,
          );
        }
        else {

          // @todo: provide a link to the master site.
          $form = array();
        }
      }
      break;
    case 'user_pass':

      // @todo: provide a link to the master site.
      if (!variable_get('bakery_is_master', FALSE)) {
        $form = array();
      }
      break;
    case 'user_pass_reset':

      // @todo: provide a link to the master site.
      if (!variable_get('bakery_is_master', FALSE)) {
        $form = array();
      }
      break;
    case 'user_login_block':
    case 'user_login':

      // @todo: provide a link to the master site.
      if (!variable_get('bakery_is_master', FALSE)) {
        $form = array();
      }
      break;
    default:
      break;
  }
}

/**
 * Implements hook_user_update().
 */
function bakery_user_update(&$edit, $account, $category) {
  global $user;
  if (variable_get('bakery_is_master', 0) && isset($_SESSION['bakery'])) {
    if ($user->uid === $account->uid) {

      // Rebake SSO cookie so user stays authenticated.
      $init = _bakery_init_field($account->uid);
      _bakery_bake_chocolatechip_cookie($account->name, $account->mail, $init);
    }
  }
}

/**
 * Check if a form destination is set and save it in $data array.
 *
 * Used to preserve destination in Bakery redirection to master and slave
 * during login and registration.
 *
 * @see drupal_goto()
 *
 * @param $form
 *   Form definition to check.
 * @param $data
 *   Array to store the detected destination value, if any.
 */
function _bakery_save_destination_param($form, &$data) {

  // Hold on to destination if set.
  if (strpos($form['#action'], 'destination=') !== FALSE) {

    // If an absolute URL is in destination parse_url() will issue a warning
    // and not populate $url_args so no further protection is needed.
    parse_str(parse_url($form['#action'], PHP_URL_QUERY), $url_args);
    if (!empty($url_args['destination'])) {
      $data['destination'] = $url_args['destination'];
    }
  }
}

/**
 * Admin settings, see INSTALL.txt
 */
function bakery_settings($form, &$form_state) {
  $form = array(
    '#submit' => array(
      'bakery_settings_submit',
    ),
  );
  $form['bakery_is_master'] = array(
    '#type' => 'checkbox',
    '#title' => 'Is this the master site?',
    '#default_value' => variable_get('bakery_is_master', 0),
    '#description' => t('On the master site, accounts need to be created by traditional processes, i.e by a user registering or an admin creating them.'),
  );
  $form['bakery_master'] = array(
    '#type' => 'textfield',
    '#title' => 'Master site',
    '#default_value' => variable_get('bakery_master', 'https://drupal.org/'),
    '#description' => t('Specify the master site for your bakery network.'),
  );
  $form['bakery_slaves'] = array(
    '#type' => 'textarea',
    '#title' => 'Slave sites',
    '#default_value' => implode("\n", variable_get('bakery_slaves', array())),
    '#description' => t('Specify any slave sites in your bakery network that you want to update if a user changes email or username on the master. Enter one site per line, in the form "https://sub.example.com/".'),
  );
  $form['bakery_help_text'] = array(
    '#type' => 'textarea',
    '#title' => 'Help text for users with synch problems.',
    '#default_value' => variable_get('bakery_help_text', 'Otherwise you can contact the site administrators.'),
    '#description' => t('This message will be shown to users if/when they have problems synching their accounts. It is an alternative to the "self repair" option and can be blank.'),
  );
  $form['bakery_freshness'] = array(
    '#type' => 'textfield',
    '#title' => 'Seconds of age before a cookie is old',
    '#default_value' => variable_get('bakery_freshness', '3600'),
  );
  $form['bakery_key'] = array(
    '#type' => 'textfield',
    '#title' => 'Private key for token validation',
    '#default_value' => variable_get('bakery_key', ''),
  );
  $form['bakery_domain'] = array(
    '#type' => 'textfield',
    '#title' => 'Cookie domain',
    '#default_value' => variable_get('bakery_domain', ''),
  );

  // @TODO: Move the following configuration to Bakery Profile.
  $default = variable_get('bakery_supported_fields', array(
    'mail' => 'mail',
    'name' => 'name',
  ));
  $default['mail'] = 'mail';
  $default['name'] = 'name';
  $options = array(
    'name' => t('username'),
    'mail' => t('e-mail'),
    'status' => t('status'),
    'picture' => t('user picture'),
    'language' => t('language'),
    'signature' => t('signature'),
  );
  if (module_exists('profile')) {
    $result = db_query('SELECT name, title FROM {profile_field} ORDER BY category, weight');
    foreach ($result as $field) {
      $options[$field->name] = check_plain($field->title);
    }
  }
  $form['bakery_supported_fields'] = array(
    '#type' => 'checkboxes',
    '#title' => 'Supported profile fields',
    '#default_value' => $default,
    '#options' => $options,
    '#description' => t('Choose the profile fields that should be exported by the master and imported on the slaves. Username and E-mail are always exported. The correct export of individual fields may depend on the appropriate settings for other modules on both master and slaves. You need to configure this setting on both the master and the slaves.'),
  );

  // Tell system_settings_form() to not set default_values since we have already done so.
  return system_settings_form($form, FALSE);
}
function bakery_settings_submit($form, &$form_state) {

  // Rebuild the menu because the router items are based on the selection of
  // the master site. (Rebuilding it immediately here would be too early,
  // because the 'bakery_is_master' variable doesn't get set until the next
  // submit handler runs. So we trigger a rebuild on the next page request
  // instead.)
  variable_set('menu_rebuild_needed', TRUE);

  // Updating of data on slave sites will not work unless the url of the master site has a trailing slash.
  // We now remove the trailing slash (if present) and concatenate with a new trailing slash.
  $form_state['values']['bakery_master'] = trim($form_state['values']['bakery_master'], '/') . '/';

  // The list of slave sites needs transforming from a text string into array for storage.
  // Also, redirection after login will only work if there is a trailing slash after each entry.
  if ($form_state['values']['bakery_slaves']) {

    // Transform the text string into an array.
    $form_state['values']['bakery_slaves'] = explode("\n", trim(str_replace("\r", '', $form_state['values']['bakery_slaves'])));

    // For each entry, remove the trailing slash (if present) and concatenate with a new trailing slash.
    foreach ($form_state['values']['bakery_slaves'] as &$slave) {
      $slave = trim($slave, '/') . '/';
    }
  }
  else {
    $form_state['values']['bakery_slaves'] = array();
  }
}

/**
 * Access callback for path /user.
 *
 * Displays user profile if user is logged in, or login form for anonymous
 * users.
 */
function bakery_user_page() {
  global $user;
  if ($user->uid) {
    menu_set_active_item('user/' . $user->uid);
    return menu_execute_active_handler();
  }
}

/**
 * Sign data for Bakery transfer.
 *
 * @param string $username
 *   The Drupal username.
 * @param string $type
 *   The cookie type (e.g. 'CHOCOLATECHIP').
 * @param int $expiration
 *   Unix timestamp for the cookie expiration.
 * @param Array $claims
 *   (Optional) list of claims of data to be transferred.
 *
 * @return String of signed data, URL safe.
 */
function bakery_bake_data($username, $type, $expiration, $claims = array()) {
  $key = variable_get('bakery_key', '');
  $uri = _bakery_uri();
  $now = REQUEST_TIME;
  $builder = new Builder();
  $builder
    ->setId($username, true)
    ->setIssuer($uri)
    ->setAudience($uri)
    ->setIssuedAt($now)
    ->setNotBefore($now - 60)
    ->setExpiration($expiration)
    ->set('type', $type);

  // Add additional claims.
  foreach ($claims as $name => $value) {
    $builder
      ->set($name, $value);
  }
  $builder
    ->sign(_bakery_signer(), $key);
  return (string) $builder
    ->getToken();
}

/**
 * Validate signature and attributes.
 *
 * @param Serialized token data.
 * @param Optional string defining the type of data this is.
 *
 * @return \Lcobucci\JWT\Token
 *   A token object
 *
 * @throws \Exception on failure
 */
function bakery_validate_data($data, $type = NULL) {
  $key = variable_get('bakery_key', '');

  // Parse the provided token and check the signature.
  $token = (new Parser())
    ->parse((string) $data);
  if (!$token
    ->verify(_bakery_signer(), $key)) {
    throw new \InvalidArgumentException('Could not verify the signature');
  }

  // Verify other attibutes.
  $uri = _bakery_uri();
  $validation = new ValidationData();
  $validation
    ->setIssuer($uri);
  $validation
    ->setAudience($uri);
  if (!$token
    ->validate($validation)) {
    throw new \InvalidArgumentException('Could not validate the issuer and audience');
  }

  // Prevent one cookie being used in place of another.
  if ($type !== NULL && $token
    ->getClaim('type') !== $type) {
    throw new \InvalidArgumentException('Could not validate the type');
  }
  return $token;
}

/**
 * Name for cookie including variable extension.
 *
 * @param string $type
 *   CHOCOLATECHIP or OATMEAL, default CHOCOLATECHIP
 * @return string
 *   The cookie name for this environment.
 */
function _bakery_cookie_name($type = 'CHOCOLATECHIP') {

  // Allow installation to modify the cookie name.
  $extension = variable_get('bakery_cookie_extension', '');
  $type .= $extension;
  return $type;
}

/**
 * URI for Bakery federation.
 * @return string
 *   The URI.
 */
function _bakery_uri() {
  return 'https://' . variable_get('bakery_domain', '') . '/';
}

/**
 * Provides the desired signing implementation.
 *
 * @return \Lcobucci\JWT\Signer
 *   The URI.
 */
function _bakery_signer() {
  return new Sha512();
}

/**
 * Function to validate cookies
 *
 * @param $type (string) CHOCOLATECHIP or OATMEAL, default CHOCOLATECHIP
 *
 * @return \Lcobucci\JWT\Token
 *
 * @throws \Exception
 *   If the cookie is not valid.
 */
function _bakery_validate_cookie($type = 'CHOCOLATECHIP') {
  $key = variable_get('bakery_key', '');
  $type = _bakery_cookie_name($type);
  if (!isset($_COOKIE[$type]) || !$key || !variable_get('bakery_domain', '')) {

    // No cookie is set or site is misconfigured. Return NULL so existing
    // cookie is not deleted by _bakery_destroy_cookie().
    return NULL;
  }
  return bakery_validate_data($_COOKIE[$type], $type);
}

/**
 * Test identification cookie
 */
function _bakery_taste_chocolatechip_cookie() {
  $destroy_cookie = NULL;
  $token = NULL;
  try {
    $token = _bakery_validate_cookie();
  } catch (\Exception $e) {
    $destroy_cookie = TRUE;
    $GLOBALS['bakery_exception'] = $e;
  }

  // Continue if this is a valid cookie. That only happens for users who have
  // a current valid session on the master site.
  if ($token) {
    $destroy_cookie = FALSE;
    global $user;

    // Detect SSO cookie mismatch if there is already a valid session for user.
    if ($user->uid && $token
      ->getClaim('jti') !== $user->name) {

      // The SSO cookie doesn't match the existing session so force a logout.
      drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
      _bakery_user_logout();
    }

    // Bake a fresh cookie. Yum.
    _bakery_bake_chocolatechip_cookie($token
      ->getClaim('jti'), $token
      ->getClaim('mail'), $token
      ->getClaim('init'));
    if (!$user->uid) {

      // Since this might happen in hook_boot we need to bootstrap first.
      // Note that this only runs if they have a valid session on the master
      // and do not have one on the slave so it only creates the extra load of
      // a bootstrap on one pageview per session on the site which is not much.
      drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

      // User is anonymous. If they do not have an account we'll create one by
      // requesting their information from the master site. If they do have an
      // account we may need to correct some disparant information.
      $account = user_load_multiple(array(), array(
        'name' => $token
          ->getClaim('jti'),
        'mail' => $token
          ->getClaim('mail'),
      ));
      $account = reset($account);

      // Fix out of sync users with valid init.
      if (!$account && !variable_get('bakery_is_master', 0) && $token
        ->getClaim('master')) {
        $count = db_select('users', 'u')
          ->fields('u', array(
          'uid',
        ))
          ->condition('init', $token
          ->getClaim('init'))
          ->countQuery()
          ->execute()
          ->fetchField();
        if ($count > 1) {

          // Uh oh.
          watchdog('bakery', 'Account uniqueness problem: Multiple users found with init %init.', array(
            '%init' => $token['init'],
          ), WATCHDOG_ERROR);
          drupal_set_message(t('Account uniqueness problem detected. <a href="@contact">Please contact the site administrator.</a>', array(
            '@contact' => variable_get('bakery_master', 'https://drupal.org/') . 'contact',
          )), 'error');
        }
        if ($count == 1) {
          $account = user_load_multiple(array(), array(
            'init' => $token
              ->getClaim('init'),
          ));
          if (is_array($account)) {
            $account = reset($account);
          }
          if ($account) {
            watchdog('bakery', 'Fixing out of sync uid %uid. Changed name %name_old to %name_new, mail %mail_old to %mail_new.', array(
              '%uid' => $account->uid,
              '%name_old' => $account->name,
              '%name_new' => $token
                ->getClaim('jti'),
              '%mail_old' => $account->mail,
              '%mail_new' => $token
                ->getClaim('mail'),
            ));
            user_save($account, array(
              'name' => $token
                ->getClaim('jti'),
              'mail' => $token
                ->getClaim('mail'),
            ));
            $account = user_load_multiple(array(), array(
              'name' => $token
                ->getClaim('jti'),
              'mail' => $token
                ->getClaim('mail'),
            ));
            $account = reset($account);
          }
        }
      }

      // Create the account if it doesn't exist.
      if (!$account && !variable_get('bakery_is_master', 0) && $token
        ->getClaim('master')) {
        $checks = TRUE;
        $mail_count = db_select('users', 'u')
          ->fields('u', array(
          'uid',
        ))
          ->condition('uid', $user->uid, '!=')
          ->condition('mail', '', '!=')
          ->where('LOWER(mail) = LOWER(:mail)', array(
          ':mail' => $token
            ->getClaim('mail'),
        ))
          ->countQuery()
          ->execute()
          ->fetchField();
        if ($mail_count > 0) {
          $checks = FALSE;
        }
        $name_count = db_select('users', 'u')
          ->fields('u', array(
          'uid',
        ))
          ->condition('uid', $user->uid, '!=')
          ->where('LOWER(name) = LOWER(:name)', array(
          ':name' => $token
            ->getClaim('jti'),
        ))
          ->countQuery()
          ->execute()
          ->fetchField();
        if ($name_count > 0) {
          $checks = FALSE;
        }
        $init_count = db_select('users', 'u')
          ->fields('u', array(
          'uid',
        ))
          ->condition('uid', $user->uid, '!=')
          ->condition('init', $token
          ->getClaim('init'), '=')
          ->where('LOWER(name) = LOWER(:name)', array(
          ':name' => $token
            ->getClaim('jti'),
        ))
          ->countQuery()
          ->execute()
          ->fetchField();
        if ($init_count > 0) {
          $checks = FALSE;
        }
        if ($checks) {

          // Request information from master to keep data in sync.
          $uid = bakery_request_account($token
            ->getClaim('jti'));

          // In case the account creation failed we want to make sure the user
          // gets their bad cookie destroyed by not returning too early.
          if ($uid) {
            $account = user_load($uid);
          }
          else {
            $destroy_cookie = TRUE;
          }
        }
        else {
          drupal_set_message(t('Your user account on %site appears to have problems.', array(
            '%site' => variable_get('site_name', 'Drupal'),
          )));
          drupal_set_message(filter_xss_admin(variable_get('bakery_help_text', 'Please contact the site administrators.')));
        }
      }
      if ($account && $token
        ->getClaim('master') && $account->uid && !variable_get('bakery_is_master', 0) && $account->init != $token
        ->getClaim('init')) {

        // User existed previously but init is wrong. Fix it to ensure account
        // remains in sync.
        // Make sure that there aren't any OTHER accounts with this init already.
        $count = db_select('users', 'u')
          ->fields('u', array(
          'uid',
        ))
          ->condition('init', $token
          ->getClaim('init'), '=')
          ->countQuery()
          ->execute()
          ->fetchField();
        if ($count == 0) {
          db_update('users')
            ->fields(array(
            'init' => $token['init'],
          ))
            ->condition('uid', $account->uid)
            ->execute();
          watchdog('bakery', 'uid %uid out of sync. Changed init field from %oldinit to %newinit', array(
            '%oldinit' => $account->init,
            '%newinit' => $token
              ->getClaim('init'),
            '%uid' => $account->uid,
          ));
        }
        else {

          // Username and email matched, but init belonged to a DIFFERENT account.
          // Something got seriously tangled up.
          watchdog('bakery', 'Accounts mixed up! Username %user and init %init disagree with each other!', array(
            '%user' => $account->name,
            '%init' => $token
              ->getClaim('init'),
          ), WATCHDOG_CRITICAL);
        }
      }
      if ($account && $user->uid == 0) {

        // If the login attempt fails we need to destroy the cookie to prevent
        // infinite redirects (with infinite failed login messages).
        $login = bakery_user_external_login($account);
        if ($login) {

          // If an anonymous user has just been logged in, trigger a 'refresh'
          // of the current page, ensuring that drupal_goto() does not override
          // the current page with the destination query.
          $query = drupal_get_query_parameters();
          unset($_GET['destination']);
          drupal_goto(current_path(), array(
            'query' => $query,
          ));
        }
        else {
          $destroy_cookie = TRUE;
        }
      }
    }
    if ($destroy_cookie !== TRUE) {
      return TRUE;
    }
  }
  if ($destroy_cookie === TRUE) {

    // Destroy the bad cookie. Burp.
    _bakery_destroy_cookie();
  }
  global $user;

  // Log out users that have lost their SSO cookie, with the exception of
  // roles with permission to bypass.
  if ($user->uid > 1) {

    // This runs for logged in users. Those folks are going to get a full bootstrap anyway so this isn't a problem.
    drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
    if (!user_access('bypass bakery')) {
      watchdog('bakery', 'Logging out the user with the bad cookie.');
      _bakery_user_logout();
    }
  }
  return FALSE;
}

/**
 * Create a new cookie for identification
 */
function _bakery_bake_chocolatechip_cookie($username, $mail, $init) {
  $key = variable_get('bakery_key', '');
  if (!empty($key)) {
    $claims = array(
      'mail' => $mail,
      'init' => $init,
      'master' => variable_get('bakery_is_master', 0),
    );
    $type = _bakery_cookie_name('CHOCOLATECHIP');
    $now = $_SERVER['REQUEST_TIME'];
    $expiration = variable_get('bakery_freshness', '3600') > 0 ? $now + variable_get('bakery_freshness', '3600') : '0';
    $token = bakery_bake_data($username, $type, $expiration, $claims);

    // Allow cookies to expire when the browser closes.
    setcookie($type, $token, $expiration, '/', variable_get('bakery_domain', ''), TRUE, TRUE);
  }
}

/**
 * Destroy unwanted cookies
 */
function _bakery_destroy_cookie($type = 'CHOCOLATECHIP') {
  $type = _bakery_cookie_name($type);
  setcookie($type, '', $_SERVER['REQUEST_TIME'] - 3600, '/', '', TRUE, TRUE);
  setcookie($type, '', $_SERVER['REQUEST_TIME'] - 3600, '/', variable_get('bakery_domain', ''), TRUE, TRUE);
}

/**
 * Build internal init url (without scheme).
 */
function _bakery_init_field($uid) {
  $url = variable_get('bakery_master', 'https://drupal.org/');
  return str_replace('https://', '', $url) . 'user/' . $uid . '/edit';
}

/**
 * Build full init url to master.
 */
function _bakery_init_field_url($init) {
  return 'https://' . $init;
}

/**
 * Perform standard Drupal login operations for a user object.
 *
 * The user object must already be authenticated. This function verifies
 * that the user account is not blocked/denied and then performs the login,
 * updates the login timestamp in the database, invokes hook_user('login'),
 * and regenerates the session.
 *
 * @param $account
 *    An authenticated user object to be set as the currently logged
 *    in user.
 * @param $edit
 *    The array of form values submitted by the user, if any.
 *    This array is passed to hook_user op login.
 * @return boolean
 *    TRUE if the login succeeds, FALSE otherwise.
 */
function bakery_user_external_login($account, $edit = array()) {
  $form = drupal_get_form('user_login');
  $state['values'] = $edit;
  if (empty($state['values']['name'])) {
    $state['values']['name'] = $account->name;
  }

  // Check if user is blocked or denied by access rules.
  user_login_name_validate($form, $state, (array) $account);
  if (form_get_errors()) {

    // Invalid login.
    return FALSE;
  }

  // Valid login.
  global $user;
  $user = $account;
  bakery_user_authenticate_finalize($state['values']);
  return TRUE;
}

/**
 * Finalize the login process. Must be called when logging in a user.
 *
 * The function records a watchdog message about the new session, saves the
 * login timestamp, calls hook_user op 'login' and generates a new session.
 *
 * $param $edit
 *   This array is passed to hook_user op login.
 */
function bakery_user_authenticate_finalize(&$edit) {
  global $user;
  watchdog('user', 'Session opened for %name.', array(
    '%name' => $user->name,
  ));

  // Update the user table timestamp noting user has logged in.
  // This is also used to invalidate one-time login links.
  $user->login = time();
  db_update('users')
    ->fields(array(
    'login' => $user->login,
  ))
    ->condition('uid', $user->uid, '=')
    ->execute();

  // Regenerate the session ID to prevent against session fixation attacks.
  drupal_session_regenerate();
  user_module_invoke('login', $edit, $user);
}

/**
 * Custom logout function modified from user_logout.
 */
function _bakery_user_logout() {
  global $user;
  watchdog('user', 'Session closed for %name.', array(
    '%name' => $user->name,
  ));

  // Destroy the current session:
  session_destroy();
  module_invoke_all('user_logout', $user);

  // Load the anonymous user
  $user = drupal_anonymous_user();

  // We want to redirect the user to his original destination.
  $get = $_GET;
  $destination = !empty($get['q']) ? $get['q'] : '';
  unset($get['q']);

  // We append a GET parameter so that the browser reloads the page.
  $get['no_cache'] = time();

  // Build the URL we'll redirect to. We set alias to TRUE so as not to try and
  // hit the unavailable database looking for an alias.
  $url = url($destination, array(
    'query' => $get,
    'absolute' => TRUE,
    'alias' => TRUE,
  ));

  // Remove newlines from the URL to avoid header injection attacks.
  $url = str_replace(array(
    "\n",
    "\r",
  ), '', $url);

  // We can't use drupal_goto because it assumes it's in a later boot phase. Set
  // the status code to be temporary redirect because of the no_cache time.
  header('Location: ' . $url, TRUE, 307);
  exit;
}

/**
 * Save UID provided by a slave site. Should only be used on the master site.
 *
 * @param $account
 *   A local user object.
 * @param $slave
 *   The URL of the slave site.
 * @param $slave_uid
 *   The corresponding UID on the slave site.
 */
function _bakery_save_slave_uid($account, $slave, $slave_uid) {
  $slave_user_exists = db_query_range("SELECT 1 FROM {bakery_user} WHERE uid = :uid AND slave = :slave", 0, 1, array(
    ':uid' => $account->uid,
    ':slave' => $slave,
  ))
    ->fetchField();
  if (variable_get('bakery_is_master', 0) && !empty($slave_uid) && in_array($slave, variable_get('bakery_slaves', array())) && !$slave_user_exists) {
    $row = array(
      'uid' => $account->uid,
      'slave' => $slave,
      'slave_uid' => $slave_uid,
    );
    drupal_write_record('bakery_user', $row);
  }
}

Functions

Namesort descending Description
bakery_bake_data Sign data for Bakery transfer.
bakery_boot Implements hook_boot().
bakery_form_alter Implements hook_form_alter().
bakery_init Implements hook_boot().
bakery_menu Implements hook_menu().
bakery_permission Implements hook_permission().
bakery_settings Admin settings, see INSTALL.txt
bakery_settings_submit
bakery_translated_menu_link_alter Implements hook_translated_menu_link_alter().
bakery_user_authenticate_finalize Finalize the login process. Must be called when logging in a user.
bakery_user_external_login Perform standard Drupal login operations for a user object.
bakery_user_login Implements hook_user_login().
bakery_user_logout Implements hook_user_logout().
bakery_user_page Access callback for path /user.
bakery_user_update Implements hook_user_update().
bakery_user_view Implements hook_user_view().
bakery_validate_data Validate signature and attributes.
_bakery_bake_chocolatechip_cookie Create a new cookie for identification
_bakery_cookie_name Name for cookie including variable extension.
_bakery_destroy_cookie Destroy unwanted cookies
_bakery_init_field Build internal init url (without scheme).
_bakery_init_field_url Build full init url to master.
_bakery_save_destination_param Check if a form destination is set and save it in $data array.
_bakery_save_slave_uid Save UID provided by a slave site. Should only be used on the master site.
_bakery_signer Provides the desired signing implementation.
_bakery_taste_chocolatechip_cookie Test identification cookie
_bakery_uri URI for Bakery federation.
_bakery_user_logout Custom logout function modified from user_logout.
_bakery_validate_cookie Function to validate cookies