Allow use with Email Registration module

Created on 30 September 2024, 7 months ago

Problem/Motivation

TFA module doesn't work well with the Email Registration module because they both override UserAuthenticationController.

TfaUserAuthenticationController.php

  /**
   * {@inheritdoc}
   */
  public function login(Request $request) {

    $format = $this->getRequestFormat($request);
    $content = $request->getContent();
    $credentials = $this->serializer->decode($content, $format);

    if (!isset($credentials['name'])) {
      throw new BadRequestHttpException('Missing credentials.name.');
    }

    /** @var \Drupal\user\UserInterface[] $users */
    $users = $this->userStorage->loadByProperties(['name' => $credentials['name']]);

    if (count($users) !== 1) {
      throw new BadRequestHttpException('Sorry, unrecognized username or password.');
    }

    $this->setUser(reset($users));

    // TFA Disabled globally or not enabled for user
    // we allow the core controller to process.
    if ($this->isTfaDisabled()) {
      return parent::login($request);
    }

    // Reject the request if TFA is enabled for the user, even if it is not
    // yet fully configured. We use the not activated message to avoid leaking
    // information about TFA status.
    throw new AccessDeniedHttpException('The user has not been activated or is blocked.');

  }

Email registration's UserHttpAuthenticationController.php


  /**
   * {@inheritdoc}
   */
  public function login(Request $request) {
    $format = $this->getRequestFormat($request);
    $content = $request->getContent();
    $credentials = $this->serializer->decode($content, $format);

    // If a name is provided, fallback to
    // \Drupal\user\Controller\UserAuthenticationController::login.
    if (isset($credentials['name'])) {
      return parent::login($request);
    }

    if (!isset($credentials['mail']) && !isset($credentials['pass'])) {
      throw new BadRequestHttpException('Missing credentials.');
    }

    if (!isset($credentials['mail'])) {
      throw new BadRequestHttpException('Missing credentials.mail.');
    }
    if (!isset($credentials['pass'])) {
      throw new BadRequestHttpException('Missing credentials.pass.');
    }

    // Set default.
    $name = '';

    // If an email is provided, find the associated name.
    if ($user = user_load_by_mail($credentials['mail'])) {
      $name = $user->getAccountName();

      $this->floodControl($request, $name);

      if ($this->userIsBlocked($name)) {
        throw new BadRequestHttpException('The user has not been activated or is blocked.');
      }

      if ($uid = $this->userAuth->authenticate($name, $credentials['pass'])) {
        $this->userFloodControl->clear('user.http_login', $this->getLoginFloodIdentifier($request, $name));
        /** @var \Drupal\user\UserInterface $user */
        $user = $this->userStorage->load($uid);
        $this->userLoginFinalize($user);

        // Send basic metadata about the logged in user.
        $response_data = [];
        if ($user->get('uid')->access('view', $user)) {
          $response_data['current_user']['uid'] = $user->id();
        }
        if ($user->get('roles')->access('view', $user)) {
          $response_data['current_user']['roles'] = $user->getRoles();
        }
        if ($user->get('mail')->access('view', $user)) {
          $response_data['current_user']['mail'] = $user->getEmail();
        }
        $response_data['csrf_token'] = $this->csrfToken->get('rest');

        $logout_route = $this->routeProvider->getRouteByName('user.logout.http');
        // Trim '/' off path to match \Drupal\Core\Access\CsrfAccessCheck.
        $logout_path = ltrim($logout_route->getPath(), '/');
        $response_data['logout_token'] = $this->csrfToken->get($logout_path);

        $encoded_response_data = $this->serializer->encode($response_data, $format);
        return new Response($encoded_response_data);
      }
    }

    $flood_config = $this->config('user.flood');
    if ($identifier = $this->getLoginFloodIdentifier($request, $name)) {
      $this->userFloodControl->register('user.http_login', $flood_config->get('user_window'), $identifier);
    }

    // Always register an IP-based failed login event.
    $this->userFloodControl->register('user.failed_login_ip', $flood_config->get('ip_window'));
    throw new BadRequestHttpException('Sorry, unrecognized email or password.');
  }

Proposed resolution

Provide a way for TFA to handle $credentials['mail'] if Email Registration is enabled.

✨ Feature request
Status

Active

Version

1.0

Component

Code

Created by

πŸ‡―πŸ‡΅Japan ptmkenny

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @ptmkenny
  • πŸ‡΅πŸ‡ΉPortugal jcnventura

    If the compatibility problem is arising here, there is not much that can be done. This login controller is not used for the normal functioning of the module, but as a lock to prevent use of the user.login.http REST API. This because we haven't found a way to request TFA over the REST API, so we gave up and simply locked that path completely, so as to not open a security hole by allowing TFA to be bypassed completely on that method. If you want to skip this horrible, horrible kludge simply use branch 2.x of the module, in which this was never added (and is of course totally open to TFA bypass...).

    If you want a site with this TFA module that is not trivially bypassed via REST API, it simply can't be decoupled. Maybe other TFA modules have better solutions to this, and we would LOVE to have help in moving the 2.x branch to an event-based solution. That solution would not rely on overwriting the login form (and controller in this case), but would actually hook into every request and insert a few additional steps on some cases (login via /user/login form, login via REST API, login via smoke signals and password reset), to request the TFA token from the user in those cases.

  • πŸ‡ΊπŸ‡ΈUnited States cmlara

    This because we haven't found a way to request TFA over the REST API… in which this was never added (and is of course totally open to TFA bypass...)

    2.x should have secure login support (including REST) since πŸ› Installing contrib modules can lead to TFA accidently being bypassed Fixed was committed. See https://project.pages.drupalcode.org/tfa/technical/set-user-protection/ for the technical details of how it implements protection for 2.x going forward.

    We just have not implemented password reset yet (and it is now failing secure due to the overall event processing code). Even the password reset code is mostly there at this point (it works, just a new scenario was raised that allowing time for feedback to see if we can modify the process to be even more flexible).

    I need to update[#3374221].

  • πŸ‡―πŸ‡΅Japan ptmkenny

    Thank you both for the quick response! That documentation is fantastic, great work!

Production build 0.71.5 2024