auth0-8.x-2.4/src/Controller/AuthController.php

src/Controller/AuthController.php
<?php

namespace Drupal\auth0\Controller;

/**
 * @file
 * Contains \Drupal\auth0\Controller\AuthController.
 */

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Session\SessionManagerInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Render\Markup;
use Drupal\user\UserInterface;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\Core\PageCache\ResponsePolicyInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

use Drupal\auth0\Event\Auth0UserSigninEvent;
use Drupal\auth0\Event\Auth0UserSignupEvent;
use Drupal\auth0\Event\Auth0UserPreLoginEvent;
use Drupal\auth0\Exception\EmailNotSetException;
use Drupal\auth0\Exception\EmailNotVerifiedException;
use Drupal\auth0\Util\AuthHelper;

use Auth0\SDK\Auth0;
use Auth0\SDK\API\Authentication;
use Auth0\SDK\Configuration\SdkConfiguration;
use Auth0\SDK\Utility\TransientStoreHandler;
use Auth0\SDK\Store\SessionStore;
use GuzzleHttp\ClientInterface;
use Psr\Log\LoggerInterface;

/**
 * Controller routines for auth0 authentication.
 */
class AuthController extends ControllerBase {

  use StringTranslationTrait;

  const SESSION = 'auth0';
  const STATE = 'state';
  const AUTH0_LOGGER = 'auth0_controller';
  const AUTH0_DOMAIN = 'auth0_domain';
  const AUTH0_CLIENT_ID = 'auth0_client_id';
  const AUTH0_CLIENT_SECRET = 'auth0_client_secret';
  const AUTH0_COOKIE_SECRET = 'auth0_cookie_secret';
  const AUTH0_REDIRECT_FOR_SSO = 'auth0_redirect_for_sso';
  const AUTH0_JWT_SIGNING_ALGORITHM = 'auth0_jwt_signature_alg';
  const AUTH0_SECRET_ENCODED = 'auth0_secret_base64_encoded';
  const AUTH0_OFFLINE_ACCESS = 'auth0_allow_offline_access';

  /**
   * The event_dispatcher service.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected EventDispatcherInterface $eventDispatcher;

  /**
   * The temp store.
   *
   * @var \Drupal\Core\TempStore\PrivateTempStore
   */
  protected PrivateTempStore $tempStore;

  /**
   * The current session.
   *
   * @var \Drupal\Core\Session\SessionManagerInterface
   */
  protected SessionManagerInterface $sessionManager;

  /**
   * The logger.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected LoggerInterface $logger;

  /**
   * Logger to log 'auth0' messages.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected LoggerInterface $auth0Logger;

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

  /**
   * The Auth0 client id.
   *
   * @var string|null
   */
  protected ?string $clientId;

  /**
   * The Auth0 client secret.
   *
   * @var string|null
   */
  protected ?string $clientSecret;

  /**
   * If we should redirect for SSO.
   *
   * @var bool
   */
  protected bool $redirectForSso;

  /**
   * If we allow offline access.
   *
   * @var bool
   */
  protected bool $offlineAccess;

  /**
   * The Auth0 cookie secret.
   *
   * @var string|null
   */
  protected ?string $cookieSecret;

  /**
   * The Auth0 helper.
   *
   * @var \Drupal\auth0\Util\AuthHelper
   */
  protected AuthHelper $helper;

  /**
   * The Auth0 SDK.
   *
   * @var \Auth0\SDK\Auth0
   */
  protected Auth0 $auth0;

  /**
   * The http client.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected ClientInterface $httpClient;

  /**
   * Database.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected Connection $database;

  /**
   * Current Request.
   *
   * @var \Symfony\Component\HttpFoundation\Request|null
   */
  protected Request $currentRequest;

  /**
   * Initialize the controller.
   *
   * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
   *   The temp store factory.
   * @param \Drupal\Core\Session\SessionManagerInterface $session_manager
   *   The current session.
   * @param \Drupal\Core\PageCache\ResponsePolicyInterface $page_cache
   *   Page cache.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger factory.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\auth0\Util\AuthHelper $auth0_helper
   *   The Auth0 helper.
   * @param \GuzzleHttp\ClientInterface $http_client
   *   The http client.
   * @param \Drupal\Core\Database\Connection $database
   *   Database Connection.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   Request Stack.
   */
  final public function __construct(
    PrivateTempStoreFactory $temp_store_factory,
    SessionManagerInterface $session_manager,
    ResponsePolicyInterface $page_cache,
    LoggerChannelFactoryInterface $logger_factory,
    EventDispatcherInterface $event_dispatcher,
    ConfigFactoryInterface $config_factory,
    AuthHelper $auth0_helper,
    ClientInterface $http_client,
    Connection $database,
    RequestStack $request_stack
  ) {
    $this->database = $database;
    $this->helper = $auth0_helper;
    $this->httpClient = $http_client;
    $this->eventDispatcher = $event_dispatcher;

    global $base_url;

    // Ensure the pages this controller servers never gets cached.
    /** @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch $page_cache */
    $page_cache->trigger();

    $this->tempStore = $temp_store_factory->get(AuthController::SESSION);
    $this->sessionManager = $session_manager;
    $this->logger = $logger_factory->get(AuthController::AUTH0_LOGGER);
    $this->auth0Logger = $logger_factory->get('auth0');
    $this->config = $config_factory->get('auth0.settings');
    $this->clientId = $this->config->get(AuthController::AUTH0_CLIENT_ID);
    $this->clientSecret = $this->config->get(AuthController::AUTH0_CLIENT_SECRET);
    $this->cookieSecret = $this->config->get(AuthController::AUTH0_COOKIE_SECRET);
    $this->redirectForSso = (bool) $this->config->get(AuthController::AUTH0_REDIRECT_FOR_SSO);
    $this->offlineAccess = (bool) $this->config->get(AuthController::AUTH0_OFFLINE_ACCESS);
    $this->currentRequest = $request_stack->getCurrentRequest();

    $scopes = explode(' ', AUTH0_DEFAULT_SCOPES);
    $sdk_configuration = new SdkConfiguration([
      'domain'        => $this->helper->getAuthDomain(),
      'clientId'     => $this->clientId,
      'clientSecret' => $this->clientSecret,
      'cookieSecret' => $this->cookieSecret,
      'redirectUri'  => "$base_url/auth0/callback",
      'persistUser' => FALSE,
      'scope' => ($this->offlineAccess ? array_merge($scopes, ['offline_access']) : $scopes),
    ]);
    $transient_store = new SessionStore($sdk_configuration);
    $sdk_configuration->setTransientStorage($transient_store);

    $this->auth0 = new Auth0($sdk_configuration);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('tempstore.private'),
      $container->get('session_manager'),
      $container->get('page_cache_kill_switch'),
      $container->get('logger.factory'),
      $container->get('event_dispatcher'),
      $container->get('config.factory'),
      $container->get('auth0.helper'),
      $container->get('http_client'),
      $container->get('database'),
      $container->get('request_stack')
    );
  }

  /**
   * Handles the login page override.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
   * @return array|\Drupal\Core\Routing\TrustedRedirectResponse
   *   The redirect or a renderable array.
   */
  public function login(Request $request) {
    global $base_url;

    $lockExtraSettings = $this->config->get('auth0_lock_extra_settings');

    if (trim($lockExtraSettings) == "") {
      $lockExtraSettings = "{}";
    }

    $returnTo = $request->request->get('returnTo', $request->query->get('returnTo', NULL));

    // If supporting SSO, redirect to the hosted login page for authorization.
    if ($this->redirectForSso) {
      $response = new TrustedRedirectResponse($this->auth0->login($returnTo));
      return $response->send();
    }

    // Not doing SSO, so show login page.
    return [
      '#theme' => 'auth0_login',
      '#loginCSS' => $this->config->get('auth0_login_css'),
      '#attached' => [
        'library' => [
          'auth0/auth0.lock',
        ],
        'drupalSettings' => [
          'auth0' => [
            'clientId' => $this->config->get('auth0_client_id'),
            'domain' => $this->helper->getAuthDomain(),
            'lockExtraSettings' => $lockExtraSettings,
            'configurationBaseUrl' => $this->helper->getTenantCdn($this->config->get('auth0_domain')),
            'showSignup' => $this->config->get('auth0_allow_signup'),
            'callbackURL' => "$base_url/auth0/callback",
            'state' => $this->getState($returnTo),
            'nonce' => $this->getNonce(),
            'scopes' => AUTH0_DEFAULT_SCOPES,
            'offlineAccess' => $this->offlineAccess,
            'formTitle' => $this->config->get('auth0_form_title'),
            'jsonErrorMsg' => $this->t('There was an error parsing the "Lock extra settings" field.'),
          ],
        ],
      ],
    ];
  }

  /**
   * Handles the logout page override.
   *
   * @param string $return
   *   URL to return to after logout.
   *
   * @return \Drupal\Core\Routing\TrustedRedirectResponse
   *   The response after logout.
   *
   * @todo Allow returnTo and frederated parameters.
   * @see https://auth0.com/docs/api/authentication?http#logout
   */
  public function logout(string $return = NULL) {
    global $base_url;
    $auth0Api = new Authentication($this->auth0->configuration());
    user_logout();

    if (!$return) {
      $return = $base_url;
    }
    $response = new TrustedRedirectResponse($auth0Api->getLogoutLink($return));
    return $response->send();
  }

  /**
   * Create a new state in session and return it.
   *
   * @param string|null $returnTo
   *   The return url.
   *
   * @return string
   *   The state string.
   */
  protected function getState($returnTo = NULL) {
    // Have to start the session after putting something into the session, or
    // we don't actually start it!
    if (!$this->sessionManager->isStarted() && !isset($_SESSION['auth0_is_session_started'])) {
      $_SESSION['auth0_is_session_started'] = 'yes';
      $this->sessionManager->regenerate();
    }

    $transientStoreHandler = new TransientStoreHandler(new SessionStore($this->auth0->configuration()));
    $states = $this->tempStore->get(AuthController::STATE);
    if (!is_array($states)) {
      $states = [];
    }
    $state = $transientStoreHandler->issue('state');
    $states[$state] = $returnTo ?? '';
    $this->tempStore->set(AuthController::STATE, $states);

    return $state;
  }

  /**
   * Create a new nonce in session and return it.
   *
   * @return string
   *   The nonce string.
   */
  protected function getNonce() {
    // Have to start the session after putting something into the session, or
    // we don't actually start it!
    if (!$this->sessionManager->isStarted() && !isset($_SESSION['auth0_is_session_started'])) {
      $_SESSION['auth0_is_session_started'] = 'yes';
      $this->sessionManager->regenerate();
    }

    $transientStoreHandler = new TransientStoreHandler(new SessionStore($this->auth0->configuration()));
    $nonce = $transientStoreHandler->issue('nonce');
    $this->tempStore->set('nonce', $nonce);

    return $nonce;
  }

  /**
   * Check for errors.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
   * @return \Drupal\Core\Routing\TrustedRedirectResponse|RedirectResponse|null
   *   The redirect response.
   */
  private function checkForError(Request $request) {
    $error_msg = $this->t('There was a problem logging you in');

    // Check for in URL parameters and REQUEST.
    $error_code = $request->query->get('error', $request->request->get('error'));

    // Error codes that should be redirected back to Auth0 for authentication.
    $redirect_errors = [
      'login_required',
      'interaction_required',
      'consent_required',
    ];
    if ($error_code && in_array($error_code, $redirect_errors)) {
      return new TrustedRedirectResponse($this->auth0->login());
    }
    elseif ($error_code) {
      $error_desc = $request->query->get('error_description', $request->request->get('error_description', $error_code));
      return $this->failLogin($error_msg . ':  ' . $error_desc, $error_desc);
    }

    return NULL;
  }

  /**
   * Handles the callback for the oauth transaction.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
   * @return \Drupal\Core\Routing\TrustedRedirectResponse|null|\Symfony\Component\HttpFoundation\RedirectResponse
   *   The redirect response.
   *
   * @throws \Auth0\SDK\Exception\CoreException
   *   The Auth0 exception.
   */
  public function callback(Request $request) {
    $problem_logging_in_msg = $this->t('There was a problem logging you in, sorry for the inconvenience.');

    $response = $this->checkForError($request);
    if ($response !== NULL) {
      return $response;
    }

    $refreshToken = NULL;

    // Exchange the code for the tokens (happens behind the scenes in the SDK).
    try {
      $this->auth0->exchange();
      $userInfo = $this->auth0->getUser();
      $idToken = $this->auth0->getIdToken();
    }
    catch (\Exception $e) {
      return $this->failLogin(
        $problem_logging_in_msg,
        $this->t('Failed to exchange code for tokens: @exception', ['@exception' => $e->getMessage()])
      );
    }

    if ($this->offlineAccess) {
      try {
        $refreshToken = $this->auth0->getRefreshToken();
      }
      catch (\Exception $e) {
        // Do NOT fail here, just log the error.
        $this->auth0Logger->warning($this->t('Failed getting refresh token: @exception', ['@exception' => $e->getMessage()]));
      }
    }

    try {
      $user = $this->auth0->decode($idToken);
    }
    catch (\Exception $e) {
      return $this->failLogin($problem_logging_in_msg, $this->t('Failed to validate JWT: @exception', ['@exception' => $e->getMessage()]));
    }

    // State value is validated in $this->auth0->getUser() above.
    $returnTo = NULL;
    $validatedState = $request->query->get('state');
    $currentSession = $this->tempStore->get(AuthController::STATE);
    if (!empty($currentSession[$validatedState])) {
      $returnTo = $currentSession[$validatedState];
      unset($currentSession[$validatedState]);
    }

    if ($userInfo) {
      if (empty($userInfo['sub']) && !empty($userInfo['user_id'])) {
        $userInfo['sub'] = $userInfo['user_id'];
      }
      elseif (empty($userInfo['user_id']) && !empty($userInfo['sub'])) {
        $userInfo['user_id'] = $userInfo['sub'];
      }

      if ($userInfo['sub'] != $user->getSubject()) {
        return $this->failLogin($problem_logging_in_msg, $this->t('Failed to verify JWT sub'));
      }

      $this->auth0Logger->notice('Good Login');

      return $this->processUserLogin($request, $userInfo, $refreshToken, $user->getExpiration(), $returnTo);
    }
    else {
      return $this->failLogin($problem_logging_in_msg, 'No userinfo found');
    }
  }

  /**
   * Checks if the email is valid.
   *
   * @param array $userInfo
   *   The user information.
   *
   * @throws \Drupal\auth0\Exception\EmailNotSetException
   *   When an email hasn't been set.
   * @throws \Drupal\auth0\Exception\EmailNotVerifiedException
   *   When an email hasn't been verified.
   */
  protected function validateUserEmail(array $userInfo) {
    $requires_email = $this->config->get('auth0_requires_verified_email');

    if ($requires_email) {
      if (!isset($userInfo['email']) || empty($userInfo['email'])) {
        throw new EmailNotSetException();
      }
      if (!$userInfo['email_verified']) {
        throw new EmailNotVerifiedException();
      }
    }
  }

  /**
   * Process the Auth0 user profile and sign in or sign the user up.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   * @param array $userInfo
   *   The user data info.
   * @param string $refreshToken
   *   The refresh token.
   * @param int $expiresAt
   *   When the token expires.
   * @param string $returnTo
   *   The return url.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   The redirect url.
   *
   * @throws \Exception
   *   An exception.
   */
  protected function processUserLogin(Request $request, array $userInfo, $refreshToken, $expiresAt, $returnTo) {
    $this->auth0Logger->notice('process user login');

    $event = new Auth0UserPreLoginEvent($userInfo);
    $this->eventDispatcher->dispatch($event);

    try {
      $this->validateUserEmail($userInfo);

      // See if there is a user in the auth0_user table with the user
      // info client ID.
      $this->auth0Logger->notice($userInfo['user_id'] . ' looking up Drupal user by Auth0 user_id');
      $user = $this->findAuth0User($userInfo['user_id']);

      if ($user) {
        $this->auth0Logger->notice('uid of existing Drupal user found');

        // User exists, update the auth0_user with the new userInfo object.
        $this->updateAuth0User($userInfo);

        // Update field and role mappings.
        $this->auth0UpdateFieldsAndRoles($userInfo, $user);

        $event = new Auth0UserSigninEvent($user, $userInfo, $refreshToken, $expiresAt);
        $this->eventDispatcher->dispatch($event);
      }
      else {
        $this->auth0Logger->notice('existing Drupal user NOT found');

        $user = $this->signupUser($userInfo);

        $this->insertAuth0User($userInfo, $user->id());

        $event = new Auth0UserSignupEvent($user, $userInfo);
        $this->eventDispatcher->dispatch($event);
      }
    }
    catch (EmailNotSetException $e) {
      return $this->failLogin($this->t('This account does not have an email associated. Please login with a different provider.'), 'No Email Found');
    }
    catch (EmailNotVerifiedException $e) {
      return $this->auth0FailWithVerifyEmail();
    }

    user_login_finalize($user);

    if ($returnTo) {
      return new RedirectResponse($returnTo);
    }
    elseif ($request->request->has('destination')) {
      return new RedirectResponse($request->request->get('destination'));
    }

    return $this->redirect('entity.user.canonical', ['user' => $user->id()]);
  }

  /**
   * Fails user login.
   *
   * @param string $message
   *   The message to display.
   * @param string $logMessage
   *   The message to log.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   The redirect response after fail.
   */
  protected function failLogin($message, $logMessage) {
    $this->messenger()->addError($message);
    $this->logger->error($logMessage);
    $this->auth0->logout();
    return new RedirectResponse('/');
  }

  /**
   * Create or link a new user based on the auth0 profile.
   *
   * @param array $userInfo
   *   The user info data array.
   *
   * @return bool|mixed
   *   The user object.
   *
   * @throws \Drupal\auth0\Exception\EmailNotVerifiedException
   *   The email not verified exception.
   * @throws \Exception
   */
  protected function signupUser(array $userInfo) {
    // If the user doesn't exist we need to either create a new one,
    // or assign them to an existing one.
    $isDatabaseUser = FALSE;

    $user_sub_arr = explode('|', $userInfo['user_id']);
    $provider = $user_sub_arr[0];

    if ('auth0' === $provider) {
      $isDatabaseUser = TRUE;
    }

    $joinUser = FALSE;

    $user_name_claim = $this->config->get('auth0_username_claim') ?: AUTH0_DEFAULT_USERNAME_CLAIM;

    // Drupal usernames do not allow pipe characters.
    $user_name_used = !empty($userInfo[$user_name_claim])
      ? $userInfo[$user_name_claim]
      : str_replace('|', '_', $userInfo['user_id']);

    if ($this->config->get('auth0_join_user_by_mail_enabled') && !empty($userInfo['email'])) {
      $this->auth0Logger->notice($userInfo['email'] . ' join user by mail is enabled, looking up user by email');
      // If the user has a verified email or is a database user try to see if
      // there is a user to join with. The isDatabase is because we don't want
      // to allow database user creation if there is an existing one with no
      // verified email.
      if ($userInfo['email_verified'] || $isDatabaseUser) {
        $joinUser = user_load_by_mail($userInfo['email']);
      }
    }
    else {
      $this->auth0Logger->notice($user_name_used . ' join user by username');

      if (!empty($userInfo['email_verified']) || $isDatabaseUser) {
        $joinUser = user_load_by_name($user_name_used);
      }
    }

    if ($joinUser) {
      /** @var \Drupal\user\UserInterface $joinUser */
      $this->auth0Logger->notice($joinUser->id() . ' Drupal user found by email with uid');

      // If we are here, we have a potential join user.
      // Don't allow creation or assignation of user if the email is not
      // verified, that would be hijacking.
      if (!$userInfo['email_verified']) {
        throw new EmailNotVerifiedException();
      }
      $user = $joinUser;
    }
    else {
      $this->auth0Logger->notice($user_name_used . ' creating new Drupal user from Auth0 user');

      // If we are here, we need to create the user.
      $user = $this->createDrupalUser($userInfo);

      // Update field and role mappings.
      $this->auth0UpdateFieldsAndRoles($userInfo, $user);
    }

    return $user;
  }

  /**
   * Email not verified error message.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   The redirect response.
   */
  protected function auth0FailWithVerifyEmail() {
    $messageHtml = sprintf('<p>%s.</p>',
      $this->t('Please verify your email and log in again')
    );

    return $this->failLogin(Markup::create($messageHtml), 'Email not verified');
  }

  /**
   * Get the auth0 user profile.
   */
  protected function findAuth0User($id) {
    $auth0_user = $this->database->select('auth0_user', 'a')
      ->fields('a', ['drupal_id'])
      ->condition('auth0_id', $id, '=')
      ->execute()
      ->fetchAssoc();

    return empty($auth0_user) ? FALSE : $this->entityTypeManager()->getStorage('user')->load($auth0_user['drupal_id']);
  }

  /**
   * Update the auth0 user profile.
   *
   * @param array $userInfo
   *   The user info array.
   */
  protected function updateAuth0User(array $userInfo) {
    $this->database->update('auth0_user')
      ->fields([
        'auth0_object' => serialize($userInfo),
      ])
      ->condition('auth0_id', $userInfo['user_id'], '=')
      ->execute();
  }

  /**
   * Update the Auth fields.
   *
   * @param array $userInfo
   *   The user info array.
   * @param \Drupal\user\UserInterface $user
   *   The Drupal user entity.
   */
  protected function auth0UpdateFieldsAndRoles(array $userInfo, UserInterface $user) {

    $edit = [];
    $this->auth0UpdateFields($userInfo, $user, $edit);
    $this->auth0UpdateRoles($userInfo, $user, $edit);

    $user->save();
  }

  /**
   * Update the $user profile attributes based on the auth0 field mappings.
   *
   * @param array $userInfo
   *   The user info array.
   * @param \Drupal\user\UserInterface $user
   *   The Drupal user entity.
   * @param array $edit
   *   The edit array.
   */
  protected function auth0UpdateFields(array $userInfo, UserInterface $user, array &$edit) {
    $auth0_claim_mapping = $this->config->get('auth0_claim_mapping');

    if (isset($auth0_claim_mapping) && !empty($auth0_claim_mapping)) {
      // For each claim mapping, lookup the value, otherwise set to blank.
      $mappings = $this->auth0PipeListToArray($auth0_claim_mapping);

      // Remove mappings handled automatically by the module.
      $skip_mappings = [
        'uid',
        'name',
        'mail',
        'init',
        'is_new',
        'status',
        'pass',
      ];

      foreach ($mappings as $mapping) {
        $this->auth0Logger->notice('mapping ' . $mapping);

        $key = $mapping[1];
        if (in_array($key, $skip_mappings)) {
          $this->auth0Logger->notice('skipping mapping handled already by Auth0 module ' . $mapping);
        }
        else {
          $value = $userInfo[$mapping[0]] ?? '';
          $current_value = $user->get($key)->value;
          if ($current_value === $value) {
            $this->auth0Logger->notice('value is unchanged ' . $key);
          }
          else {
            $this->auth0Logger->notice('value changed ' . $key . ' from [' . $current_value . '] to [' . $value . ']');
            $edit[$key] = $value;
            $user->set($key, $value);
          }
        }
      }
    }
  }

  /**
   * Updates the $user->roles of a user based on the Auth0 role mappings.
   *
   * @param array $userInfo
   *   The user info array.
   * @param \Drupal\user\UserInterface $user
   *   The drupal user entity.
   * @param array $edit
   *   The edit array.
   */
  protected function auth0UpdateRoles(array $userInfo, UserInterface $user, array &$edit) {
    $this->auth0Logger->notice("Mapping Roles");
    $auth0_claim_to_use_for_role = $this->config->get('auth0_claim_to_use_for_role');

    if (isset($auth0_claim_to_use_for_role) && !empty($auth0_claim_to_use_for_role)) {
      $claim_value = $userInfo[$auth0_claim_to_use_for_role] ?? '';
      $this->auth0Logger->notice('claim_value ' . print_r($claim_value, TRUE));

      $claim_values = [];
      if (is_array($claim_value)) {
        $claim_values = $claim_value;
      }
      else {
        $claim_values[] = $claim_value;
      }

      $auth0_role_mapping = $this->config->get('auth0_role_mapping');
      $mappings = $this->auth0PipeListToArray($auth0_role_mapping);

      $roles_granted = [];
      $roles_managed_by_mapping = [];

      foreach ($mappings as $mapping) {
        $this->auth0Logger->notice('mapping ' . print_r($mapping, TRUE));
        $roles_managed_by_mapping[] = $mapping[1];

        if (in_array($mapping[0], $claim_values)) {
          $roles_granted[] = $mapping[1];
        }
      }

      $roles_granted = array_unique($roles_granted);
      $roles_managed_by_mapping = array_unique($roles_managed_by_mapping);

      $not_granted = array_diff($roles_managed_by_mapping, $roles_granted);

      $user_roles = $user->getRoles();

      $new_user_roles = array_merge(array_diff($user_roles, $not_granted), $roles_granted);

      $roles_to_add = array_diff($new_user_roles, $user_roles);
      $roles_to_remove = array_diff($user_roles, $new_user_roles);

      if (empty($roles_to_add) && empty($roles_to_remove)) {
        $this->auth0Logger->notice('no changes to roles detected');
        return;
      }

      $this->auth0Logger->notice('changes to roles detected');
      $edit['roles'] = $new_user_roles;

      foreach ($roles_to_add as $new_role) {
        $user->addRole($new_role);
      }
      foreach ($roles_to_remove as $remove_role) {
        $user->removeRole($remove_role);
      }
    }
  }

  /**
   * Convert mappings to a pipelist.
   *
   * @param array $mappings
   *   The mappings array.
   *
   * @return string
   *   The result of the conversion.
   */
  protected function auth0MappingsToPipeList(array $mappings) {
    $result_text = "";
    foreach ($mappings as $map) {
      $result_text .= $map['from'] . '|' . $map['user_entered'] . "\n";
    }
    return $result_text;
  }

  /**
   * Convert pipe list to array.
   *
   * @param string $mappingListTxt
   *   The pipe list string.
   *
   * @return array
   *   An array of items.
   */
  protected function auth0PipeListToArray($mappingListTxt) {
    $return = [];
    $mappings = explode(PHP_EOL, $mappingListTxt);
    foreach ($mappings as $line) {
      if (empty($line) || FALSE === strpos($line, '|')) {
        continue;
      }
      $line_parts = explode('|', $line);
      $return[] = [trim($line_parts[0]), trim($line_parts[1])];
    }
    return $return;
  }

  /**
   * Insert the Auth0 user.
   *
   * @param array $userInfo
   *   The user info array.
   * @param int $uid
   *   The Drupal user id.
   *
   * @throws \Exception
   */
  protected function insertAuth0User(array $userInfo, $uid) {

    $this->database->insert('auth0_user')->fields([
      'auth0_id' => $userInfo['user_id'],
      'drupal_id' => $uid,
      'auth0_object' => json_encode($userInfo),
    ])->execute();

  }

  /**
   * Get random bytes string.
   *
   * @param int $nbBytes
   *   The number of bytes to generate.
   *
   * @return string
   *   The generated string.
   *
   * @throws \Exception
   */
  private function getRandomBytes($nbBytes = 32) {
    $bytes = openssl_random_pseudo_bytes($nbBytes, $strong);
    if (FALSE !== $bytes && TRUE === $strong) {
      return $bytes;
    }
    else {
      throw new \Exception("Unable to generate secure token from OpenSSL.");
    }
  }

  /**
   * Generate a random password.
   *
   * @param int $length
   *   The length of the password.
   *
   * @return string
   *   The generated password.
   *
   * @throws \Exception
   */
  private function generatePassword($length) {
    return substr(preg_replace("/[^a-zA-Z0-9]\+\//", "", base64_encode($this->getRandomBytes($length + 1))), 0, $length);
  }

  /**
   * Create the Drupal user based on the Auth0 user profile.
   *
   * @param array $userInfo
   *   The user info array.
   *
   * @return \Drupal\user\UserInterface
   *   The Drupal user entity.
   *
   * @throws \Exception
   */
  protected function createDrupalUser(array $userInfo) {
    $user_name_claim = $this->config->get('auth0_username_claim');
    if ($user_name_claim == '') {
      $user_name_claim = 'nickname';
    }

    /** @var \Drupal\user\UserInterface $user */
    $user = $this->entityTypeManager()->getStorage('user')->create();

    $user->setPassword($this->generatePassword(16));
    $user->enforceIsNew();

    if (!empty($userInfo['email'])) {
      $user->setEmail($userInfo['email']);
    }
    else {
      $user->setEmail("change_this_email@" . uniqid() . ".com");
    }

    // If the username already exists, create a new random one.
    $username = !empty($userInfo[$user_name_claim])
      ? $userInfo[$user_name_claim]
      : $userInfo['user_id'];

    if (user_load_by_name($username)) {
      $username .= time();
    }

    $user->setUsername($username);
    $user->activate();
    $user->save();

    return $user;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc