From 80f3325c99e4aca14d49d335c6b06eddf4d1a04b Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Thu, 29 Jan 2026 15:34:38 +1100 Subject: [PATCH 01/15] [SD-1482] Added text changes to the tfa overview page. --- .../tide_tfa/src/Form/TideTfaOverviewForm.php | 132 ++++++++++++++++++ .../Plugin/TfaSetup/TideTfaEmailOtpSetup.php | 2 +- .../src/Routing/TideTfaRouteSubscriber.php | 5 + modules/tide_tfa/tide_tfa.module | 15 ++ 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 modules/tide_tfa/src/Form/TideTfaOverviewForm.php diff --git a/modules/tide_tfa/src/Form/TideTfaOverviewForm.php b/modules/tide_tfa/src/Form/TideTfaOverviewForm.php new file mode 100644 index 000000000..fd11fc8b5 --- /dev/null +++ b/modules/tide_tfa/src/Form/TideTfaOverviewForm.php @@ -0,0 +1,132 @@ + 'markup', + '#markup' => '

' . $this->t('Multi-factor authentication provides + additional security for your account. With multi-factor authentication enabled, + you log in to the CMS with a verification code in addition to your username and + password.') . '

', + ]; + // $form_state['storage']['account'] = $user;. + $config = $this->config('tfa.settings'); + $user_tfa = $this->tfaGetTfaData($user->id(), $this->userData); + $enabled = isset($user_tfa['status']) && $user_tfa['status']; + + if ($config->get('enabled')) { + $enabled = isset($user_tfa['status'], $user_tfa['data']) && !empty($user_tfa['data']['plugins']) && $user_tfa['status']; + $enabled_plugins = $user_tfa['data']['plugins'] ?? []; + + $validation_plugins = $this->tfaValidation->getDefinitions(); + if ($validation_plugins) { + $output['validation'] = [ + '#type' => 'details', + '#title' => $this->t('Validation plugins'), + '#open' => TRUE, + ]; + + foreach ($validation_plugins as $plugin_id => $plugin) { + if (!empty($config->get('allowed_validation_plugins')[$plugin_id])) { + $output['validation'][$plugin_id] = $this->tfaPluginSetupFormOverview($plugin, $user, !empty($enabled_plugins[$plugin_id])); + } + } + } + + if ($enabled) { + $login_plugins = $this->tfaLogin->getDefinitions(); + if ($login_plugins) { + $output['login'] = [ + '#type' => 'details', + '#title' => $this->t('Login plugins'), + '#open' => TRUE, + '#access' => FALSE, + ]; + + foreach ($login_plugins as $plugin_id => $plugin) { + if (!empty($config->get('login_plugins')[$plugin_id])) { + $output['login'][$plugin_id] = $this->tfaPluginSetupFormOverview($plugin, $user, TRUE); + $output['login']['#access'] = TRUE; + } + } + } + + $send_plugins = $this->tfaSend->getDefinitions(); + if ($send_plugins) { + $output['send'] = [ + '#type' => 'details', + '#title' => $this->t('Send plugins'), + '#open' => TRUE, + ]; + + foreach ($send_plugins as $plugin_id => $plugin) { + if (!empty($config->get('send_plugins')[$plugin_id])) { + $output['send'][$plugin_id] = $this->tfaPluginSetupFormOverview($plugin, $user, TRUE); + } + } + } + } + + if (!empty($user_tfa)) { + if ($enabled && !empty($user_tfa['data']['plugins'])) { + $disable_url = Url::fromRoute('tfa.disable', ['user' => $user->id()]); + if ($disable_url->access()) { + $status_text = $this->t('Status: TFA enabled, set + @time. Disable TFA', [ + '@time' => $this->dateFormatter->format($user_tfa['saved']), + ':url' => $disable_url->toString(), + ]); + } + else { + $status_text = $this->t('Status: Multi-factor authentication enabled'); + } + } + else { + $status_text = $this->t('Status: Multi-factor authentication disabled'); + } + $output['status'] = [ + '#type' => 'markup', + '#markup' => '

' . $status_text . '

', + ]; + } + + $output['validation_skip_status'] = [ + '#type' => 'markup', + '#markup' => '

' . $this->t('Authentication setup: @remaining logins remain before multi-factor authentication is required', [ + '@remaining' => $config->get('validation_skip') - $user_tfa['validation_skipped'], + ]) . '

', + ]; + } + else { + $output['disabled'] = [ + '#type' => 'markup', + '#markup' => 'Currently there are no enabled plugins.', + ]; + } + + if ($this->canPerformReset($user)) { + $output['actions'] = ['#type' => 'actions']; + $output['actions']['reset_skip_attempts'] = [ + '#type' => 'submit', + '#value' => $this->t('Reset skip validation attempts'), + '#submit' => ['::resetSkipValidationAttempts'], + ]; + $output['account'] = [ + '#type' => 'value', + '#value' => $user, + ]; + } + + return $output; + } +} \ No newline at end of file diff --git a/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php b/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php index 6b9a8f3be..e32372130 100644 --- a/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php +++ b/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php @@ -77,7 +77,7 @@ public function getOverview(array $params) { '#access' => !$params['enabled'], '#links' => [ 'admin' => [ - 'title' => $this->t('Enable two-factor authentication via email'), + 'title' => $this->t('Enable multi-factor authentication via email'), 'url' => Url::fromRoute('tfa.validation.setup', [ 'user' => $params['account']->id(), 'method' => $params['plugin_id'], diff --git a/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php index fbf1d9760..a4e4ec105 100644 --- a/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php +++ b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php @@ -25,6 +25,11 @@ protected function alterRoutes(RouteCollection $collection) { if ($route = $collection->get('user.reset.login')) { $route->setDefault('_controller', '\Drupal\tide_tfa\Controller\TideTfaUserController::doResetPassLogin'); } + // TFA overview page (User → Security → TFA). + if ($route = $collection->get('tfa.overview')) { + $route->setDefault('_title', 'Multi-factor authentication'); + $route->setDefault('_form', '\Drupal\tide_tfa\Form\TideTfaOverviewForm'); + } } } diff --git a/modules/tide_tfa/tide_tfa.module b/modules/tide_tfa/tide_tfa.module index f9771b950..74cd0f4b4 100644 --- a/modules/tide_tfa/tide_tfa.module +++ b/modules/tide_tfa/tide_tfa.module @@ -24,3 +24,18 @@ function tide_tfa_form_alter(&$form, FormStateInterface $form_state, $form_id) { } } } + +/** + * Implements hook_menu_local_tasks_alter(). + */ +function tide_tfa_menu_local_tasks_alter(&$data, $route_name, &$ref_root) { + // Check if the tfa.overview tab exists in the current render array + if (isset($data['tabs'][0]['tfa.overview'])) { + $data['tabs'][0]['tfa.overview']['#link']['title'] = t('Multi-factor authentication'); + } + + // Check if the tfa.settings tab exists + if (isset($data['tabs'][0]['tfa.settings'])) { + $data['tabs'][0]['tfa.settings']['#link']['title'] = t('Multi-factor authentication settings'); + } +} From bd28573d22c89de43a2c971851af205cf2358d1a Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Thu, 29 Jan 2026 15:40:50 +1100 Subject: [PATCH 02/15] [SD-1482] Updated tfa and tfa email otp module. --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 6f35dfada..43f2ade60 100644 --- a/composer.json +++ b/composer.json @@ -59,8 +59,8 @@ "drupal/smtp": "^1.2", "drupal/stage_file_proxy": "^2.0", "drupal/tablefield": "2.4", - "drupal/tfa": "1.9", - "drupal/tfa_email_otp": "^1.0@beta", + "drupal/tfa": "^1.12", + "drupal/tfa_email_otp": "^1.0", "drupal/token_conditions": "dev-compatible-with-d10", "drupal/token_filter": "^2.0", "drupal/twig_field_value": "^2.0", From ba29307226eb9235bea12daf0a56e429c61ae103 Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Sun, 1 Feb 2026 21:55:29 +1100 Subject: [PATCH 03/15] [SD-1482] Added changes for tfa setup form and enabaled view password module. --- composer.json | 3 ++- .../src/Routing/TideTfaRouteSubscriber.php | 4 ++++ modules/tide_tfa/src/TideTfaOperation.php | 21 +++++++++++++++++++ modules/tide_tfa/tide_tfa.install | 11 ++++++++++ modules/tide_tfa/tide_tfa.module | 14 +++++++++++++ 5 files changed, 52 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 43f2ade60..0c3830885 100644 --- a/composer.json +++ b/composer.json @@ -124,7 +124,8 @@ "drupal/shield": "^1.8", "drupal/media_alias_display": "^2.1", "drupal/search_api_exclude_entity": "^3.0", - "drupal/default_paragraphs": "^2.0" + "drupal/default_paragraphs": "^2.0", + "drupal/view_password": "^6.0" }, "repositories": { "drupal": { diff --git a/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php index a4e4ec105..28ffbe633 100644 --- a/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php +++ b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php @@ -30,6 +30,10 @@ protected function alterRoutes(RouteCollection $collection) { $route->setDefault('_title', 'Multi-factor authentication'); $route->setDefault('_form', '\Drupal\tide_tfa\Form\TideTfaOverviewForm'); } + // TFA setup page. + if ($route = $collection->get('tfa.validation.setup')) { + $route->setDefault('_title', 'Multi-factor authentication setup'); + } } } diff --git a/modules/tide_tfa/src/TideTfaOperation.php b/modules/tide_tfa/src/TideTfaOperation.php index d9b329b6a..0f85f5862 100644 --- a/modules/tide_tfa/src/TideTfaOperation.php +++ b/modules/tide_tfa/src/TideTfaOperation.php @@ -145,5 +145,26 @@ public static function setupTfaRolePermissions() { user_role_grant_permissions($rid, $permissions); } } + + /** + * Setup view password. + */ + public static function setupViewPassword() { + // Enable view_password module if not already enabled. + $module_installer = \Drupal::service('module_installer'); + if (!$module_installer->isInstalled('view_password')) { + $module_installer->install(['view_password']); + } + + // Set view_password configuration. + $config = \Drupal::configFactory()->getEditable('view_password.settings'); + + $form_ids = $config->get('form_ids') ?? []; + + if (!in_array('tfa_setup', $form_ids, TRUE)) { + $form_ids[] = 'tfa_setup'; + $config->set('form_ids', $form_ids)->save(); + } + } } diff --git a/modules/tide_tfa/tide_tfa.install b/modules/tide_tfa/tide_tfa.install index 982a8ace0..9f3411a84 100644 --- a/modules/tide_tfa/tide_tfa.install +++ b/modules/tide_tfa/tide_tfa.install @@ -21,4 +21,15 @@ function tide_tfa_install() { // Setup TFA role permissions. $tideTfaOperation->setupTfaRolePermissions(); + + // Setup view password. + $tideTfaOperation->setupViewPassword(); +} + +/** + * Setup view password. + */ +function tide_tfa_update_10000() { + $tideTfaOperation = new TideTfaOperation(); + $tideTfaOperation->setupViewPassword(); } diff --git a/modules/tide_tfa/tide_tfa.module b/modules/tide_tfa/tide_tfa.module index 74cd0f4b4..c517af7b4 100644 --- a/modules/tide_tfa/tide_tfa.module +++ b/modules/tide_tfa/tide_tfa.module @@ -11,6 +11,7 @@ use Drupal\Core\Form\FormStateInterface; * Implements hook_form_alter(). */ function tide_tfa_form_alter(&$form, FormStateInterface $form_state, $form_id) { + $current_user = \Drupal::currentUser(); // [SD-375] Bypass tfa during reset pass for all users. if ($form_id == 'tfa_settings_form') { if (isset($form['reset_pass_skip_enabled'])) { @@ -23,6 +24,19 @@ function tide_tfa_form_alter(&$form, FormStateInterface $form_state, $form_id) { $form['actions']['send']['#value'] = t('Email me a verification code'); } } + if ($form_id == 'tfa_setup') { + $route_user = \Drupal::routeMatch()->getParameter('user'); + // If this is NOT "admin altering another user's TFA", + // change only the default description. + if ( + $current_user->id() === $route_user->id() + && !$current_user->hasPermission('administer tfa for other users') + ) { + $form['current_pass']['#description'] = t( + 'The current password is mandatory.' + ); + } + } } /** From 8c5cfdcebe9cdc0a99c02b323ef327bc9c728832 Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Sun, 1 Feb 2026 22:19:38 +1100 Subject: [PATCH 04/15] [SD-1482] Added Email setup from changes. --- modules/tide_tfa/src/Form/TideTfaOverviewForm.php | 1 + .../tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/tide_tfa/src/Form/TideTfaOverviewForm.php b/modules/tide_tfa/src/Form/TideTfaOverviewForm.php index fd11fc8b5..bdb3a9b90 100644 --- a/modules/tide_tfa/src/Form/TideTfaOverviewForm.php +++ b/modules/tide_tfa/src/Form/TideTfaOverviewForm.php @@ -4,6 +4,7 @@ use Drupal\tfa\Form\TfaOverviewForm; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; use Drupal\user\UserInterface; class TideTfaOverviewForm extends TfaOverviewForm { diff --git a/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php b/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php index e32372130..e0fa9b1b0 100644 --- a/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php +++ b/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php @@ -28,10 +28,16 @@ public function getSetupForm(array $form, FormStateInterface $form_state) { $params = $form_state->getValues(); $userData = $this->userData->get('tfa', $params['account']->id(), 'tfa_email_otp'); + $form['email_otp_heading'] = [ + '#type' => 'html_tag', + '#tag' => 'h2', + '#value' => $this->t('Email authentication for login'), + ]; + // [SD-294] Changing the title and description. $form['enabled'] = [ '#type' => 'checkbox', - '#title' => $this->t('Yes, email me a verification code every time I log in'), + '#title' => $this->t('I agree to be sent a verification code via email each time I log in.'), '#description' => $this->t('Each single-use verification code expires after use, or after 10 minutes if not used.'), '#required' => TRUE, '#default_value' => $userData['enable'] ?? 0, From 4b02a1eb1189a9270bec6766490c4d367e5c7b23 Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Mon, 2 Feb 2026 16:30:33 +1100 Subject: [PATCH 05/15] [SD-1482] Added breadcrumb changes and some label, title and description updates. --- modules/tide_dashboard/tide_dashboard.module | 29 ++++++++++++++++++ .../tide_tfa/src/Form/TideTfaOverviewForm.php | 30 +++++++++++-------- .../Plugin/TfaSetup/TideTfaEmailOtpSetup.php | 2 +- modules/tide_tfa/tide_tfa.module | 6 ++++ tide_core.module | 10 +++++++ 5 files changed, 63 insertions(+), 14 deletions(-) diff --git a/modules/tide_dashboard/tide_dashboard.module b/modules/tide_dashboard/tide_dashboard.module index 56d5d3866..debe0a6dd 100644 --- a/modules/tide_dashboard/tide_dashboard.module +++ b/modules/tide_dashboard/tide_dashboard.module @@ -5,6 +5,11 @@ * Tide Dashboard. */ +use Drupal\Core\Link; +use Drupal\Core\Url; +use Drupal\Core\Breadcrumb\Breadcrumb; +use Drupal\Core\Routing\RouteMatchInterface; + /** * Implements hook_toolbar_alter(). * @@ -39,3 +44,27 @@ function tide_dashboard_user_login() { $request->query->set('destination', '/admin/workbench'); } } + +/** + * Implements hook_system_breadcrumb_alter(). + */ +function tide_dashboard_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) { + $links = $breadcrumb->getLinks(); + + if (!empty($links)) { + // Check if the first link is the Home link. + if ($links[0]->getUrl()->isRouted() && $links[0]->getUrl()->getRouteName() === '') { + + $new_url = Url::fromUserInput('/admin/workbench'); + $new_link = Link::fromTextAndUrl($links[0]->getText(), $new_url); + + // Swap the first link to workbench. + $links[0] = $new_link; + + $reflection = new \ReflectionClass($breadcrumb); + $property = $reflection->getProperty('links'); + $property->setAccessible(true); + $property->setValue($breadcrumb, $links); + } + } +} diff --git a/modules/tide_tfa/src/Form/TideTfaOverviewForm.php b/modules/tide_tfa/src/Form/TideTfaOverviewForm.php index bdb3a9b90..66fa04a10 100644 --- a/modules/tide_tfa/src/Form/TideTfaOverviewForm.php +++ b/modules/tide_tfa/src/Form/TideTfaOverviewForm.php @@ -16,7 +16,7 @@ public function buildForm(array $form, FormStateInterface $form_state, UserInter $output['info'] = [ '#type' => 'markup', '#markup' => '

' . $this->t('Multi-factor authentication provides - additional security for your account. With multi-factor authentication enabled, + additional security for your account.
With multi-factor authentication enabled, you log in to the CMS with a verification code in addition to your username and password.') . '

', ]; @@ -79,18 +79,16 @@ public function buildForm(array $form, FormStateInterface $form_state, UserInter } if (!empty($user_tfa)) { + $status_text = ''; if ($enabled && !empty($user_tfa['data']['plugins'])) { $disable_url = Url::fromRoute('tfa.disable', ['user' => $user->id()]); if ($disable_url->access()) { - $status_text = $this->t('Status: TFA enabled, set - @time. Disable TFA', [ + $status_text = $this->t('Status: Multi-factor authentication enabled, set + @time. Disable Multi-factor authentication', [ '@time' => $this->dateFormatter->format($user_tfa['saved']), ':url' => $disable_url->toString(), ]); } - else { - $status_text = $this->t('Status: Multi-factor authentication enabled'); - } } else { $status_text = $this->t('Status: Multi-factor authentication disabled'); @@ -100,13 +98,19 @@ public function buildForm(array $form, FormStateInterface $form_state, UserInter '#markup' => '

' . $status_text . '

', ]; } + else { + $validation_skipped = $user_tfa['validation_skipped'] ?? 0; - $output['validation_skip_status'] = [ - '#type' => 'markup', - '#markup' => '

' . $this->t('Authentication setup: @remaining logins remain before multi-factor authentication is required', [ - '@remaining' => $config->get('validation_skip') - $user_tfa['validation_skipped'], - ]) . '

', - ]; + $output['validation_skip_status'] = [ + '#type' => 'markup', + '#markup' => '

' . $this->t( + 'Authentication setup: @remaining logins remain before multi-factor authentication is required', + [ + '@remaining' => $config->get('validation_skip') - $validation_skipped, + ] + ) . '

', + ]; + } } else { $output['disabled'] = [ @@ -130,4 +134,4 @@ public function buildForm(array $form, FormStateInterface $form_state, UserInter return $output; } -} \ No newline at end of file +} diff --git a/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php b/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php index e0fa9b1b0..bb5c98720 100644 --- a/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php +++ b/modules/tide_tfa/src/Plugin/TfaSetup/TideTfaEmailOtpSetup.php @@ -64,7 +64,7 @@ public function getOverview(array $params) { // [SD-294] Modify the description. $description = ''; if ($params['enabled']) { - $description .= $this->t('

Enabled

'); + $description .= $this->t('

Multi-factor authentication enabled

'); } $output = [ 'heading' => [ diff --git a/modules/tide_tfa/tide_tfa.module b/modules/tide_tfa/tide_tfa.module index c517af7b4..682ee574f 100644 --- a/modules/tide_tfa/tide_tfa.module +++ b/modules/tide_tfa/tide_tfa.module @@ -37,6 +37,12 @@ function tide_tfa_form_alter(&$form, FormStateInterface $form_state, $form_id) { ); } } + if ($form_id == 'tfa_entry_form') { + if (isset($form['code'])) { + $form['code']['#title'] = t('Verification code'); + $form['code']['#description'] = t('The verification code field is mandatory.'); + } + } } /** diff --git a/tide_core.module b/tide_core.module index 5b3c92b6d..6405000ea 100644 --- a/tide_core.module +++ b/tide_core.module @@ -899,3 +899,13 @@ function tide_core_field_widget_complete_form_alter(&$field_widget_complete_form $field_widget_complete_form['widget'][0]['state']['#default_value'] = 'draft'; } } + +/** + * Implements hook_menu_local_tasks_alter(). + */ +function tide_core_menu_local_tasks_alter(&$data, $route_name, &$ref_root) { + // Update "View" to "View profile" + if (isset($data['tabs'][0]['entity.user.canonical'])) { + $data['tabs'][0]['entity.user.canonical']['#link']['title'] = t('View profile'); + } +} From fdda4c95f937a8b6d12fe96bbace454f2ea0c44e Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Tue, 3 Feb 2026 15:58:22 +1100 Subject: [PATCH 06/15] [SD-1482] Updated dashboard module to redirect to tfa setup form after user login and lint fix. --- modules/tide_dashboard/tide_dashboard.module | 49 ++++++++++++++----- .../tide_tfa/src/Form/TideTfaOverviewForm.php | 10 +++- modules/tide_tfa/src/TideTfaOperation.php | 9 ++-- modules/tide_tfa/tide_tfa.module | 4 +- tide_core.module | 2 +- 5 files changed, 55 insertions(+), 19 deletions(-) diff --git a/modules/tide_dashboard/tide_dashboard.module b/modules/tide_dashboard/tide_dashboard.module index debe0a6dd..b5ec69ea4 100644 --- a/modules/tide_dashboard/tide_dashboard.module +++ b/modules/tide_dashboard/tide_dashboard.module @@ -5,10 +5,11 @@ * Tide Dashboard. */ -use Drupal\Core\Link; -use Drupal\Core\Url; use Drupal\Core\Breadcrumb\Breadcrumb; +use Drupal\Core\Link; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Url; +use Drupal\user\UserInterface; /** * Implements hook_toolbar_alter(). @@ -35,12 +36,38 @@ function _tide_dashboard_workbench_content_title_callback() { return t('Dashboard'); } -/** - * Implements hook_user_login(). - */ -function tide_dashboard_user_login() { +function tide_dashboard_user_login(UserInterface $account) { $request = \Drupal::service('request_stack')->getCurrentRequest(); - if (($request->query->has('destination')) === FALSE) { + $uid = $account->id(); + + $module_handler = \Drupal::service('module_handler'); + if ($module_handler->moduleExists('tfa')) { + // Check which roles are forced to use tfa. + $tfa_config = \Drupal::config('tfa.settings'); + $required_roles = $tfa_config->get('required_roles') ?: []; + + // Check if the current user has any of the required roles. + $user_roles = $account->getRoles(); + $is_required = (bool) array_intersect($required_roles, $user_roles); + + // If they ARE required to have it, check if they've actually set it up. + if ($is_required) { + $user_data = \Drupal::service('user.data'); + $tfa_settings = $user_data->get('tfa', $uid, 'tfa_user_settings'); + // check 'status' AND ensure the 'plugins' array is not empty. + $has_active_plugins = !empty($tfa_settings['data']['plugins']); + $is_enabled = !empty($tfa_settings['status']) && $tfa_settings['status'] == 1; + + // If user don't have active plugins, they haven't finished setup. + if (!$is_enabled || !$has_active_plugins) { + $request->query->set('destination', "/user/$uid/security/tfa"); + return; + } + } + } + + // Fallback: If not required or already setup, go to workbench. + if (!$request->query->has('destination')) { $request->query->set('destination', '/admin/workbench'); } } @@ -54,16 +81,16 @@ function tide_dashboard_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMa if (!empty($links)) { // Check if the first link is the Home link. if ($links[0]->getUrl()->isRouted() && $links[0]->getUrl()->getRouteName() === '') { - + $new_url = Url::fromUserInput('/admin/workbench'); $new_link = Link::fromTextAndUrl($links[0]->getText(), $new_url); - + // Swap the first link to workbench. $links[0] = $new_link; - + $reflection = new \ReflectionClass($breadcrumb); $property = $reflection->getProperty('links'); - $property->setAccessible(true); + $property->setAccessible(TRUE); $property->setValue($breadcrumb, $links); } } diff --git a/modules/tide_tfa/src/Form/TideTfaOverviewForm.php b/modules/tide_tfa/src/Form/TideTfaOverviewForm.php index 66fa04a10..855e0792e 100644 --- a/modules/tide_tfa/src/Form/TideTfaOverviewForm.php +++ b/modules/tide_tfa/src/Form/TideTfaOverviewForm.php @@ -2,17 +2,24 @@ namespace Drupal\tide_tfa\Form; -use Drupal\tfa\Form\TfaOverviewForm; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; +use Drupal\tfa\Form\TfaOverviewForm; use Drupal\user\UserInterface; +/** + * Provides a customised TFA overview form for Tide. + * + * Extends the core TFA overview form to alter the presentation and behaviour + * of multi-factor authentication setup and status within the Tide CMS. + */ class TideTfaOverviewForm extends TfaOverviewForm { /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state, UserInterface $user = NULL) { + $output = []; $output['info'] = [ '#type' => 'markup', '#markup' => '

' . $this->t('Multi-factor authentication provides @@ -134,4 +141,5 @@ public function buildForm(array $form, FormStateInterface $form_state, UserInter return $output; } + } diff --git a/modules/tide_tfa/src/TideTfaOperation.php b/modules/tide_tfa/src/TideTfaOperation.php index 0f85f5862..7ba22b0b5 100644 --- a/modules/tide_tfa/src/TideTfaOperation.php +++ b/modules/tide_tfa/src/TideTfaOperation.php @@ -145,15 +145,16 @@ public static function setupTfaRolePermissions() { user_role_grant_permissions($rid, $permissions); } } - + /** * Setup view password. */ public static function setupViewPassword() { // Enable view_password module if not already enabled. - $module_installer = \Drupal::service('module_installer'); - if (!$module_installer->isInstalled('view_password')) { - $module_installer->install(['view_password']); + $moduleHandler = \Drupal::service('module_handler'); + $moduleInstaller = \Drupal::service('module_installer'); + if (!$moduleHandler->moduleExists('view_password')) { + $moduleInstaller->install(['view_password']); } // Set view_password configuration. diff --git a/modules/tide_tfa/tide_tfa.module b/modules/tide_tfa/tide_tfa.module index 682ee574f..4933f8c13 100644 --- a/modules/tide_tfa/tide_tfa.module +++ b/modules/tide_tfa/tide_tfa.module @@ -49,12 +49,12 @@ function tide_tfa_form_alter(&$form, FormStateInterface $form_state, $form_id) { * Implements hook_menu_local_tasks_alter(). */ function tide_tfa_menu_local_tasks_alter(&$data, $route_name, &$ref_root) { - // Check if the tfa.overview tab exists in the current render array + // Check if the tfa.overview tab exists. if (isset($data['tabs'][0]['tfa.overview'])) { $data['tabs'][0]['tfa.overview']['#link']['title'] = t('Multi-factor authentication'); } - // Check if the tfa.settings tab exists + // Check if the tfa.settings tab exists. if (isset($data['tabs'][0]['tfa.settings'])) { $data['tabs'][0]['tfa.settings']['#link']['title'] = t('Multi-factor authentication settings'); } diff --git a/tide_core.module b/tide_core.module index 6405000ea..834311f77 100644 --- a/tide_core.module +++ b/tide_core.module @@ -904,7 +904,7 @@ function tide_core_field_widget_complete_form_alter(&$field_widget_complete_form * Implements hook_menu_local_tasks_alter(). */ function tide_core_menu_local_tasks_alter(&$data, $route_name, &$ref_root) { - // Update "View" to "View profile" + // Update "View" to "View profile". if (isset($data['tabs'][0]['entity.user.canonical'])) { $data['tabs'][0]['entity.user.canonical']['#link']['title'] = t('View profile'); } From bcda8e00516eb44e16af7a37123b204fd117b1e5 Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Wed, 4 Feb 2026 12:22:40 +1100 Subject: [PATCH 07/15] [SD-1482] Fixed type miss match error during build and lint fix. --- modules/tide_dashboard/tide_dashboard.module | 7 +++++-- modules/tide_tfa/src/TideTfaOperation.php | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/tide_dashboard/tide_dashboard.module b/modules/tide_dashboard/tide_dashboard.module index b5ec69ea4..23e48b6de 100644 --- a/modules/tide_dashboard/tide_dashboard.module +++ b/modules/tide_dashboard/tide_dashboard.module @@ -36,6 +36,9 @@ function _tide_dashboard_workbench_content_title_callback() { return t('Dashboard'); } +/** + * Implements hook_user_login(). + */ function tide_dashboard_user_login(UserInterface $account) { $request = \Drupal::service('request_stack')->getCurrentRequest(); $uid = $account->id(); @@ -45,7 +48,7 @@ function tide_dashboard_user_login(UserInterface $account) { // Check which roles are forced to use tfa. $tfa_config = \Drupal::config('tfa.settings'); $required_roles = $tfa_config->get('required_roles') ?: []; - + // Check if the current user has any of the required roles. $user_roles = $account->getRoles(); $is_required = (bool) array_intersect($required_roles, $user_roles); @@ -54,7 +57,7 @@ function tide_dashboard_user_login(UserInterface $account) { if ($is_required) { $user_data = \Drupal::service('user.data'); $tfa_settings = $user_data->get('tfa', $uid, 'tfa_user_settings'); - // check 'status' AND ensure the 'plugins' array is not empty. + // Check 'status' AND ensure the 'plugins' array is not empty. $has_active_plugins = !empty($tfa_settings['data']['plugins']); $is_enabled = !empty($tfa_settings['status']) && $tfa_settings['status'] == 1; diff --git a/modules/tide_tfa/src/TideTfaOperation.php b/modules/tide_tfa/src/TideTfaOperation.php index 7ba22b0b5..56777f530 100644 --- a/modules/tide_tfa/src/TideTfaOperation.php +++ b/modules/tide_tfa/src/TideTfaOperation.php @@ -160,7 +160,10 @@ public static function setupViewPassword() { // Set view_password configuration. $config = \Drupal::configFactory()->getEditable('view_password.settings'); - $form_ids = $config->get('form_ids') ?? []; + $form_ids = $config->get('form_ids'); + $form_ids = is_array($form_ids) ? $form_ids : (array) $form_ids; + + $form_ids = array_filter($form_ids); if (!in_array('tfa_setup', $form_ids, TRUE)) { $form_ids[] = 'tfa_setup'; From 4eb2a59f2a8e141682c2b4c09a74909e1c8d2b9a Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Wed, 4 Feb 2026 12:46:50 +1100 Subject: [PATCH 08/15] [SD-1482] Fixed type miss match error during build for view password settings. --- modules/tide_tfa/src/TideTfaOperation.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/tide_tfa/src/TideTfaOperation.php b/modules/tide_tfa/src/TideTfaOperation.php index 56777f530..db380fd5e 100644 --- a/modules/tide_tfa/src/TideTfaOperation.php +++ b/modules/tide_tfa/src/TideTfaOperation.php @@ -159,15 +159,15 @@ public static function setupViewPassword() { // Set view_password configuration. $config = \Drupal::configFactory()->getEditable('view_password.settings'); - - $form_ids = $config->get('form_ids'); - $form_ids = is_array($form_ids) ? $form_ids : (array) $form_ids; - - $form_ids = array_filter($form_ids); + $form_ids_string = $config->get('form_ids') ?? ''; + $form_ids = array_filter(array_map('trim', explode(',', $form_ids_string))); if (!in_array('tfa_setup', $form_ids, TRUE)) { $form_ids[] = 'tfa_setup'; - $config->set('form_ids', $form_ids)->save(); + // Convert back to a comma-separated string. + // view_password only accept string for the schema. + $new_value = implode(',', $form_ids); + $config->set('form_ids', $new_value)->save(); } } From 03880285feb7eb51c6cebcffeb47d47ef6b0ca3f Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Thu, 5 Feb 2026 12:46:54 +1100 Subject: [PATCH 09/15] [SD-1482] Added custom messages and replaced tfa module messages. --- .../tide_tfa/src/Form/TideTfaOverviewForm.php | 8 +++- .../src/Routing/TideTfaRouteSubscriber.php | 15 ++++++ modules/tide_tfa/tide_tfa.module | 46 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/modules/tide_tfa/src/Form/TideTfaOverviewForm.php b/modules/tide_tfa/src/Form/TideTfaOverviewForm.php index 855e0792e..0a1cfe099 100644 --- a/modules/tide_tfa/src/Form/TideTfaOverviewForm.php +++ b/modules/tide_tfa/src/Form/TideTfaOverviewForm.php @@ -85,8 +85,8 @@ public function buildForm(array $form, FormStateInterface $form_state, UserInter } } + // Moved it inside to show the status if only TFA is enabled. if (!empty($user_tfa)) { - $status_text = ''; if ($enabled && !empty($user_tfa['data']['plugins'])) { $disable_url = Url::fromRoute('tfa.disable', ['user' => $user->id()]); if ($disable_url->access()) { @@ -96,6 +96,9 @@ public function buildForm(array $form, FormStateInterface $form_state, UserInter ':url' => $disable_url->toString(), ]); } + else { + $status_text = $this->t('Status: Multi-factor authentication enabled'); + } } else { $status_text = $this->t('Status: Multi-factor authentication disabled'); @@ -105,7 +108,8 @@ public function buildForm(array $form, FormStateInterface $form_state, UserInter '#markup' => '

' . $status_text . '

', ]; } - else { + + if (!$config->get('forced')) { $validation_skipped = $user_tfa['validation_skipped'] ?? 0; $output['validation_skip_status'] = [ diff --git a/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php index 28ffbe633..e6a1d14ed 100644 --- a/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php +++ b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php @@ -4,6 +4,7 @@ use Drupal\Core\Routing\RouteSubscriberBase; use Symfony\Component\Routing\RouteCollection; +use Drupal\Core\Routing\RoutingEvents; /** * Listens to the dynamic route events. @@ -34,6 +35,20 @@ protected function alterRoutes(RouteCollection $collection) { if ($route = $collection->get('tfa.validation.setup')) { $route->setDefault('_title', 'Multi-factor authentication setup'); } + // TFA disable page. + if ($route = $collection->get('tfa.disable')) { + $route->setDefault('_title', 'Disable multi-factor authentication'); + } + } + + /** + * Attempt to be the last subscriber to allow our routes to take priority. + */ + public static function getSubscribedEvents(): array { + $events = parent::getSubscribedEvents(); + // Use lower priority than tfa module. + $events[RoutingEvents::ALTER] = ['onAlterRoutes', (PHP_INT_MIN - 1)]; + return $events; } } diff --git a/modules/tide_tfa/tide_tfa.module b/modules/tide_tfa/tide_tfa.module index 4933f8c13..4c9bb0d12 100644 --- a/modules/tide_tfa/tide_tfa.module +++ b/modules/tide_tfa/tide_tfa.module @@ -59,3 +59,49 @@ function tide_tfa_menu_local_tasks_alter(&$data, $route_name, &$ref_root) { $data['tabs'][0]['tfa.settings']['#link']['title'] = t('Multi-factor authentication settings'); } } + +/** + * Implements hook_preprocess_HOOK(). + */ +function tide_tfa_preprocess_status_messages(&$variables) { + if (!$variables && !isset($variables['message_list'])) { + return; + } + + // Custom messages to replace TFA messages. + if (isset($variables['message_list']['error'])) { + foreach ($variables['message_list']['error'] as $key => $message) { + $message_string = (string) $message; + + // Convert the error message into warning message. + if (strpos($message_string, 'You are required to') !== false && + strpos($message_string, 'setup two-factor authentication') !== false && + strpos($message_string, 'unable to login') !== false) { + + if (!isset($variables['message_list']['warning'])) { + $variables['message_list']['warning'] = []; + } + + $variables['message_list']['warning'][] = t('You are required to set up multi-factor authentication. Select the link below to enable multi-factor authentication via email.'); + + // Remove the original message from the error list. + unset($variables['message_list']['error'][$key]); + } + } + + // Clean up: If the error list is now empty, remove the error key entirely. + if (empty($variables['message_list']['error'])) { + unset($variables['message_list']['error']); + } + } + if (isset($variables['message_list']['status'])) { + foreach ($variables['message_list']['status'] as $key => $message) { + if (strpos((string) $message, 'TFA setup complete.') !== FALSE) { + $variables['message_list']['status'][$key] = t("Multi-factor authentication setup is complete. Go to the dashboard."); + } + if (strpos((string) $message, 'TFA has been disabled.') !== FALSE) { + $variables['message_list']['status'][$key] = t("Multi-factor authentication has been disabled. Go to the dashboard."); + } + } + } +} From bc3087210e40c2101a1108c440746ae1bfb16e6b Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Mon, 9 Feb 2026 11:56:22 +1100 Subject: [PATCH 10/15] lint fix. --- modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php | 2 +- modules/tide_tfa/tide_tfa.module | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php index e6a1d14ed..3120027bb 100644 --- a/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php +++ b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php @@ -3,8 +3,8 @@ namespace Drupal\tide_tfa\Routing; use Drupal\Core\Routing\RouteSubscriberBase; -use Symfony\Component\Routing\RouteCollection; use Drupal\Core\Routing\RoutingEvents; +use Symfony\Component\Routing\RouteCollection; /** * Listens to the dynamic route events. diff --git a/modules/tide_tfa/tide_tfa.module b/modules/tide_tfa/tide_tfa.module index 4c9bb0d12..61c56ff29 100644 --- a/modules/tide_tfa/tide_tfa.module +++ b/modules/tide_tfa/tide_tfa.module @@ -74,10 +74,10 @@ function tide_tfa_preprocess_status_messages(&$variables) { $message_string = (string) $message; // Convert the error message into warning message. - if (strpos($message_string, 'You are required to') !== false && - strpos($message_string, 'setup two-factor authentication') !== false && - strpos($message_string, 'unable to login') !== false) { - + if (strpos($message_string, 'You are required to') !== FALSE && + strpos($message_string, 'setup two-factor authentication') !== FALSE && + strpos($message_string, 'unable to login') !== FALSE) { + if (!isset($variables['message_list']['warning'])) { $variables['message_list']['warning'] = []; } From 1831efc1e9908b1ae9351b53e1b2d12e258c3888 Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Tue, 10 Feb 2026 18:48:00 +1100 Subject: [PATCH 11/15] [SD-1483] Added Multifactor authentication verification enhancements. --- .../TideTfaEmailOtpValidation.php | 128 ++++++++++++++++-- .../src/Routing/TideTfaRouteSubscriber.php | 4 + modules/tide_tfa/tide_tfa.module | 28 ++-- 3 files changed, 136 insertions(+), 24 deletions(-) diff --git a/modules/tide_tfa/src/Plugin/TfaValidation/TideTfaEmailOtpValidation.php b/modules/tide_tfa/src/Plugin/TfaValidation/TideTfaEmailOtpValidation.php index 8d682c448..e27894ca3 100644 --- a/modules/tide_tfa/src/Plugin/TfaValidation/TideTfaEmailOtpValidation.php +++ b/modules/tide_tfa/src/Plugin/TfaValidation/TideTfaEmailOtpValidation.php @@ -2,6 +2,9 @@ namespace Drupal\tide_tfa\Plugin\TfaValidation; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\HtmlCommand; +use Drupal\Core\Ajax\InvokeCommand; use Drupal\Core\Form\FormStateInterface; use Drupal\tfa_email_otp\Plugin\TfaValidation\TfaEmailOtpValidation; @@ -17,6 +20,73 @@ */ class TideTfaEmailOtpValidation extends TfaEmailOtpValidation { + /** + * {@inheritdoc} + */ + public function getForm(array $form, FormStateInterface $form_state) { + $code_sent = $this->hasActiveOtp(); + + // Send automatically on first land. + if (!$code_sent && empty($userInput)) { + $this->send(); + } + + // Hide the heading if the resend link was clicked. + $form['email_otp_entry_heading'] = [ + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => $this->t('Enter the 8-digit verification code that was sent to your registered email.'), + '#prefix' => '
', + '#suffix' => '
', + ]; + + // Used in Ajax call back. + // To hide the emepty error box. + // This makes sure to not load the whole form. + $form['messages'] = [ + '#markup' => '', + '#prefix' => '
', + '#suffix' => '
', + ]; + + $form['code'] = [ + '#type' => 'textfield', + '#title' => $this->t('Verification code'), + '#required' => TRUE, + '#description' => $this->t('The verification code field is mandatory.'), + '#attributes' => ['autocomplete' => 'off'], + ]; + + $form['actions']['#type'] = 'actions'; + $form['actions']['#attributes']['style'] = 'display: block;'; + $form['actions']['login'] = [ + '#type' => 'submit', + '#button_type' => 'primary', + '#value' => $this->t('Verify'), + '#prefix' => '
', + '#suffix' => '
', + ]; + + // Convert the Resend button into a link-style. + $form['actions']['rsend_link'] = [ + '#type' => 'submit', + '#value' => $this->t('Send me a new verification code.'), + '#prefix' => '
' . $this->t('Didn’t receive an email or need a new code? '), + '#suffix' => '
', + '#attributes' => [ + 'style' => 'background:none; border:none; padding:0; color:#003CC5; text-decoration:underline; cursor:pointer; font-size: inherit; font-weight:400;', + ], + '#limit_validation_errors' => [['']], + '#ajax' => [ + 'callback' => [$this, 'updateButtonValue'], + 'event' => 'click', + 'wrapper' => 'send-button-wrapper', + ], + ]; + + return $form; + } + /** * {@inheritdoc} */ @@ -24,9 +94,17 @@ public function validateForm(array $form, FormStateInterface $form_state) { $values = $form_state->getValues(); // If user is asking for sending the code, // no need to validate the input. - // [SD-294]. - // Updating the op string to match with form alter change. - if (isset($values['op']) && $values['op']->getUntranslatedString() === 'Email me a verification code') { + if (isset($values['op']) && in_array($values['op']->getUntranslatedString(), ['Send me a new verification code.'], TRUE)) { + // Check flood control for email sending to prevent email bombing. + $flood_identifier = 'tfa_email_otp_send_' . $this->uid; + if (!$this->flood->isAllowed('tfa_email_otp.send', static::EMAIL_SEND_FLOOD_THRESHOLD, static::EMAIL_SEND_FLOOD_WINDOW, $flood_identifier)) { + $this->errorMessages['send'] = $this->t('Too many code requests. Please wait before requesting another code.'); + return FALSE; + } + + // Register the send attempt before sending. + $this->flood->register('tfa_email_otp.send', static::EMAIL_SEND_FLOOD_WINDOW, $flood_identifier); + // Send user the access code. $this->send(); @@ -40,19 +118,49 @@ public function validateForm(array $form, FormStateInterface $form_state) { if (!$this->validate($values['code'])) { if (!isset($this->errorMessages['code'])) { - $this->errorMessages['code'] = $this->t('Invalid authentication code. Please try again.'); - } - if ($this->alreadyAccepted) { - $form_state->clearErrors(); - $this->errorMessages['code'] = $this->t('Invalid code, it was recently used for a login. Please try a new code.'); + $this->errorMessages['code'] = $this->t('Enter a valid verification code.'); } return FALSE; } else { - // Store accepted code to prevent replay attacks. - $this->storeAcceptedCode($values['code']); return TRUE; } } + /** + * AJAX callback to update the form buttons and display status messages. + * + * This function is triggered when the "resend-link" submit is clicked. + * This updates the message area with any status messages. + * + * @param array &$form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * The AJAX response to send back to the browser, containing the updated + * form elements and any status messages. + */ + public function updateButtonValue(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + + // Clear all pending messages from the Drupal system. + // This ensures they don't pop up later if the page is refreshed. + \Drupal::messenger()->deleteAll(); + + // Wipe the message area in the browser. + // By sending an empty string to the #tfa-email-message-area, + // Remove any existing HTML (the green or red boxes). + $response->addCommand(new HtmlCommand('#tfa-email-message-area', '')); + + $response->addCommand(new InvokeCommand('#tfa-email-heading-wrapper', 'addClass', ['hidden'])); + + // Update the buttons as before. + $response->addCommand(new HtmlCommand('#tfa-email-verify-button', $form['actions']['login'])); + $response->addCommand(new HtmlCommand('#tfa-email-send-button', $form['actions']['rsend_link'])); + + return $response; + } + } diff --git a/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php index 3120027bb..2621da2f9 100644 --- a/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php +++ b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php @@ -35,6 +35,10 @@ protected function alterRoutes(RouteCollection $collection) { if ($route = $collection->get('tfa.validation.setup')) { $route->setDefault('_title', 'Multi-factor authentication setup'); } + // TFA Entry page. + if ($route = $collection->get('tfa.entry')) { + $route->setDefault('_title', 'Multi-factor authentication'); + } // TFA disable page. if ($route = $collection->get('tfa.disable')) { $route->setDefault('_title', 'Disable multi-factor authentication'); diff --git a/modules/tide_tfa/tide_tfa.module b/modules/tide_tfa/tide_tfa.module index 61c56ff29..cdc3b7af0 100644 --- a/modules/tide_tfa/tide_tfa.module +++ b/modules/tide_tfa/tide_tfa.module @@ -18,12 +18,6 @@ function tide_tfa_form_alter(&$form, FormStateInterface $form_state, $form_id) { $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'])) { - $form['actions']['send']['#value'] = t('Email me a verification code'); - } - } if ($form_id == 'tfa_setup') { $route_user = \Drupal::routeMatch()->getParameter('user'); // If this is NOT "admin altering another user's TFA", @@ -37,12 +31,6 @@ function tide_tfa_form_alter(&$form, FormStateInterface $form_state, $form_id) { ); } } - if ($form_id == 'tfa_entry_form') { - if (isset($form['code'])) { - $form['code']['#title'] = t('Verification code'); - $form['code']['#description'] = t('The verification code field is mandatory.'); - } - } } /** @@ -87,6 +75,10 @@ function tide_tfa_preprocess_status_messages(&$variables) { // Remove the original message from the error list. unset($variables['message_list']['error'][$key]); } + // When wrong code entered, this error shows up. + if (strpos($message_string, 'Verification code') !== FALSE) { + $variables['message_list']['error'][$key] = t('We couldn’t verify your code. Enter a valid verification code or request a new one below.'); + } } // Clean up: If the error list is now empty, remove the error key entirely. @@ -96,12 +88,20 @@ function tide_tfa_preprocess_status_messages(&$variables) { } if (isset($variables['message_list']['status'])) { foreach ($variables['message_list']['status'] as $key => $message) { - if (strpos((string) $message, 'TFA setup complete.') !== FALSE) { + $message_string = (string) $message; + if (strpos($message_string, 'The authentication code has been sent') !== FALSE) { + unset($variables['message_list']['status'][$key]); + continue; + } + if (strpos($message_string, 'TFA setup complete.') !== FALSE) { $variables['message_list']['status'][$key] = t("Multi-factor authentication setup is complete. Go to the dashboard."); } - if (strpos((string) $message, 'TFA has been disabled.') !== FALSE) { + if (strpos($message_string, 'TFA has been disabled.') !== FALSE) { $variables['message_list']['status'][$key] = t("Multi-factor authentication has been disabled. Go to the dashboard."); } } + if (empty($variables['message_list']['status'])) { + unset($variables['message_list']['status']); + } } } From f8ca4a4b2d9b49eac68db2586f1b771b8b94d046 Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Tue, 10 Feb 2026 19:00:37 +1100 Subject: [PATCH 12/15] [SD-1483] lint fix. --- .../TfaValidation/TideTfaEmailOtpValidation.php | 12 ++++++------ modules/tide_tfa/src/TideTfaOperation.php | 16 +++------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/modules/tide_tfa/src/Plugin/TfaValidation/TideTfaEmailOtpValidation.php b/modules/tide_tfa/src/Plugin/TfaValidation/TideTfaEmailOtpValidation.php index e27894ca3..2a7016680 100644 --- a/modules/tide_tfa/src/Plugin/TfaValidation/TideTfaEmailOtpValidation.php +++ b/modules/tide_tfa/src/Plugin/TfaValidation/TideTfaEmailOtpValidation.php @@ -71,16 +71,16 @@ public function getForm(array $form, FormStateInterface $form_state) { $form['actions']['rsend_link'] = [ '#type' => 'submit', '#value' => $this->t('Send me a new verification code.'), - '#prefix' => '
' . $this->t('Didn’t receive an email or need a new code? '), + '#prefix' => '
' . $this->t('Didn’t receive an email or need a new code?'), '#suffix' => '
', '#attributes' => [ 'style' => 'background:none; border:none; padding:0; color:#003CC5; text-decoration:underline; cursor:pointer; font-size: inherit; font-weight:400;', ], '#limit_validation_errors' => [['']], '#ajax' => [ - 'callback' => [$this, 'updateButtonValue'], - 'event' => 'click', - 'wrapper' => 'send-button-wrapper', + 'callback' => [$this, 'updateButtonValue'], + 'event' => 'click', + 'wrapper' => 'send-button-wrapper', ], ]; @@ -139,7 +139,7 @@ public function validateForm(array $form, FormStateInterface $form_state) { * The current state of the form. * * @return \Drupal\Core\Ajax\AjaxResponse - * The AJAX response to send back to the browser, containing the updated + * The AJAX response to send back to the browser, containing the updated * form elements and any status messages. */ public function updateButtonValue(array &$form, FormStateInterface $form_state) { @@ -150,7 +150,7 @@ public function updateButtonValue(array &$form, FormStateInterface $form_state) \Drupal::messenger()->deleteAll(); // Wipe the message area in the browser. - // By sending an empty string to the #tfa-email-message-area, + // By sending an empty string to the #tfa-email-message-area, // Remove any existing HTML (the green or red boxes). $response->addCommand(new HtmlCommand('#tfa-email-message-area', '')); diff --git a/modules/tide_tfa/src/TideTfaOperation.php b/modules/tide_tfa/src/TideTfaOperation.php index db380fd5e..e3e506e47 100644 --- a/modules/tide_tfa/src/TideTfaOperation.php +++ b/modules/tide_tfa/src/TideTfaOperation.php @@ -150,25 +150,15 @@ public static function setupTfaRolePermissions() { * Setup view password. */ public static function setupViewPassword() { - // Enable view_password module if not already enabled. $moduleHandler = \Drupal::service('module_handler'); $moduleInstaller = \Drupal::service('module_installer'); + if (!$moduleHandler->moduleExists('view_password')) { - $moduleInstaller->install(['view_password']); + $moduleInstaller->install(['view_password']); } - // Set view_password configuration. $config = \Drupal::configFactory()->getEditable('view_password.settings'); - $form_ids_string = $config->get('form_ids') ?? ''; - $form_ids = array_filter(array_map('trim', explode(',', $form_ids_string))); - - if (!in_array('tfa_setup', $form_ids, TRUE)) { - $form_ids[] = 'tfa_setup'; - // Convert back to a comma-separated string. - // view_password only accept string for the schema. - $new_value = implode(',', $form_ids); - $config->set('form_ids', $new_value)->save(); - } + $config->set('form_ids', (string) 'tfa_setup')->save(); } } From dd662275a759a54f2f6bddc80e0542b7f9490211 Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Wed, 11 Feb 2026 12:41:42 +1100 Subject: [PATCH 13/15] Added hook to update email templates for TFA and TFA email otp. --- modules/tide_dashboard/tide_dashboard.module | 33 ++------------------ modules/tide_tfa/src/TideTfaOperation.php | 14 ++++----- modules/tide_tfa/tide_tfa.install | 31 ++++++++++++++++++ modules/tide_tfa/tide_tfa.module | 32 +++++++++++++++++++ 4 files changed, 72 insertions(+), 38 deletions(-) diff --git a/modules/tide_dashboard/tide_dashboard.module b/modules/tide_dashboard/tide_dashboard.module index 23e48b6de..dc47f8bfe 100644 --- a/modules/tide_dashboard/tide_dashboard.module +++ b/modules/tide_dashboard/tide_dashboard.module @@ -39,38 +39,9 @@ function _tide_dashboard_workbench_content_title_callback() { /** * Implements hook_user_login(). */ -function tide_dashboard_user_login(UserInterface $account) { +function tide_dashboard_user_login() { $request = \Drupal::service('request_stack')->getCurrentRequest(); - $uid = $account->id(); - - $module_handler = \Drupal::service('module_handler'); - if ($module_handler->moduleExists('tfa')) { - // Check which roles are forced to use tfa. - $tfa_config = \Drupal::config('tfa.settings'); - $required_roles = $tfa_config->get('required_roles') ?: []; - - // Check if the current user has any of the required roles. - $user_roles = $account->getRoles(); - $is_required = (bool) array_intersect($required_roles, $user_roles); - - // If they ARE required to have it, check if they've actually set it up. - if ($is_required) { - $user_data = \Drupal::service('user.data'); - $tfa_settings = $user_data->get('tfa', $uid, 'tfa_user_settings'); - // Check 'status' AND ensure the 'plugins' array is not empty. - $has_active_plugins = !empty($tfa_settings['data']['plugins']); - $is_enabled = !empty($tfa_settings['status']) && $tfa_settings['status'] == 1; - - // If user don't have active plugins, they haven't finished setup. - if (!$is_enabled || !$has_active_plugins) { - $request->query->set('destination', "/user/$uid/security/tfa"); - return; - } - } - } - - // Fallback: If not required or already setup, go to workbench. - if (!$request->query->has('destination')) { + if (($request->query->has('destination')) === FALSE) { $request->query->set('destination', '/admin/workbench'); } } diff --git a/modules/tide_tfa/src/TideTfaOperation.php b/modules/tide_tfa/src/TideTfaOperation.php index e3e506e47..aeade38f2 100644 --- a/modules/tide_tfa/src/TideTfaOperation.php +++ b/modules/tide_tfa/src/TideTfaOperation.php @@ -102,19 +102,19 @@ public static function setupTfaSettings() { 'tfa_email_otp' => [ 'code_validity_period' => '600', 'email_setting' => [ - 'subject' => 'Single Digtial Presence CMS two-factor authentication code', - 'body' => 'Hi [user:display-name],\r\n\r\nYour two-factor authentication code is: [code]\r\n\r\nThis code is valid for [length] minutes. \r\n\r\nThis code will expire when you have logged in.\r\n\r\nFrom the SDP team\r\n\r\nRead more about 2FA: https://digital-vic.atlassian.net/servicedesk/customer/article/2439479507', + 'subject' => 'Single Digital Presence CMS multi-factor authentication code', + 'body' => "Hi [user:display-name],\r\n\r\nYour verification code is\r\n\r\n[code]\r\n\r\nThis code will expire in [length] minutes, and can't be reused after you have logged in.\r\n\r\nIf you did not initiate this request, please contact the Single Digital Presence support team immediately.\r\n\r\nKind regards,\r\nSingle Digital Presence team", ], ], ]; $mail_settings = [ 'tfa_enabled_configuration' => [ - 'subject' => 'Your Single Digtial Presence CMS account now has two-factor authentication', - 'body' => "[user:display-name],\r\n\r\nThanks for configuring two-factor authentication on your Single Digital Presence account!\r\n\r\nThis additional level of security will help to ensure that only you are able to log in to your account.\r\n\r\nIf you ever lose the device you configured, you should act quickly to delete its association with this account.\r\n\r\nFrom the SDP team\r\n\r\nRead more about 2FA: https://digital-vic.atlassian.net/servicedesk/customer/article/2439479507", + 'subject' => 'Your Single Digtial Presence CMS account now has multi-factor authentication', + 'body' => "Hi [user:display-name],\r\n\r\nThanks for enabling multi-factor authentication on your Single Digital Presence CMS account.\r\n\r\nThis additional level of security will help to ensure that only you are able to log in to your account.\r\n\r\nRead more about multi-factor authentication https://digital-vic.atlassian.net/servicedesk/customer/article/2439479507\r\n\r\nKind regards,\r\nSingle Digital Presence team" ], 'tfa_disabled_configuration' => [ - 'subject' => 'Your Single Digtial Presence CMS account now has two-factor authentication', - 'body' => "[user:display-name],\r\n\r\nThanks for configuring two-factor authentication on your Single Digital Presence account!\r\n\r\nThis additional level of security will help to ensure that only you are able to log in to your account.\r\n\r\nIf you ever lose the device you configured, you should act quickly to delete its association with this account.\r\n\r\nFrom the SDP team\r\n\r\nRead more about 2FA: https://digital-vic.atlassian.net/servicedesk/customer/article/2439479507", + 'subject' => 'Your Single Digital Presence CMS account no longer has multi-factor authentication', + 'body' => "Hi [user:display-name],\r\n\r\nMulti-factor authentication has been disabled on your Single Digital Presence CMS user account.\r\n\r\nIf you did not take this action, please contact your site administrator immediately.\r\n\r\nRead more about multi-factor authentication https://digital-vic.atlassian.net/servicedesk/customer/article/2439479507\r\n\r\nKind regards,\r\nSingle Digital Presence team", ], ]; @@ -154,7 +154,7 @@ public static function setupViewPassword() { $moduleInstaller = \Drupal::service('module_installer'); if (!$moduleHandler->moduleExists('view_password')) { - $moduleInstaller->install(['view_password']); + $moduleInstaller->install(['view_password']); } $config = \Drupal::configFactory()->getEditable('view_password.settings'); diff --git a/modules/tide_tfa/tide_tfa.install b/modules/tide_tfa/tide_tfa.install index 9f3411a84..8d6c9a991 100644 --- a/modules/tide_tfa/tide_tfa.install +++ b/modules/tide_tfa/tide_tfa.install @@ -33,3 +33,34 @@ function tide_tfa_update_10000() { $tideTfaOperation = new TideTfaOperation(); $tideTfaOperation->setupViewPassword(); } + +/** + * Update TFA email settings for SDP. + */ +function tide_tfa_update_10001() { + $config = \Drupal::configFactory()->getEditable('tfa.settings'); + + $validation_settings = $config->get('validation_plugin_settings'); + + if (isset($validation_settings['tfa_email_otp'])) { + $validation_settings['tfa_email_otp']['email_setting'] = [ + 'subject' => 'Single Digital Presence CMS multi-factor authentication code', + 'body' => "Hi [user:display-name],\r\n\r\nYour verification code is\r\n\r\n[code]\r\n\r\nThis code will expire in [length] minutes, and can't be reused after you have logged in.\r\n\r\nIf you did not initiate this request, please contact the Single Digital Presence support team immediately.\r\n\r\nKind regards,\r\nSingle Digital Presence team", + ]; + $config->set('validation_plugin_settings', $validation_settings); + } + + $mail_settings = [ + 'tfa_enabled_configuration' => [ + 'subject' => 'Your Single Digtial Presence CMS account now has multi-factor authentication', + 'body' => "Hi [user:display-name],\r\n\r\nThanks for enabling multi-factor authentication on your Single Digital Presence CMS account.\r\n\r\nThis additional level of security will help to ensure that only you are able to log in to your account.\r\n\r\nRead more about multi-factor authentication https://digital-vic.atlassian.net/servicedesk/customer/article/2439479507\r\n\r\nKind regards,\r\nSingle Digital Presence team", + ], + 'tfa_disabled_configuration' => [ + 'subject' => 'Your Single Digital Presence CMS account no longer has multi-factor authentication', + 'body' => "Hi [user:display-name],\r\n\r\nMulti-factor authentication has been disabled on your Single Digital Presence CMS user account.\r\n\r\nIf you did not take this action, please contact your site administrator immediately.\r\n\r\nRead more about multi-factor authentication https://digital-vic.atlassian.net/servicedesk/customer/article/2439479507\r\n\r\nKind regards,\r\nSingle Digital Presence team", + ], + ]; + + $config->set('mail', $mail_settings); + $config->save(); +} diff --git a/modules/tide_tfa/tide_tfa.module b/modules/tide_tfa/tide_tfa.module index cdc3b7af0..e1ef1551c 100644 --- a/modules/tide_tfa/tide_tfa.module +++ b/modules/tide_tfa/tide_tfa.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Form\FormStateInterface; +use Drupal\user\UserInterface; /** * Implements hook_form_alter(). @@ -105,3 +106,34 @@ function tide_tfa_preprocess_status_messages(&$variables) { } } } + +/** + * Implements hook_user_login(). + */ +function tide_tfa_user_login(UserInterface $account) { + $request = \Drupal::service('request_stack')->getCurrentRequest(); + $uid = $account->id(); + + // Check which roles are forced to use tfa. + $tfa_config = \Drupal::config('tfa.settings'); + $required_roles = $tfa_config->get('required_roles') ?: []; + + // Check if the current user has any of the required roles. + $user_roles = $account->getRoles(); + $is_required = (bool) array_intersect($required_roles, $user_roles); + + // If they ARE required to have it, check if they've actually set it up. + if ($tfa_config->get('enabled') && $is_required) { + $user_data = \Drupal::service('user.data'); + $tfa_settings = $user_data->get('tfa', $uid, 'tfa_user_settings'); + // Check 'status' AND ensure the 'plugins' array is not empty. + $has_active_plugins = !empty($tfa_settings['data']['plugins']); + $is_enabled = !empty($tfa_settings['status']) && $tfa_settings['status'] == 1; + + // If user don't have active plugins, they haven't finished setup. + if (!$is_enabled || !$has_active_plugins) { + $request->query->set('destination', "/user/$uid/security/tfa"); + return; + } + } +} From 4598c7aa2c0dddbdd7941c9ae63387df8ce85dff Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Wed, 11 Feb 2026 12:52:05 +1100 Subject: [PATCH 14/15] lint fix. --- modules/tide_dashboard/tide_dashboard.module | 1 - modules/tide_tfa/src/TideTfaOperation.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/tide_dashboard/tide_dashboard.module b/modules/tide_dashboard/tide_dashboard.module index dc47f8bfe..fce8255a2 100644 --- a/modules/tide_dashboard/tide_dashboard.module +++ b/modules/tide_dashboard/tide_dashboard.module @@ -9,7 +9,6 @@ use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Link; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; -use Drupal\user\UserInterface; /** * Implements hook_toolbar_alter(). diff --git a/modules/tide_tfa/src/TideTfaOperation.php b/modules/tide_tfa/src/TideTfaOperation.php index aeade38f2..58c8e68c6 100644 --- a/modules/tide_tfa/src/TideTfaOperation.php +++ b/modules/tide_tfa/src/TideTfaOperation.php @@ -110,7 +110,7 @@ public static function setupTfaSettings() { $mail_settings = [ 'tfa_enabled_configuration' => [ 'subject' => 'Your Single Digtial Presence CMS account now has multi-factor authentication', - 'body' => "Hi [user:display-name],\r\n\r\nThanks for enabling multi-factor authentication on your Single Digital Presence CMS account.\r\n\r\nThis additional level of security will help to ensure that only you are able to log in to your account.\r\n\r\nRead more about multi-factor authentication https://digital-vic.atlassian.net/servicedesk/customer/article/2439479507\r\n\r\nKind regards,\r\nSingle Digital Presence team" + 'body' => "Hi [user:display-name],\r\n\r\nThanks for enabling multi-factor authentication on your Single Digital Presence CMS account.\r\n\r\nThis additional level of security will help to ensure that only you are able to log in to your account.\r\n\r\nRead more about multi-factor authentication https://digital-vic.atlassian.net/servicedesk/customer/article/2439479507\r\n\r\nKind regards,\r\nSingle Digital Presence team", ], 'tfa_disabled_configuration' => [ 'subject' => 'Your Single Digital Presence CMS account no longer has multi-factor authentication', From da7e68c581839ac3af09798f48c0631f8a5b20a1 Mon Sep 17 00:00:00 2001 From: Md Nadim Hossain Date: Wed, 11 Feb 2026 15:26:11 +1100 Subject: [PATCH 15/15] Added bheat test for the multi-faction authentication enhancements. --- behat.yml | 1 + .../tests/behat/features/tide_2fa.feature | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/behat.yml b/behat.yml index cebeb9f81..523d379ef 100644 --- a/behat.yml +++ b/behat.yml @@ -20,6 +20,7 @@ default: - '%paths.base%/modules/tide_webform/tests/behat/features' - '%paths.base%/modules/tide_publication/tests/behat/features' - '%paths.base%/modules/tide_alert/tests/behat/features' + - '%paths.base%/modules/tide_tfa/tests/behat/features' contexts: - Tide\Tests\Context\FeatureContext - Drupal\DrupalExtension\Context\MinkContext diff --git a/modules/tide_tfa/tests/behat/features/tide_2fa.feature b/modules/tide_tfa/tests/behat/features/tide_2fa.feature index e6c59a924..6ae51e16c 100644 --- a/modules/tide_tfa/tests/behat/features/tide_2fa.feature +++ b/modules/tide_tfa/tests/behat/features/tide_2fa.feature @@ -1,4 +1,4 @@ -@tide +@tide @wip Feature: Force 2FA setup Ensure that all users setup 2FA. @@ -30,3 +30,39 @@ Feature: Force 2FA setup | role | | administrator | | site_admin | + + @api @javascript + Scenario Outline: Enable multi-factor authentication + Given I am logged in as an administrator + Then I go to "/admin/config/people/tfa" + And I click on the element "#edit-tfa-enabled" + Then I save screenshot + And I should see the text "Roles required to set up TFA" + And I should see the text "Allowed Validation plugins" + And I should see the text "TFA Email one-time password (EOTP)" + And the "edit-tfa-allowed-validation-plugins-tfa-email-otp" checkbox should be checked + And I should see the text "Validation Settings" + And I should see the text "TFA Email one-time password (EOTP)" + And the "#edit-validation-plugin-settings-tfa-email-otp-code-validity-period option[selected='selected']" element should contain "10" + Then the "validation_plugin_settings[tfa_email_otp][email_setting][subject]" field should contain "Single Digital Presence CMS multi-factor authentication code" + Then I press the "Save configuration" button + + @api @javascript + Scenario Outline: Non admin user multi-factor authentication flow + Given I am logged in as an administrator + When I go to "/" + And I click "Multi-factor authentication" + Then I should see the heading "Multi-factor authentication" in the "header" region + And I should see the text "Email verification code" + And I click "Enable multi-factor authentication via email" + Then I should see the heading "Multi-factor authentication setup" in the "header" region + Then I save screenshot + + @api @javascript + Scenario Outline: Disable multi-factor authentication + Given I am logged in as an administrator + Then I go to "/admin/config/people/tfa" + And I click on the element "#edit-tfa-enabled" + And I should not see the text "Roles required to set up TFA" + Then I save screenshot + Then I press the "Save configuration" button \ No newline at end of file