View source
<?php
define('OPENID_CONNECT_REDIRECT_PATH_BASE', 'openid-connect');
function openid_connect_menu() {
$items = array();
$items['admin/config/services/openid-connect'] = array(
'title' => 'OpenID Connect',
'description' => 'Config OpenID Connect, choose active OpenID Connect clients etc.',
'page callback' => 'drupal_get_form',
'page arguments' => array(
'openid_connect_admin_form',
),
'access arguments' => array(
'configure openid connect clients',
),
'file' => 'includes/openid_connect.admin.inc',
);
$items[OPENID_CONNECT_REDIRECT_PATH_BASE . '/%'] = array(
'title' => 'OpenID Connect redirect page',
'page callback' => 'openid_connect_redirect_page',
'page arguments' => array(
1,
),
'access callback' => 'openid_connect_redirect_access',
'type' => MENU_CALLBACK,
'file' => 'includes/openid_connect.pages.inc',
);
$items['user/%user/connected-accounts'] = array(
'title' => 'Connected accounts',
'page callback' => 'drupal_get_form',
'page arguments' => array(
'openid_connect_connect_form',
1,
),
'access callback' => 'openid_connect_connected_accounts_access',
'access arguments' => array(
1,
),
'type' => MENU_LOCAL_TASK,
'weight' => 5,
'file' => 'includes/openid_connect.forms.inc',
);
return $items;
}
function openid_connect_permission() {
return array(
'configure openid connect clients' => array(
'title' => t('Configure OpenID Connect clients'),
),
'manage own openid_connect accounts' => array(
'title' => t('Manage own connected accounts'),
),
'openid_connect set own password' => array(
'title' => t('Set a password for local authentication'),
'description' => t('If the account is connected with an external provider, the user needs this permission in order to set their own password.'),
),
);
}
function openid_connect_ctools_plugin_directory($module, $plugin) {
if ($module == 'openid_connect') {
return 'plugins/' . $plugin;
}
}
function openid_connect_ctools_plugin_type() {
$plugins['openid_connect_client'] = array(
'classes' => array(
'class',
),
);
return $plugins;
}
function openid_connect_get_client($client_name) {
$clients =& drupal_static(__FUNCTION__);
if (!isset($clients[$client_name])) {
$plugin = openid_connect_get_plugin($client_name);
if ($plugin) {
$settings = variable_get('openid_connect_client_' . $client_name, array());
$clients[$client_name] = new $plugin['class']($client_name, $plugin['title'], $settings);
}
else {
$clients[$client_name] = FALSE;
}
}
return $clients[$client_name];
}
function openid_connect_get_plugin($client_name) {
ctools_include('plugins');
return ctools_get_plugins('openid_connect', 'openid_connect_client', $client_name);
}
function openid_connect_get_plugins($enabled_only = FALSE) {
ctools_include('plugins');
$plugins = ctools_get_plugins('openid_connect', 'openid_connect_client');
$plugins_enabled = variable_get('openid_connect_clients_enabled', array());
foreach ($plugins as $key => $plugin) {
if (!class_exists($plugin['class']) || $enabled_only && empty($plugins_enabled[$plugin['name']])) {
unset($plugins[$key]);
continue;
}
}
uasort($plugins, 'ctools_plugin_sort');
return $plugins;
}
function openid_connect_block_info() {
return array(
'openid_connect_login' => array(
'info' => t('OpenID Connect login'),
'cache' => DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE,
),
);
}
function openid_connect_block_view($delta = '') {
if ($delta == 'openid_connect_login' && user_is_anonymous()) {
module_load_include('inc', 'openid_connect', 'includes/openid_connect.forms');
return array(
'subject' => t('Log in'),
'content' => drupal_get_form('openid_connect_login_form'),
);
}
}
function openid_connect_create_state_token() {
$state = drupal_random_key();
$_SESSION['openid_connect_state'] = $state;
return $state;
}
function openid_connect_confirm_state_token($state_token) {
return isset($_SESSION['openid_connect_state']) && $state_token == $_SESSION['openid_connect_state'];
}
function openid_connect_redirect_access() {
return !empty($_GET['state']) && openid_connect_confirm_state_token($_GET['state']);
}
function openid_connect_connected_accounts_access($account) {
global $user;
if (user_access('administer users')) {
return TRUE;
}
return $user->uid && $user->uid === $account->uid && user_access('manage own openid_connect accounts');
}
function openid_connect_save_userinfo($account, $userinfo) {
$account_wrapper = entity_metadata_wrapper('user', $account);
$properties = $account_wrapper
->getPropertyInfo();
$properties_skip = _openid_connect_user_properties_to_skip();
foreach ($properties as $property_name => $property) {
if (isset($properties_skip[$property_name])) {
continue;
}
$claim = variable_get('openid_connect_userinfo_mapping_property_' . $property_name, NULL);
if ($claim && isset($userinfo[$claim])) {
try {
$account_wrapper->{$property_name} = $userinfo[$claim];
} catch (EntityMetadataWrapperException $e) {
watchdog_exception('openid_connect', $e);
}
}
}
if (isset($userinfo['name'])) {
$account->data['oidc_name'] = $userinfo['name'];
}
$account_wrapper
->save();
if (variable_get('user_pictures') && variable_get('openid_connect_user_pictures', TRUE) && !empty($userinfo['picture'])) {
openid_connect_save_user_picture($account, $userinfo['picture']);
}
}
function openid_connect_save_user_picture($account, $picture_url) {
$picture_directory = file_default_scheme() . '://' . variable_get('user_picture_path', 'pictures');
if (!file_prepare_directory($picture_directory, FILE_CREATE_DIRECTORY)) {
return;
}
$response = drupal_http_request($picture_url);
if ($response->code != 200) {
watchdog('openid_connect', 'The user picture could not be fetched from URL: @url', array(
'@url' => $picture_url,
));
return;
}
$hash = md5($response->data);
if (!empty($account->picture) && isset($account->data['oidc_picture_hash']) && $account->data['oidc_picture_hash'] === $hash) {
return;
}
$picture_path = file_stream_wrapper_uri_normalize($picture_directory . '/picture-' . $account->uid . '-' . REQUEST_TIME . '.jpg');
$picture_file = file_save_data($response->data, $picture_path, FILE_EXISTS_REPLACE);
$status_messages = isset($_SESSION['messages']['status']) ? $_SESSION['messages']['status'] : NULL;
file_validate_image_resolution($picture_file, variable_get('user_picture_dimensions', '1024x1024'));
if (isset($status_messages)) {
$_SESSION['messages']['status'] = $status_messages;
}
else {
unset($_SESSION['messages']['status']);
}
$account->picture = $picture_file;
$account->data['oidc_picture_hash'] = $hash;
user_save($account);
}
function openid_connect_login_user($account, &$destination) {
$form_state['uid'] = $account->uid;
$form = array();
if (module_exists('tfa')) {
unset($_GET['code'], $_GET['state']);
$form_state['redirect'] = $destination;
tfa_login_submit($form, $form_state);
tfa_login_form_redirect($form, $form_state);
if (isset($form_state['redirect']) && $form_state['redirect'] != 'user/' . $form_state['uid']) {
$destination = $form_state['redirect'];
}
}
else {
user_login_submit($form, $form_state);
}
}
function openid_connect_save_destination() {
$destination = drupal_get_destination();
$destination = $destination['destination'] == 'user/login' ? 'user' : $destination['destination'];
$parsed = drupal_parse_url($destination);
$_SESSION['openid_connect_destination'] = array(
$parsed['path'],
array(
'query' => $parsed['query'],
),
);
}
function openid_connect_create_user($sub, $userinfo, $client_name) {
$edit = array(
'name' => openid_connect_generate_username($sub, $userinfo, $client_name),
'pass' => user_password(),
'mail' => $userinfo['email'],
'init' => $userinfo['email'],
'status' => 1,
'openid_connect_client' => $client_name,
'openid_connect_sub' => $sub,
);
return user_save(NULL, $edit);
}
function openid_connect_generate_username($sub, $userinfo, $client_name) {
$name = 'oidc_' . $client_name . '_' . $sub;
$candidates = array(
'preferred_username',
'name',
);
foreach ($candidates as $candidate) {
if (!empty($userinfo[$candidate])) {
$name = trim($userinfo[$candidate]);
break;
}
}
for ($original = $name, $i = 1; openid_connect_username_exists($name); $i++) {
$name = $original . '_' . $i;
}
return $name;
}
function openid_connect_username_exists($name) {
return db_query('SELECT COUNT(*) FROM {users} WHERE name = :name', array(
':name' => $name,
))
->fetchField() > 0;
}
function openid_connect_username_alter(&$name, $account) {
if (!empty($account->data['oidc_name']) && (strpos($name, 'oidc_') === 0 || strpos($name, '@'))) {
$name = $account->data['oidc_name'];
}
}
function openid_connect_user_insert(&$edit, $account, $category) {
if (isset($edit['openid_connect_client'])) {
openid_connect_connect_account($account, $edit['openid_connect_client'], $edit['openid_connect_sub']);
}
}
function openid_connect_authmap_delete($uid) {
db_delete('authmap')
->condition('uid', $uid)
->condition('module', db_like('openid_connect_') . '%', 'LIKE')
->execute();
}
function openid_connect_user_delete($account) {
openid_connect_authmap_delete($account->uid);
}
function openid_connect_user_cancel($edit, $account, $method) {
openid_connect_authmap_delete($account->uid);
}
function openid_connect_form_user_profile_form_alter(&$form, &$form_state) {
if (isset($form['account'])) {
$account_form =& $form['account'];
}
else {
$account_form =& $form;
}
if (!empty($account_form['pass']['#access']) && !openid_connect_set_password_access($form['#user'])) {
$account_form['current_pass']['#access'] = FALSE;
$account_form['current_pass_required_values']['#value'] = array();
$account_form['pass']['#access'] = FALSE;
}
}
function openid_connect_set_password_access($account) {
if (user_access('openid_connect set own password', $account)) {
return TRUE;
}
$connected_accounts = openid_connect_get_connected_accounts($account);
return empty($connected_accounts);
}
function openid_connect_user_load_by_sub($sub, $client_name) {
$result = db_select('authmap', 'a')
->fields('a', array(
'uid',
'module',
))
->condition('authname', $sub)
->condition('module', 'openid_connect_' . $client_name)
->execute()
->fetchAssoc();
if ($result) {
$account = user_load($result['uid']);
if (is_object($account)) {
return $account;
}
}
return FALSE;
}
function openid_connect_claims() {
$claims = array(
'name' => array(
'scope' => 'profile',
),
'family_name' => array(
'scope' => 'profile',
),
'given_name' => array(
'scope' => 'profile',
),
'middle_name' => array(
'scope' => 'profile',
),
'nickname' => array(
'scope' => 'profile',
),
'preferred_username' => array(
'scope' => 'profile',
),
'profile' => array(
'scope' => 'profile',
),
'picture' => array(
'scope' => 'profile',
),
'website' => array(
'scope' => 'profile',
),
'gender' => array(
'scope' => 'profile',
),
'birthdate' => array(
'scope' => 'profile',
),
'zoneinfo' => array(
'scope' => 'profile',
),
'locale' => array(
'scope' => 'profile',
),
'updated_at' => array(
'scope' => 'profile',
),
'email' => array(
'scope' => 'email',
),
'email_verified' => array(
'scope' => 'email',
),
'address' => array(
'scope' => 'address',
),
'phone_number' => array(
'scope' => 'phone',
),
'phone_number_verified' => array(
'scope' => 'phone',
),
);
drupal_alter(__FUNCTION__, $claims);
return $claims;
}
function openid_connect_claims_options() {
$options = array();
foreach (openid_connect_claims() as $claim_name => $claim) {
$options[$claim['scope']][$claim_name] = $claim_name;
}
return $options;
}
function openid_connect_get_scopes() {
$claims = variable_get('openid_connect_userinfo_mapping_claims', array());
$scopes = array(
'openid',
'email',
);
$claims_info = openid_connect_claims();
foreach ($claims as $claim) {
if (isset($claims_info[$claim]) && !isset($scopes[$claims_info[$claim]['scope']]) && $claim != 'email') {
$scopes[$claims_info[$claim]['scope']] = $claims_info[$claim]['scope'];
}
}
return implode(' ', $scopes);
}
function _openid_connect_user_properties_to_skip() {
$properties_to_skip = array(
'name',
'mail',
'uid',
'url',
'edit_url',
'last_access',
'last_login',
'created',
'roles',
'status',
'theme',
);
return drupal_map_assoc($properties_to_skip);
}
function openid_connect_log_request_error($method, $client_name, $response) {
switch ($method) {
case 'retrieveTokens':
$message = 'Could not retrieve tokens (@code @error). Details: @details';
break;
case 'retrieveUserInfo':
$message = 'Could not retrieve user profile information (@code @error). Details: @details';
break;
default:
return;
}
$details = '';
if (!empty($response->data)) {
$details = print_r(drupal_json_decode($response->data), TRUE);
}
$variables = array(
'@error' => $response->error,
'@code' => $response->code,
'@details' => $details,
);
watchdog('openid_connect_' . $client_name, $message, $variables, WATCHDOG_ERROR);
}
function openid_connect_entity_property_info_alter(&$info) {
$properties =& $info['user']['properties'];
if (!isset($properties['timezone'])) {
$properties['timezone'] = array(
'label' => t('Time zone'),
'description' => t("The user's time zone."),
'options list' => 'system_time_zones',
'getter callback' => 'entity_property_verbatim_get',
'setter callback' => 'entity_property_verbatim_set',
'schema field' => 'timezone',
);
}
}
function openid_connect_get_connected_accounts($account) {
$auth_maps = db_query("SELECT module, authname FROM {authmap} WHERE uid = :uid AND module LIKE 'openid_connect_%'", array(
':uid' => $account->uid,
));
$module_offset = strlen('openid_connect_');
$results = array();
foreach ($auth_maps as $auth_map) {
$client = substr($auth_map->module, $module_offset);
$sub = $auth_map->authname;
$results[$client] = $sub;
}
return $results;
}
function openid_connect_connect_account($account, $client_name, $sub) {
user_set_authmaps($account, array(
'authname_openid_connect_' . $client_name => $sub,
));
}
function openid_connect_disconnect_account($account, $client_name, $sub = NULL) {
$query = db_delete('authmap');
$query
->condition('uid', $account->uid)
->condition('module', 'openid_connect_' . $client_name);
if ($sub !== NULL) {
$query
->condition('authname', $sub);
}
$query
->execute();
}
function openid_connect_extract_sub($user_data, $userinfo) {
if (!isset($user_data['sub']) && !isset($userinfo['sub'])) {
return FALSE;
}
elseif (!isset($user_data['sub'])) {
return $userinfo['sub'];
}
elseif (isset($userinfo['sub']) && $user_data['sub'] != $userinfo['sub']) {
return FALSE;
}
else {
return $user_data['sub'];
}
}
function openid_connect_complete_authorization($client, $tokens, &$destination) {
if (user_is_logged_in()) {
throw new \RuntimeException('User already logged in');
}
$user_data = $client
->decodeIdToken($tokens['id_token']);
$userinfo = $client
->retrieveUserInfo($tokens['access_token']);
if (empty($userinfo['email'])) {
watchdog('openid_connect', 'No e-mail address provided by @provider', array(
'@provider' => $client
->getLabel(),
), WATCHDOG_ERROR);
return FALSE;
}
$sub = openid_connect_extract_sub($user_data, $userinfo);
if (empty($sub)) {
watchdog('openid_connect', 'No "sub" found from @provider', array(
'@provider' => $client
->getLabel(),
), WATCHDOG_ERROR);
return FALSE;
}
$account = openid_connect_user_load_by_sub($sub, $client
->getName());
$results = module_invoke_all('openid_connect_pre_authorize', $tokens, $account, $userinfo, $client
->getName());
if (in_array(FALSE, $results, TRUE)) {
watchdog('openid_connect', 'Login denied for @email via pre-authorize hook.', array(
'@email' => $userinfo['email'],
), WATCHDOG_ERROR);
return FALSE;
}
if ($account) {
if (variable_get('openid_connect_always_save_userinfo', TRUE)) {
openid_connect_save_userinfo($account, $userinfo);
}
$account_is_new = FALSE;
}
else {
if (!filter_var($userinfo['email'], FILTER_VALIDATE_EMAIL)) {
drupal_set_message(t('The e-mail address %mail is not valid.', array(
'%mail' => $userinfo['email'],
)), 'error');
return FALSE;
}
if (user_load_by_mail($userinfo['email'])) {
drupal_set_message(t('The e-mail address %email is already taken.', array(
'%email' => $userinfo['email'],
)), 'error');
return FALSE;
}
$account = openid_connect_create_user($sub, $userinfo, $client
->getName());
$account = user_load($account->uid);
openid_connect_save_userinfo($account, $userinfo);
$account_is_new = TRUE;
}
openid_connect_login_user($account, $destination);
module_invoke_all('openid_connect_post_authorize', $tokens, $account, $userinfo, $client
->getName(), $account_is_new);
return TRUE;
}
function openid_connect_connect_current_user($client, $tokens) {
global $user;
if (!$user->uid) {
throw new \RuntimeException('User not logged in');
}
$user_data = $client
->decodeIdToken($tokens['id_token']);
$userinfo = $client
->retrieveUserInfo($tokens['access_token']);
$provider_param = array(
'@provider' => $client
->getLabel(),
);
if (empty($userinfo['email'])) {
watchdog('openid_connect', 'No e-mail address provided by @provider', $provider_param, WATCHDOG_ERROR);
return FALSE;
}
$sub = openid_connect_extract_sub($user_data, $userinfo);
if (empty($sub)) {
watchdog('openid_connect', 'No "sub" found from @provider', $provider_param, WATCHDOG_ERROR);
return FALSE;
}
$account = openid_connect_user_load_by_sub($sub, $client
->getName());
$results = module_invoke_all('openid_connect_pre_authorize', $tokens, $account, $userinfo, $client
->getName());
if (in_array(FALSE, $results, TRUE)) {
watchdog('openid_connect', 'Login denied for @email via pre-authorize hook.', array(
'@email' => $userinfo['email'],
), WATCHDOG_ERROR);
return FALSE;
}
if ($account && $account->uid !== $user->uid) {
drupal_set_message(t('Another user is already connected to this @provider account.', $provider_param), 'error');
return FALSE;
}
if (!$account) {
$account = $user;
openid_connect_connect_account($account, $client
->getName(), $sub);
}
if (variable_get('openid_connect_always_save_userinfo', TRUE)) {
openid_connect_save_userinfo($account, $userinfo);
}
module_invoke_all('openid_connect_post_authorize', $tokens, $account, $userinfo, $client
->getName(), FALSE);
return TRUE;
}
function openid_connect_form_user_pass_alter(array &$form, &$form_state) {
$form['#validate'][] = '_openid_connect_user_pass_form_validate';
}
function _openid_connect_user_pass_form_validate(array &$form, &$form_state) {
if (empty($form_state['values']['account'])) {
return;
}
$account = $form_state['values']['account'];
$results = openid_connect_get_connected_accounts($account);
if (empty($results)) {
return;
}
if (user_access('openid_connect set own password', $account)) {
return;
}
form_set_error('name', t('%name is connected to an external authentication system.', array(
'%name' => $form_state['values']['name'],
)));
}