ppoidc-8.x-1.2/pixelpin_openid_connect.module
pixelpin_openid_connect.module
<?php
/**
* @file
* A pluggable client implementation for the OpenID Connect protocol.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\user\Entity\User;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Implements hook_entity_property_info_alter().
*/
function pixelpin_openid_connect_entity_property_info_alter(&$info) {
$properties = &$info['user']['properties'];
if (!isset($properties['timezone'])) {
// Adds the missing timezone property.
$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',
);
}
}
/**
* Implements hook_user_insert().
*/
function pixelpin_openid_connect_user_insert(EntityInterface $entity) {
if (isset($edit['pixelpin_openid_connect_client'])) {
pixelpin_openid_connect_connect_account($entity, $edit['pixelpin_openid_connect_client'], $edit['pixelpin_openid_connect_sub']);
}
}
/**
* Implements hook_user_cancel().
*/
function pixelpin_openid_connect_user_cancel($edit, $account, $method) {
$authmap = \Drupal::service('pixelpin_openid_connect.authmap');
$authmap->deleteAssociation($account->id());
}
/**
* Implements hook_ENTITY_TYPE_delete().
*/
function pixelpin_openid_connect_user_delete(EntityInterface $entity) {
$authmap = \Drupal::service('pixelpin_openid_connect.authmap');
$authmap->deleteAssociation($entity->id());
}
/**
* Implements hook_user_format_name_alter().
*/
function pixelpin_openid_connect_user_format_name_alter(&$name, $account) {
// Ensure that usernames are not displayed if they are email addresses, or if
// they are generated names starting with 'oidc_'.
$oidc_name = \Drupal::service('user.data')->get('pixelpin_openid_connect', $account->id(), 'oidc_name');
if (!empty($oidc_name) && (strpos($name, 'oidc_') === 0 || strpos($name, '@'))) {
$name = $oidc_name;
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function pixelpin_openid_connect_form_user_form_alter(&$form, &$form_state) {
if (isset($form['account'])) {
$account_form = &$form['account'];
}
else {
$account_form = &$form;
}
$account = \Drupal::currentUser();
$access = pixelpin_openid_connect_set_password_access($account);
if (!$access) {
$account_form['current_pass']['#access'] = FALSE;
$account_form['current_pass_required_values']['#value'] = array();
$account_form['pass']['#access'] = FALSE;
$account_form['pass']['#required'] = FALSE;
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function pixelpin_openid_connect_form_user_profile_form_alter(&$form, &$form_state) {
if (isset($form['account'])) {
$account_form = &$form['account'];
}
else {
$account_form = &$form;
}
$account = \Drupal::currentUser();
if (!empty($account_form['pass']['#access']) && !pixelpin_openid_connect_set_password_access($account)) {
$account_form['current_pass']['#access'] = FALSE;
$account_form['current_pass_required_values']['#value'] = array();
$account_form['pass']['#access'] = FALSE;
}
}
/**
* Saves user profile information into a user account.
*
* @param \Drupal\user\UserInterface $account
* An user account object.
* @param array $userinfo
* An array with information about the user.
*/
function pixelpin_openid_connect_save_userinfo($account, $userinfo) {
$properties = \Drupal::entityManager()->getFieldDefinitions('user', 'user');
$properties_skip = _pixelpin_openid_connect_user_properties_to_skip();
foreach ($properties as $property_name => $property) {
if (isset($properties_skip[$property_name])) {
continue;
}
$userinfo_mappings = \Drupal::config('pixelpin_openid_connect.settings')
->get('userinfo_mappings');
if (isset($userinfo_mappings[$property_name])) {
$claim = $userinfo_mappings[$property_name];
if ($claim && isset($userinfo[$claim])) {
$property_type = $property->getType();
// Set the user property, while ignoring exceptions from invalid values.
try {
if ($property_type === 'string') {
$account->set($property_name, $userinfo[$claim]);
}
elseif ($property_type === 'image') {
// Create file object from remote URL.
$basename = explode('?', drupal_basename($userinfo[$claim]))[0];
$data = file_get_contents($userinfo[$claim]);
$file = file_save_data($data, 'public://user-picture-' . $account->id() . '-' . $basename, FILE_EXISTS_REPLACE);
$account->set($property_name, ['target_id' => $file->id()]);
}
}
catch (\InvalidArgumentException $e) {
\Drupal::logger('pixelpin_openid_connect')->error($e->getMessage());
}
}
}
}
// Save the display name additionally in the user account 'data', for use in
// pixelpin_openid_connect_username_alter().
if (isset($userinfo['name'])) {
\Drupal::service('user.data')->set('pixelpin_openid_connect', $account->id(), 'oidc_name', $userinfo['name']);
}
$account->save();
}
/**
* Logs in a user.
*
* @param \Drupal\user\UserInterface $account
* The user account.
*/
function pixelpin_openid_connect_login_user($account) {
user_login_finalize($account);
}
/**
* Save the current path in the session, for redirecting after authorization.
*/
function pixelpin_openid_connect_save_destination() {
$destination = \Drupal::destination()->getAsArray();
$destination = $destination['destination'] == 'user/login' ? 'user' : $destination['destination'];
// The destination could contain query parameters. Ensure that they are
// preserved.
$parsed = parse_url($destination);
$_SESSION['pixelpin_openid_connect_destination'] = array(
$parsed['path'], array('query' => isset($parsed['query']) ? $parsed['query'] : ''),
);
}
/**
* Creates a user indicating sub-id and login provider.
*
* @param string $sub
* The subject identifier.
* @param array $userinfo
* The user claims, containing at least 'email'.
* @param string $client_name
* The machine name of the client.
*
* @return object|FALSE
* The user object or FALSE on failure.
*/
function pixelpin_openid_connect_create_user($sub, $userinfo, $client_name) {
/** @var \Drupal\user\Entity\User $account */
$address = $userinfo['address'];
$decodeAddress = json_decode($address);
$firstName = $userinfo['given_name'];
$lastName = $userinfo['family_name'];
$streetAddress2 = $decodeAddress->{"street_address"};
$townCity2 = $decodeAddress->{"locality"};
$region2 = $decodeAddress->{"region"};
$postalCode2 = $decodeAddress->{"postal_code"};
$country2 = $decodeAddress->{"country"};
$streetAddress = (string)$streetAddress2;
$townCity = (string)$townCity2;
$region = (string)$region2;
$postalCode = (string)$postalCode2;
$country = (string)$country2;
$account = User::create([
'name' => $firstName.$lastName.$sub,
'pass' => user_password(),
'mail' => $userinfo['email'],
'init' => $userinfo['email'],
'family_name' => $userinfo['family_name'],
'given_name' => $userinfo['given_name'],
'nickname' => $userinfo['nickname'],
'status' => 1,
'pixelpin_openid_connect_client' => $client_name,
'pixelpin_openid_connect_sub' => $sub,
]);
$account->save();
return $account;
}
/**
* Generate a username for a new account.
*
* @param string $sub
* The subject identifier.
* @param array $userinfo
* The user claims.
* @param string $client_name
* The client identifier.
*
* @return string
* A unique username.
*/
function pixelpin_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;
}
}
// Ensure there are no duplicates.
for ($original = $name, $i = 1; pixelpin_openid_connect_username_exists($name); $i++) {
$name = $original . '_' . $i;
}
return $name;
}
/**
* Check if a user name already exists.
*
* @param string $name
* A name to test.
*
* @return bool
* TRUE if a user exists with the given name, FALSE otherwise.
*/
function pixelpin_openid_connect_username_exists($name) {
return db_query('SELECT COUNT(*) FROM {users_field_data} WHERE name = :name', array(
':name' => $name,
))->fetchField() > 0;
}
/**
* Find whether the user is allowed to change their own password.
*
* @param object $account
* A user account object.
*
* @return bool
* TRUE if access is granted, FALSE otherwise.
*/
function pixelpin_openid_connect_set_password_access($account) {
if ($account->hasPermission('openid connect set own password')) {
return TRUE;
}
/* @var \Drupal\pixelpin_openid_connect\Authmap $authmap */
$authmap = \Drupal::service('pixelpin_openid_connect.authmap');
$connected_accounts = $authmap->getConnectedAccounts($account);
return empty($connected_accounts);
}
/**
* Returns user properties that can be skipped when mapping user profile info.
*/
function _pixelpin_openid_connect_user_properties_to_skip() {
$properties_to_skip = array(
'uid', 'uuid', 'langcode', 'preferred_langcode', 'preferred_admin_langcode',
'name', 'pass', 'mail', 'status', 'created', 'changed', 'access', 'login',
'init', 'roles', 'default_langcode',
);
\Drupal::moduleHandler()->alter(__FUNCTION__, $properties_to_skip);
return array_combine($properties_to_skip, $properties_to_skip);
}
/**
* Connect an external OpenID Connect account to a Drupal user account.
*
* @param object $account
* The Drupal user object.
* @param string $client_name
* The client machine name.
* @param string $sub
* The 'sub' property identifying the external account.
*/
function pixelpin_openid_connect_connect_account($account, $client_name, $sub) {
/* @var \Drupal\pixelpin_openid_connect\Authmap $authmap */
$authmap = \Drupal::service('pixelpin_openid_connect.authmap');
$authmap->createAssociation($account, $client_name, $sub);
}
/**
* Disconnect an external OpenID Connect account from a Drupal user account.
*
* @param object $account
* The Drupal user object.
* @param string $client_name
* The client machine name.
*/
function pixelpin_openid_connect_disconnect_account($account, $client_name) {
/* @var \Drupal\pixelpin_openid_connect\Authmap $authmap */
$authmap = \Drupal::service('pixelpin_openid_connect.authmap');
$authmap->deleteAssociation($account->id(), $client_name);
}
/**
* Get the 'sub' property from the user data and/or user claims.
*
* The 'sub' (Subject Identifier) is a unique ID for the external provider to
* identify the user.
*
* @param array $user_data
* The user data as returned from
* OpenIDConnectClientInterface::decodeIdToken().
* @param array $userinfo
* The user claims as returned from
* OpenIDConnectClientInterface::retrieveUserInfo().
*
* @return string|FALSE
* The sub, or FALSE if there was an error.
*/
function pixelpin_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;
}
return $user_data['sub'];
}
/**
* Complete the authorization after tokens have been retrieved.
*
* @param object $client
* The client.
* @param array $tokens
* The tokens as returned from OpenIDConnectClientInterface::retrieveTokens().
* @param string|array &$destination
* The path to redirect to after authorization.
*
* @return bool
* TRUE on success, FALSE on failure.
*/
function pixelpin_openid_connect_complete_authorization($client, $tokens, &$destination) {
if (\Drupal::currentUser()->isAuthenticated()) {
throw new \RuntimeException('User already logged in');
}
/* @var \Drupal\pixelpin_openid_connect\Authmap $authmap */
$authmap = \Drupal::service('pixelpin_openid_connect.authmap');
$user_data = $client->decodeIdToken($tokens['id_token']);
$userinfo = $client->retrieveUserInfo($tokens['access_token']);
$logger = \Drupal::logger('pixelpin_openid_connect');
if ($userinfo && empty($userinfo['email'])) {
$message = 'No e-mail address provided by PixelPin';
$variables = array('@provider' => $client->getPluginId());
$logger->error($message . ' (@code @error). Details: @details', $variables);
return FALSE;
}
$sub = pixelpin_openid_connect_extract_sub($user_data, $userinfo);
if (empty($sub)) {
$message = 'No "sub" found from PixelPin';
$variables = array('@provider' => $client->getPluginId());
$logger->error($message . ' (@code @error). Details: @details', $variables);
return FALSE;
}
/* @var \Drupal\user\UserInterface $account */
$account = $authmap->userLoadBySub($sub, $client->getPluginId());
if ($account) {
// An existing account was found. Save user claims.
if (\Drupal::config('pixelpin_openid_connect.settings')->get('always_save_userinfo')) {
pixelpin_openid_connect_save_userinfo($account, $userinfo);
}
}
else {
// Check whether the e-mail address is valid.
if (!\Drupal::service('email.validator')->isValid($userinfo['email'])) {
drupal_set_message(
t('The e-mail address is not valid: @email',
array(
'@email' => $userinfo['email'],
)
),
'error'
);
return FALSE;
}
// Check whether there is an e-mail address conflict.
if (user_load_by_mail($userinfo['email'])) {
drupal_set_message(
t('The e-mail address is already taken: @email',
array(
'@email' => $userinfo['email'],
)
),
'error'
);
return FALSE;
}
// Create a new account.
$account = pixelpin_openid_connect_create_user($sub, $userinfo, $client->getPluginId());
pixelpin_openid_connect_save_userinfo($account, $userinfo);
$authmap->createAssociation($account, $client->getPluginId(), $sub);
}
pixelpin_openid_connect_login_user($account);
\Drupal::moduleHandler()->invokeAll('pixelpin_openid_connect_post_authorize', array(
$tokens, $account, $userinfo, $client->getPluginId(),
));
return TRUE;
}
/**
* Connect the current user's account to an external provider.
*
* @param object $client
* The client.
* @param array $tokens
* The tokens as returned from OpenIDConnectClientInterface::retrieveTokens().
*
* @return bool
* TRUE on success, FALSE on failure.
*/
function pixelpin_openid_connect_connect_current_user($client, $tokens) {
/* @var \Drupal\Core\Session\AccountProxyInterface $user */
$user = \Drupal::currentUser();
if (!$user->isAuthenticated()) {
throw new \RuntimeException('User not logged in');
}
/* @var \Drupal\pixelpin_openid_connect\Authmap $authmap */
$authmap = \Drupal::service('pixelpin_openid_connect.authmap');
$user_data = $client->decodeIdToken($tokens['id_token']);
$userinfo = $client->retrieveUserInfo($tokens['access_token']);
/* @var \Psr\Log\LoggerInterface $logger */
$logger = \Drupal::logger('pixelpin_openid_connect');
$provider_param = array('@provider' => $client->getPluginId());
if ($userinfo && empty($userinfo['email'])) {
$message = 'No e-mail address provided by PixelPin';
$variables = $provider_param;
$logger->error($message . ' (@code @error). Details: @details', $variables);
return FALSE;
}
$sub = pixelpin_openid_connect_extract_sub($user_data, $userinfo);
if (empty($sub)) {
$message = 'No "sub" found from PixelPin';
$variables = $provider_param;
$logger->error($message . ' (@code @error). Details: @details', $variables);
return FALSE;
}
/* @var \Drupal\user\UserInterface $account */
$account = $authmap->userLoadBySub($sub, $client->getPluginId());
if ($account && $account->id() !== $user->id()) {
drupal_set_message(t('Another user is already connected to this PixelPin account.', $provider_param), 'error');
return FALSE;
}
if (!$account) {
$account = User::load($user->id());
pixelpin_openid_connect_connect_account($account, $client->getPluginId(), $sub);
}
$always_save_userinfo = \Drupal::config('pixelpin_openid_connect.settings')->get('always_save_userinfo');
if ($always_save_userinfo) {
pixelpin_openid_connect_save_userinfo($account, $userinfo);
}
\Drupal::moduleHandler()->invokeAll('pixelpin_openid_connect_post_authorize', array(
$tokens, $account, $userinfo, $client->getPluginId(),
));
return TRUE;
}
