Problem/Motivation

We would like to have the option to forcefully redirect the user to the TFA setup page when this is required for him and he has no remaining validation skips

Proposed resolution

Add an option to redirect the user to the TFA overview page when he's in danger of getting his account blocked if he skips it.

Remaining tasks

Review.

CommentFileSizeAuthor
#45 89.patch14.59 KBjkdev
#38 interdiff_34_38.txt5.62 KBalexgreyhead
#38 DD-1756-force-tfa-user-setup-3223327-38.patch13.06 KBalexgreyhead
#37 DD-1756-force-tfa-user-setup-3223327-37.patch.txt13.06 KBalexgreyhead
#36 DD-1756-force-tfa-user-setup-3223327-36.patch.txt13.06 KBalexgreyhead
#35 force-tfa-user-setup-3223327-34.patch13.78 KBnarendragupta
#33 force-tfa-user-setup.patch13.09 KBneslee canil pinto
#30 3223327-implement-force_tfa_setup-30.patch14.5 KByovince
#28 3223327-implement-force_tfa_setup-28.patch13.6 KByeniatencio
#27 3223327-implement-force_tfa_setup-27.patch9.82 KByeniatencio
#26 3223327-implement-force_tfa_setup-26.patch13.6 KByeniatencio
#25 3223327-implement-force_tfa_setup-25.patch23.06 KByeniatencio
#24 3223327-implement-force_tfa_setup-24.patch13.63 KByeniatencio
#23 3223327-implement-force_tfa_setup-23.patch6.16 KByeniatencio
#21 force-tfa-unchecked.png35.77 KBmalks
#21 force-tfa-checked-no-skip-validation.png26.73 KBmalks
#19 tfa-patch.png417.4 KBMadhu Kumar M E
#17 Screenshot 2023-02-28 at 5.08.46 PM.png426.07 KBMadhu Kumar M E
#16 3223327-implement-force_tfa_setup-16.patch13.42 KBmaico de jong
#14 3223327-implement-force_tfa_setup-14.patch13.37 KBmaico de jong
#11 3223327-implement-force_tfa_setup-11.patch5.95 KBmaico de jong
#8 interdiff.txt694 bytesaskibinski
#8 3223327-implement_force_tfa_setup-8.patch10.77 KBaskibinski
#7 3223327-implement_force_tfa_setup-7.patch10.75 KBsimeonkesmev
#5 interdiff-4-5.txt573 bytessimeonkesmev
#5 3223327-implement_force_tfa_setup-5.patch10.5 KBsimeonkesmev
#4 3223327-implement_force_tfa_setup-4.patch10.38 KBsimeonkesmev
#2 implement_force_tfa_setup.patch2.7 KBsimeonkesmev
implement_force_tfa_setup.patch10.3 KBsimeonkesmev

Issue fork tfa-3223327

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

SimeonKesmev created an issue. See original summary.

simeonkesmev’s picture

StatusFileSize
new2.7 KB

Fix the order conditions, so the warning is presented when necessary.

Status: Needs review » Needs work

The last submitted patch, 2: implement_force_tfa_setup.patch, failed testing. View results

simeonkesmev’s picture

StatusFileSize
new10.38 KB

Here is a fixed patch.

simeonkesmev’s picture

StatusFileSize
new10.5 KB
new573 bytes

Avoid forcing for the user/logout.

jcnventura’s picture

Status: Needs work » Needs review
simeonkesmev’s picture

StatusFileSize
new10.75 KB

Added exclusion for password and user edit page, so that after one-time login the user is able to change the password even if all TFA skips are used.

askibinski’s picture

StatusFileSize
new10.77 KB
new694 bytes

Hopefully fixed the patch not applying + interdiff of Simeon's changes in #7.

askibinski’s picture

Status: Needs review » Needs work

On second thought, it doesn't make sense to bypass the one-time-login.
Related: #2930355: Password reset bypasses TFA

jcnventura’s picture

Version: 8.x-1.x-dev » 2.x-dev

Moving all open issues to the 2.x branch.

maico de jong’s picture

StatusFileSize
new5.95 KB

Rerolled the latest patch against 2.x-dev:

  • Removed support for 'remaining skips' when forced 2FA is selected for now, Makes it a lot easier to implement and communicate.
  • Rewritten the EventSubscriber and functional tests, reused the available TfaLoginContextTrait.
  • Excluded all 2FA and user login/logout/password paths from forced 2FA redirect.

Maybe it's a good idea to move extra functionalities like configurable whitelist or remaining skips integration (needs new messages) to new tickets and focus on the basics first.

maico de jong’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 11: 3223327-implement-force_tfa_setup-11.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

maico de jong’s picture

StatusFileSize
new13.37 KB

Fixed patch

maico de jong’s picture

Status: Needs work » Needs review
maico de jong’s picture

StatusFileSize
new13.42 KB

Excluded user edit routes for password reset support

Madhu Kumar M E’s picture

StatusFileSize
new426.07 KB

Trying to applying latest patch not applying. Added screenshot for reference.

maico de jong’s picture

@Madhu Kumar M E

Patch should work, tests also passed. Please make sure you are applying the patch correctly: through composer-patches or manually at the tfa module root using the 2.x-dev branch. Looking at your screenshot the patch was not applied to the tfa module directory.

Madhu Kumar M E’s picture

StatusFileSize
new417.4 KB

@Maico de Jong

I made a mistake, i didn't checked my directory properly.
Verified the patch #16 and tested it on Drupal version 10.1.x. The patch works fine also and I have added the screenshots for reference.

Thanks for reminding me.

Madhu Kumar M E’s picture

Status: Needs review » Reviewed & tested by the community
malks’s picture

StatusFileSize
new26.73 KB
new35.77 KB

I was just testing this and noticed that when "Force TFA setup" is selected the text box for "Skip Validation" is hidden. Is this as designed?

cmlara’s picture

Status: Reviewed & tested by the community » Needs work
Related issues: +#3108099: Redirect to validation setup after login without tfa
  1. +++ b/src/EventSubscriber/ForceTfaSetup.php
    @@ -0,0 +1,134 @@
    +    $user = $this->userStorage->load($this->currentUser->id());
    +    $this->setUser($user);
    +
    +    if ($this->isReady() || !$this->forceTFASetup()) {
    +      return;
    +    }
    +
    

    This can be VERY heavy latency. While the built in plugins can get this with just a few database queries, on a test(non performance tuend) laptop redirect() this ends up being ~10% of the request time (22ms) for the front page to load.

    With other authentication plugins isReady() could trigger a network query which will add even more latency and network dependency, see https://git.drupalcode.org/project/tfa_vault_totp/-/blob/0.x/src/Plugin/... for an example.

    I've been experimenting with using the request_session and parameter bags, perhaps similar may be a good fit here?.

  2. +++ b/src/EventSubscriber/ForceTfaSetup.php
    @@ -0,0 +1,134 @@
    +    $ignored_route_names = [
    

    I'm debating if we should we include system.403 and system.404 and system.4xx in here as well? I hit #3339277: Inform admin that role does not have permission to setup own tfa and ended up in an endless redirect loop. Ideally this shouldn't ever happen when redirecting to the TFA setup page, and fixing that issue would solve this concern here, however I can't help wondering if there might be other hidden problems and that exempting these paths might be wise.

#3108099: Redirect to validation setup after login without tfa would appear to be a related request.

yeniatencio’s picture

StatusFileSize
new6.16 KB

Rerolling #16 as patch is not applying clean anymore.

yeniatencio’s picture

StatusFileSize
new13.63 KB

Missed the EventSubscriber and functional test on #23.

Adding patch again.

yeniatencio’s picture

StatusFileSize
new23.06 KB

Added patch for version 8.x-1.x

yeniatencio’s picture

StatusFileSize
new13.6 KB

Re-rolling #25 as failing to applied on 8.x-1.2

yeniatencio’s picture

StatusFileSize
new9.82 KB

Re-rolling patch for version 8.x-1.x

yeniatencio’s picture

StatusFileSize
new13.6 KB

Missed the service and the unit test on last patch. Re-rolling patch for version 8.x-1.x

cmlara’s picture

Following up on my comments from #22

In #3339277: Inform admin that role does not have permission to setup own tfa we did not actually prevent an access denied error as the action to require TFA is different from being permitted to setup TFA, as such at the system.403 should be exempted (at least for the TFA setup page) to prevent a loop.

yovince’s picture

StatusFileSize
new14.5 KB

The patch prevents generating CSS and JS aggregation in Drupal 10.1.x. fixed by excluding the CSS and JS asset routes from the list.

itsbakiya’s picture

@yovince I used your patch its fine working Drupal 10.1.x

neslee canil pinto’s picture

StatusFileSize
new13.09 KB

Rerolled patch for latest 2.x verison

narendragupta’s picture

Patch is not getting applied for 8.x-1.5 tfa module version along with 10.2.2 drupal version.

narendragupta’s picture

StatusFileSize
new13.78 KB

Rerolled patch for latest 8.x-1.x verison, working fine with Drupal 10.2.2

alexgreyhead’s picture

Hello all,

Apologies Narendra :) I've spotted four issues with the patch in #35:

- Hunk in tfa.schema.yml doesn't apply because indentation of two subsequent lines is missing a space

- Indentation is 4 spaces instead of two

- Hiding the Allowed Validation plugins field when "force redirection" is enabled seems like a bad idea to me; when hidden, the administrator can't change which validation plugins are selected, and because this field is below the "force" checkbox, users working from top to bottom down the form might never see the allowed validation plugins field.

- A typo; "FTA"

Attaching a patch which:

- Fixes broken indentation - bad:

@@ -11,6 +11,9 @@ tfa.settings:
       sequence:
         type: string
         label: 'Role'
+    forced:
+      type: integer
+      label: 'Force required roles to setup when on last validation skip'
     send_plugins:
      type: sequence
      label: 'Enabled send plugins'

Good (i.e. the last two lines):

@@ -11,6 +11,9 @@ tfa.settings:
       sequence:
         type: string
         label: 'Role'
+    forced:
+      type: integer
+      label: 'Force required roles to setup when on last validation skip'
     send_plugins:
       type: sequence
       label: 'Enabled send plugins'

- Fixes indents - bad:

+    public function __construct(RouteMatchInterface $route_match, MessengerInterface $messenger, AccountProxyInterface $current_user, ConfigFactoryInterface $config_factory,
+                                EntityTypeManagerInterface $entity_type_manager, UserDataInterface $user_data, TfaValidationPluginManager $tfa_validation_manager, TfaLoginPluginManager $tfa_plugin_manager) {
+        $this->messenger = $messenger;
+        $this->routeMatch = $route_match;
+        $this->currentUser = $current_user;
+        $this->userStorage = $entity_type_manager->getStorage('user');
+        $this->tfaSettings = $config_factory->get('tfa.settings');
+        $this->userData = $user_data;
+        $this->tfaValidationManager = $tfa_validation_manager;
+        $this->tfaLoginManager = $tfa_plugin_manager;
+    }
+
+    /**
+     * Redirect users to TFA overview when no remaining skips.
+     *
+     * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
+     *   The request event.
+     */
+    public function redirect(RequestEvent $event): void {
+        /** @var \Drupal\user\UserInterface $user */
+        $user = $this->userStorage->load($this->currentUser->id());
+        $this->setUser($user);
+
+        if ($this->isReady() || !$this->forceTFASetup()) {
+            return;
+        }
+
+        $this->messenger->addWarning(t('You need to enable Two Factor Authentication.'));
+
+        // Don't redirect the user if on password/profile edit page,
+        // as it is possible the user used one-time login URL
+        // and need to change the password.
+        $ignored_route_names = [
+            'user.login',
+            'user.logout',
+            'user.pass',
+            'user.edit',
+            'entity.user.edit_form',
+            'user.reset.login',
+            'user.reset',
+            'user.reset.form',
+            'user.well-known.change_password',
+            'tfa.entry',
+            'tfa.login',
+            'tfa.overview',
+            'tfa.validation.setup',
+            'tfa.disable',
+            'tfa.plugin.reset',
+        ];
+        if (in_array($this->routeMatch->getRouteName(), $ignored_route_names)) {
+            return;
+        }
+
+        $tfa_overview_url = Url::fromRoute('tfa.overview', ['user' => $this->user->id()]);
+        $event->setResponse(new RedirectResponse($tfa_overview_url->toString()));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public static function getSubscribedEvents() {
+        $events[KernelEvents::REQUEST][] = ['redirect', 32];
+        return $events;
+    }
+
+}

Good:

+    public function __construct(RouteMatchInterface $route_match, MessengerInterface $messenger, AccountProxyInterface $current_user, ConfigFactoryInterface $config_factory,
+                                EntityTypeManagerInterface $entity_type_manager, UserDataInterface $user_data, TfaValidationPluginManager $tfa_validation_manager, TfaLoginPluginManager $tfa_plugin_manager) {
+      $this->messenger = $messenger;
+      $this->routeMatch = $route_match;
+      $this->currentUser = $current_user;
+      $this->userStorage = $entity_type_manager->getStorage('user');
+      $this->tfaSettings = $config_factory->get('tfa.settings');
+      $this->userData = $user_data;
+      $this->tfaValidationManager = $tfa_validation_manager;
+      $this->tfaLoginManager = $tfa_plugin_manager;
+    }
+
+    /**
+     * Redirect users to TFA overview when no remaining skips.
+     *
+     * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
+     *   The request event.
+     */
+    public function redirect(RequestEvent $event): void {
+      /** @var \Drupal\user\UserInterface $user */
+      $user = $this->userStorage->load($this->currentUser->id());
+      $this->setUser($user);
+
+      if ($this->isReady() || !$this->forceTFASetup()) {
+          return;
+      }
+
+      $this->messenger->addWarning(t('You need to enable Two Factor Authentication.'));
+
+      // Don't redirect the user if on password/profile edit page,
+      // as it is possible the user used one-time login URL
+      // and need to change the password.
+      $ignored_route_names = [
+        'user.login',
+        'user.logout',
+        'user.pass',
+        'user.edit',
+        'entity.user.edit_form',
+        'user.reset.login',
+        'user.reset',
+        'user.reset.form',
+        'user.well-known.change_password',
+        'tfa.entry',
+        'tfa.login',
+        'tfa.overview',
+        'tfa.validation.setup',
+        'tfa.disable',
+        'tfa.plugin.reset',
+      ];
+      if (in_array($this->routeMatch->getRouteName(), $ignored_route_names)) {
+          return;
+      }
+
+      $tfa_overview_url = Url::fromRoute('tfa.overview', ['user' => $this->user->id()]);
+      $event->setResponse(new RedirectResponse($tfa_overview_url->toString()));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public static function getSubscribedEvents() {
+      $events[KernelEvents::REQUEST][] = ['redirect', 32];
+      return $events;
+    }
+
+}

- Removes the behaviour to hide allowed validation plugins - i.e. this chunk:

@@ -186,7 +194,14 @@ class SettingsForm extends ConfigFormBase {
       '#default_value' => $config->get('allowed_validation_plugins') ?: ['tfa_totp'],
       '#description' => $this->t('Plugins that can be setup by users for various TFA processes.'),
       // Show only when TFA is enabled.
-      '#states' => $enabled_state,
+      '#states' => [
+        'visible' => [
+          [
+            ':input[name="tfa_enabled"]' => ['checked' => TRUE],
+            ':input[name="tfa_forced"]' => ['checked' => FALSE],
+          ],
+        ],
+      ],
       '#required' => TRUE,
     ];
     $form['tfa_validate'] = [

- Fixes the typo "FTA":

+      '#description' => $this->t('Force TFA setup on login, redirect user to FTA overview page.'),

... to:

+      '#description' => $this->t('Force TFA setup on login, redirect user to TFA overview page.'),
alexgreyhead’s picture

StatusFileSize
new13.06 KB

Found another incorrect indent - updated patch attached.

alexgreyhead’s picture

StatusFileSize
new13.06 KB
new5.62 KB

Playing whack-a-mole with indentation... [facepalm.gif]

cmlara’s picture

Generally development should occur on the latest branch first (2.x) and be backported to older branches once complete. This helps prevent a feature or bugfix being lost in newer versions. Addtionaly it reduces overall effort of maintaining multiple branches at once.

Leaving issue as needs-work per #22 and #29 comments.

Note: As part of the DrupalCi deprecation process the TFA project has already been migrated to only use GitLabCI for testing of the D8+ development branches. Due to this change we need contributors to utilize the MR workflow going forward as patches are no longer testable in the TFA project.

Contributors will likely encounter this with other projects as the patch workflow as DrupalCi has already been significantly limited as of Febuary 2024 and will be fully disabled for all D.O. hosted projects on July 1st. See https://www.drupal.org/about/core/blog/drupalci-and-all-patch-testing-wi... for more information.

alexgreyhead’s picture

Hi @cmlara, apologies - I provided the patch in #38 in good faith to fix the broken patch in #35. I'm not working with the 2.x branch due to SA-CONTRIB-2024-003, so wasn't able to provide a patch against that branch.

Hopefully the patch at #38 will help those still on 8.x-1.x ¯\_(ツ)_/¯

/A

marcellinostroosnijder’s picture

Hi all!

I tried to update the patch from #30 the the new rerolled patch added in #38. I noticed a major difference (2 lines hehe), and thats in the $ignored_routes array. As both css and JS are not ignored, your assest will not be loaded in. Basicly #30 comment got reversed. Please reroll the patch of #38, but with the systems routes added in :)

alexgreyhead’s picture

Hi MarcellinoStroosnijder,

> Please reroll the patch of #38, but with the systems routes added in :)

I'm away from that project now so won't be able to do this, but feel free to do the re-roll yourself :)

/Alex

jkdev made their first commit to this issue’s fork.

jkdev’s picture

StatusFileSize
new14.59 KB

Dear friends,

I have created a merge request 89 (MR) incorporating the various patches. You can review the MR and apply it as a patch. It is based on the 2.x branch.

This is the initial commit, so I would appreciate your feedback on whether it makes sense and if everything looks correct.

Please let us know if any parts are missing or if there are additional tasks that need to be addressed.

Thank you.

cmlara’s picture

Provided feedback in the MR. Leaving as NW for concerns.

jkdev’s picture

Can you please read through the logic, let me know if I missed something:

Adding event_subscriber which listens to KernelEvents::REQUEST, make some checks, and if needed - redirect the user to TFA overview page - preventing navigating to other pages in the site.

The checks are:

  • Is TFA is enabled?
  • Is the current route applicable for redirect? (we should allow logging-out, running-cron, css/js/images assets, and setting up the TFA)
  • Is user logged in? (anonymous users are irrelevant)
  • Is TFA setting force user to setup TFA?
  • Does TFA has at least one validation plugin enabled?

At this point we should set a warning in messenger: "You must setup TFA".

Further checks:

  • Is this route applicable for a Redirect?
  • If not, should we block the request altogether?
  • Are there situations that we would allow bypassing this behavior?

At this point we should Issue the redirect for TFA overview page.

The order of the checks are not final, and should sorted by their complexity and latency impact.

taran2l made their first commit to this issue’s fork.