View source
<?php
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\ValidationData;
use Lcobucci\JWT\Signer\Hmac\Sha512;
use Lcobucci\JWT\Parser;
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;
}
function bakery_translated_menu_link_alter(&$item, $map) {
if ($item['href'] == 'bakery') {
$destination = drupal_get_destination();
$item['localized_options']['query'] = $destination;
}
}
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,
),
);
}
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);
}
}
function bakery_user_logout($account) {
_bakery_destroy_cookie();
_bakery_destroy_cookie(session_name());
}
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)),
'#access' => user_access('access user profiles'),
);
}
}
}
function bakery_boot() {
$loader = @(include_once __DIR__ . '/vendor/autoload.php');
if (!$loader) {
return;
}
_bakery_taste_chocolatechip_cookie();
}
function bakery_init() {
if (isset($GLOBALS['bakery_exception'])) {
watchdog_exception('bakery', $GLOBALS['bakery_exception']);
unset($GLOBALS['bakery_exception']);
}
}
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':
if (!variable_get('bakery_is_master', FALSE)) {
if (arg(0) == 'admin') {
$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 {
$form = array();
}
}
break;
case 'user_pass':
if (!variable_get('bakery_is_master', FALSE)) {
$form = array();
}
break;
case 'user_pass_reset':
if (!variable_get('bakery_is_master', FALSE)) {
$form = array();
}
break;
case 'user_login_block':
case 'user_login':
if (!variable_get('bakery_is_master', FALSE)) {
$form = array();
}
break;
default:
break;
}
}
function bakery_user_update(&$edit, $account, $category) {
global $user;
if (variable_get('bakery_is_master', 0) && isset($_SESSION['bakery'])) {
if ($user->uid === $account->uid) {
$init = _bakery_init_field($account->uid);
_bakery_bake_chocolatechip_cookie($account->name, $account->mail, $init);
}
}
}
function _bakery_save_destination_param($form, &$data) {
if (strpos($form['#action'], 'destination=') !== FALSE) {
parse_str(parse_url($form['#action'], PHP_URL_QUERY), $url_args);
if (!empty($url_args['destination'])) {
$data['destination'] = $url_args['destination'];
}
}
}
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', ''),
);
$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.'),
);
return system_settings_form($form, FALSE);
}
function bakery_settings_submit($form, &$form_state) {
variable_set('menu_rebuild_needed', TRUE);
$form_state['values']['bakery_master'] = trim($form_state['values']['bakery_master'], '/') . '/';
if ($form_state['values']['bakery_slaves']) {
$form_state['values']['bakery_slaves'] = explode("\n", trim(str_replace("\r", '', $form_state['values']['bakery_slaves'])));
foreach ($form_state['values']['bakery_slaves'] as &$slave) {
$slave = trim($slave, '/') . '/';
}
}
else {
$form_state['values']['bakery_slaves'] = array();
}
}
function bakery_user_page() {
global $user;
if ($user->uid) {
menu_set_active_item('user/' . $user->uid);
return menu_execute_active_handler();
}
}
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);
foreach ($claims as $name => $value) {
$builder
->set($name, $value);
}
$builder
->sign(_bakery_signer(), $key);
return (string) $builder
->getToken();
}
function bakery_validate_data($data, $type = NULL) {
$key = variable_get('bakery_key', '');
$token = (new Parser())
->parse((string) $data);
if (!$token
->verify(_bakery_signer(), $key)) {
throw new \InvalidArgumentException('Could not verify the signature');
}
$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');
}
if ($type !== NULL && $token
->getClaim('type') !== $type) {
throw new \InvalidArgumentException('Could not validate the type');
}
return $token;
}
function _bakery_cookie_name($type = 'CHOCOLATECHIP') {
$extension = variable_get('bakery_cookie_extension', '');
$type .= $extension;
return $type;
}
function _bakery_uri() {
return 'https://' . variable_get('bakery_domain', '') . '/';
}
function _bakery_signer() {
return new Sha512();
}
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', '')) {
return NULL;
}
return bakery_validate_data($_COOKIE[$type], $type);
}
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;
}
if ($token) {
$destroy_cookie = FALSE;
global $user;
if ($user->uid && $token
->getClaim('jti') !== $user->name) {
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
_bakery_user_logout();
}
_bakery_bake_chocolatechip_cookie($token
->getClaim('jti'), $token
->getClaim('mail'), $token
->getClaim('init'));
if (!$user->uid) {
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
$account = user_load_multiple(array(), array(
'name' => $token
->getClaim('jti'),
'mail' => $token
->getClaim('mail'),
));
$account = reset($account);
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) {
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);
}
}
}
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) {
$uid = bakery_request_account($token
->getClaim('jti'));
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')) {
$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 {
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) {
$login = bakery_user_external_login($account);
if ($login) {
$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) {
_bakery_destroy_cookie();
}
global $user;
if ($user->uid > 1) {
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;
}
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);
setcookie($type, $token, $expiration, '/', variable_get('bakery_domain', ''), TRUE, TRUE);
}
}
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);
}
function _bakery_init_field($uid) {
$url = variable_get('bakery_master', 'https://drupal.org/');
return str_replace('https://', '', $url) . 'user/' . $uid . '/edit';
}
function _bakery_init_field_url($init) {
return 'https://' . $init;
}
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;
}
user_login_name_validate($form, $state, (array) $account);
if (form_get_errors()) {
return FALSE;
}
global $user;
$user = $account;
bakery_user_authenticate_finalize($state['values']);
return TRUE;
}
function bakery_user_authenticate_finalize(&$edit) {
global $user;
watchdog('user', 'Session opened for %name.', array(
'%name' => $user->name,
));
$user->login = time();
db_update('users')
->fields(array(
'login' => $user->login,
))
->condition('uid', $user->uid, '=')
->execute();
drupal_session_regenerate();
user_module_invoke('login', $edit, $user);
}
function _bakery_user_logout() {
global $user;
watchdog('user', 'Session closed for %name.', array(
'%name' => $user->name,
));
session_destroy();
module_invoke_all('user_logout', $user);
$user = drupal_anonymous_user();
$get = $_GET;
$destination = !empty($get['q']) ? $get['q'] : '';
unset($get['q']);
$get['no_cache'] = time();
$url = url($destination, array(
'query' => $get,
'absolute' => TRUE,
'alias' => TRUE,
));
$url = str_replace(array(
"\n",
"\r",
), '', $url);
header('Location: ' . $url, TRUE, 307);
exit;
}
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);
}
}