Skip to content

Commit

Permalink
[SD-375] bypass tfa during pass reset use prlp (#532)
Browse files Browse the repository at this point in the history
* [SD-375] Added class to override tfa function to bypass tfa during pass reset.
---------

Co-authored-by: Md Nadim Hossain <[email protected]>

---------

Co-authored-by: Md Nadim Hossain <[email protected]>

* [SD-375] Added dependency injection for request stack.
  • Loading branch information
MdNadimHossain authored Nov 14, 2024
1 parent 6b67c71 commit 67eab3a
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 5 deletions.
143 changes: 143 additions & 0 deletions modules/tide_tfa/src/Controller/TideTfaUserController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

namespace Drupal\tide_tfa\Controller;

use Drupal\Component\Utility\Crypt;
use Drupal\prlp\Controller\PrlpController;
use Drupal\tfa\Controller\TfaUserControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

/**
* Custom controller to override the TfaUserControllerBase.
*/
class TideTfaUserController extends TfaUserControllerBase {

/**
* The request stack service.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
// Get the parent instance with inherited dependencies.
$instance = parent::create($container);
$instance->requestStack = $container->get('request_stack');

return $instance;
}

/**
* {@inheritdoc}
*/
public function doResetPassLogin($uid, $timestamp, $hash, $request = NULL) {
// Ensure a valid request object.
if (!$request) {
$request = $this->requestStack->getCurrentRequest();
}

// Check if the PRLP module is enabled.
if (!\Drupal::moduleHandler()->moduleExists('prlp')) {
// If PRLP is not enabled, call the parent method.
return parent::doResetPassLogin($uid, $timestamp, $hash, $request);
}

// Create an instance of PrlpController.
$prlp_controller = new PrlpController(
\Drupal::service('date.formatter'),
\Drupal::entityTypeManager()->getStorage('user'),
\Drupal::service('user.data'),
\Drupal::service('logger.factory')->get('prlp'),
\Drupal::service('flood'),
\Drupal::service('event_dispatcher')
);

/** @var \Drupal\user\UserInterface $user */
$user = $this->userStorage->load($uid);
$this->setUser($user);

// Let Drupal core deal with the one-time login,
// if TFA is not enabled or
// current user can skip TFA while resetting password.
if ($this->isTfaDisabled() || $this->canSkipPassReset()) {
// Use PRLP's resetPassLogin instead of the core function.
return $prlp_controller->prlpResetPassLogin($request, $uid, $timestamp, $hash);
}

// Whether the TFA Validation Plugin is set and ready for use.
$tfa_ready = $this->isReady();

// Check for authentication plugin.
if ($tfa_ready && $this->pluginAllowsLogin()) {
$this->messenger()->addStatus($this->t('You have logged in on a trusted browser.'));
return $prlp_controller->prlpResetPassLogin($request, $uid, $timestamp, $hash);
}

// Borrow the following codes from the core function:
$current = \Drupal::time()->getRequestTime();

// Verify that the user exists and is active.
if ($user === NULL || !$user->isActive()) {
throw new AccessDeniedHttpException();
}

// Time out, in seconds, until login URL expires.
$timeout = $this->config('user.settings')->get('password_reset_timeout');
if ($user->getLastLoginTime() && $current - $timestamp > $timeout) {
$this->messenger()->addError($this->t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'));
return $this->redirect('user.pass');
}
elseif ($user->isAuthenticated() && ($timestamp >= $user->getLastLoginTime()) && ($timestamp <= $current) && hash_equals($hash, user_pass_rehash($user, $timestamp))) {
if ($tfa_ready) {
$this->session->migrate();
$token = Crypt::randomBytesBase64(55);
$request->getSession()->set('pass_reset_' . $uid, $token);

$this->logger->notice('User %name used one-time login link at time %timestamp.', [
'%name' => $user->getDisplayName(),
'%timestamp' => $timestamp,
]);

$this->tempStoreUid($user->id());

return $this->redirect('tfa.entry', [
'uid' => $uid,
'hash' => $this->getLoginHash($user),
], [
'query' => ['pass-reset-token' => $token],
'absolute' => TRUE,
]);
}
else {
if ($this->canLoginWithoutTfa($this->getLogger('tfa'))) {
return $this->redirectToUserForm($user, $request, $timestamp);
}
else {
return $this->redirect('<front>');
}
}
}

// Use PRLP's resetPassLogin instead of the core function.
return $prlp_controller->prlpResetPassLogin($request, $uid, $timestamp, $hash);
}

/**
* Determines if the user can skip tfa on password reset.
*
* This function checks the TFA settings to see if the option to skip TFA
* during password reset is enabled. If enabled, users will not be required
* to complete two-factor authentication when resetting their password.
*
* @return bool
* TRUE if the user can skip TFA on password reset, FALSE otherwise.
*/
public function canSkipPassReset() {
return $this->tfaSettings->get('reset_pass_skip_enabled');
}

}
30 changes: 30 additions & 0 deletions modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Drupal\tide_tfa\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;

/**
* Listens to the dynamic route events.
*
* Class TideTfaRouteSubscriber.
*
* @package Drupal\tide_tfa\Routing
*/
class TideTfaRouteSubscriber extends RouteSubscriberBase {

/**
* Alters existing routes for TFA user password reset login.
*
* @param \Symfony\Component\Routing\RouteCollection $collection
* Route collection to be altered.
*/
protected function alterRoutes(RouteCollection $collection) {
// Override the user reset pass login route to use TideTfaUserController.
if ($route = $collection->get('user.reset.login')) {
$route->setDefault('_controller', '\Drupal\tide_tfa\Controller\TideTfaUserController::doResetPassLogin');
}
}

}
16 changes: 11 additions & 5 deletions modules/tide_tfa/src/TideTfaOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,17 @@ public static function setupTfaSettings() {
$roles = Role::loadMultiple();
// Initialize the $tfa_required_roles array.
$tfa_required_roles = [];
// Iterate through the roles and map the role IDs.
foreach ($roles as $role) {
if ($role->id() !== 'authenticated') {
// Map the role ID to itself.
$tfa_required_roles[$role->id()] = $role->id();
// Define the roles to exclude in a variable.
$excluded_roles = ['authenticated', 'previewer', 'secure_file_user'];

if (!empty($roles)) {
// Iterate through the roles and map the role IDs.
foreach ($roles as $role) {
// Check if the current role is not in the excluded roles.
if (!in_array($role->id(), $excluded_roles)) {
// Map the role ID to itself.
$tfa_required_roles[$role->id()] = $role->id();
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions modules/tide_tfa/tide_tfa.module
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ use Drupal\Core\Form\FormStateInterface;
* Implements hook_form_alter().
*/
function tide_tfa_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// [SD-375] Bypass tfa during reset pass for all users.
if ($form_id == 'tfa_settings_form') {
if (isset($form['reset_pass_skip_enabled'])) {
$form['reset_pass_skip_enabled']['#description'] = t('Allow TFA to be bypassed during password reset by the authenticated user.');
}
}
if ($form_id == 'tfa_entry_form') {
// [SD-294] Change the label of the 'Send' button.
if (isset($form['actions']['send'])) {
Expand Down
5 changes: 5 additions & 0 deletions modules/tide_tfa/tide_tfa.services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
tide_tfa.route_subscriber:
class: Drupal\tide_tfa\Routing\TideTfaRouteSubscriber
tags:
- { name: event_subscriber }

0 comments on commit 67eab3a

Please sign in to comment.