From 218a9e70c5b0a734ed2edf87f393e3b096ceab53 Mon Sep 17 00:00:00 2001 From: Fredys Fonseca Date: Thu, 17 Jul 2025 05:50:45 +0000 Subject: [PATCH 01/10] Add admin MFA enforcement feature --- app/controllers/concerns/mfa_force_concern.rb | 39 +++++ docs/MFA_FORCE.md | 139 ++++++++++++++++++ .../concerns/mfa_force_concern_spec.rb | 97 ++++++++++++ 3 files changed, 275 insertions(+) create mode 100644 app/controllers/concerns/mfa_force_concern.rb create mode 100644 docs/MFA_FORCE.md create mode 100644 spec/controllers/concerns/mfa_force_concern_spec.rb diff --git a/app/controllers/concerns/mfa_force_concern.rb b/app/controllers/concerns/mfa_force_concern.rb new file mode 100644 index 0000000000..c2dc4f8a98 --- /dev/null +++ b/app/controllers/concerns/mfa_force_concern.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module MfaForceConcern + extend ActiveSupport::Concern + + included do + before_action :check_mfa_requirement, if: :user_signed_in? + end + + private + + def check_mfa_requirement + return unless mfa_force_enabled? + return if current_user.otp_enabled? + return if mfa_setup_allowed_paths? + + flash[:warning] = I18n.t('mfa_force.required_message') + redirect_to settings_otp_authentication_path + end + + def mfa_force_enabled? + ENV['MFA_FORCE'] == 'true' + end + + def mfa_setup_allowed_paths? + allowed_paths = [ + settings_otp_authentication_path, + new_settings_two_factor_authentication_confirmation_path, + settings_two_factor_authentication_confirmation_path, + settings_two_factor_authentication_methods_path, + settings_two_factor_authentication_recovery_codes_path, + destroy_user_session_path, + auth_setup_path, + edit_user_registration_path, + ] + + allowed_paths.any? { |path| request.path.start_with?(path) } + end +end diff --git a/docs/MFA_FORCE.md b/docs/MFA_FORCE.md new file mode 100644 index 0000000000..2a51496774 --- /dev/null +++ b/docs/MFA_FORCE.md @@ -0,0 +1,139 @@ +# MFA Force Feature + +## Overview + +The MFA Force feature allows administrators to require all users to enable two-factor authentication (2FA) before they can access the platform. This is useful for organizations with strict security policies. + +## Configuration + +### Environment Variable + +To enable MFA forcing, set the following environment variable: + +```bash +MFA_FORCE=true +``` + +### Docker Compose + +Add the environment variable to your `.env.production` file: + +```env +MFA_FORCE=true +``` + +Or add it directly to your `docker-compose.yml`: + +```yaml +services: + web: + environment: + - MFA_FORCE=true + # ... other configuration +``` + +## Behavior + +When `MFA_FORCE=true` is set: + +1. **After Login**: Users who don't have 2FA enabled will be automatically redirected to the 2FA setup page (`/settings/otp_authentication`) + +2. **Message Display**: A warning message (using Mastodon's flash message system) is shown explaining that 2FA is required due to security policies + +3. **Access Restriction**: Users cannot access most parts of the platform until they configure 2FA + +4. **Allowed Pages**: Users can still access: + + - 2FA setup pages (`/settings/otp_authentication`) + - 2FA confirmation pages (`/settings/two_factor_authentication/confirmation`) + - Account settings (`/settings/profile`) + - Logout (`/auth/sign_out`) + - Setup pages for unconfirmed users (`/auth/setup`) + +5. **User Experience**: A clear message explains why 2FA is required and guides users through the setup process + +## User Interface + +### Message Display + +When MFA forcing is enabled, users will see: + +- **Warning Message**: "The administrator of this site has configured as mandatory that users enable two-factor authentication due to security policies. Please configure your two-factor authentication to continue using the platform." + +- **Flash Message**: Uses Mastodon's built-in flash message system with warning styling + +- **Visual Indicator**: A prominent notice on the 2FA setup page with a security icon + +### Multi-language Support + +The feature includes translations for: + +- English +- Spanish +- And other supported languages + +## Implementation Details + +### Files Modified + +1. **`app/controllers/concerns/mfa_force_concern.rb`**: Core logic for checking MFA requirements +2. **`app/controllers/application_controller.rb`**: Includes the MFA force concern +3. **`app/helpers/flashes_helper.rb`**: Updated to support warning flash messages +4. **`app/views/settings/two_factor_authentication/otp_authentication/show.html.haml`**: Updated to show the forced MFA message +5. **`app/javascript/styles/mastodon/forms.scss`**: Added styles for the MFA force notice +6. **`config/locales/en.yml`**: English translations +7. **`config/locales/es.yml`**: Spanish translations + +### Testing + +Run the tests to verify the functionality: + +```bash +bundle exec rspec spec/controllers/concerns/mfa_force_concern_spec.rb +``` + +## Security Considerations + +- **Existing Users**: Users who already have 2FA enabled are not affected +- **New Users**: All new users must configure 2FA before accessing the platform +- **Admin Access**: Administrators are also subject to this requirement +- **Graceful Degradation**: If the environment variable is not set, the feature is disabled + +## Troubleshooting + +### Common Issues + +1. **Users can't access the platform**: Ensure they complete 2FA setup +2. **Message not appearing**: Check that `MFA_FORCE=true` is set correctly +3. **Translation missing**: Add translations to the appropriate locale files + +### Disabling the Feature + +To disable MFA forcing: + +```bash +# Remove the environment variable or set it to false +MFA_FORCE=false +# or +unset MFA_FORCE +``` + +## Migration Guide + +### For Existing Instances + +1. **Backup**: Always backup your database before enabling this feature +2. **Communication**: Inform users about the new requirement +3. **Testing**: Test in a staging environment first +4. **Gradual Rollout**: Consider enabling for specific user groups first + +### For New Instances + +1. Set `MFA_FORCE=true` in your environment configuration +2. All new users will be required to set up 2FA during registration + +## Related Features + +- **Two-Factor Authentication**: The underlying 2FA system +- **Account Security**: General security features +- **User Management**: Admin tools for managing user accounts diff --git a/spec/controllers/concerns/mfa_force_concern_spec.rb b/spec/controllers/concerns/mfa_force_concern_spec.rb new file mode 100644 index 0000000000..4a88825917 --- /dev/null +++ b/spec/controllers/concerns/mfa_force_concern_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe MfaForceConcern do + controller(ApplicationController) do + def index + render plain: 'OK' + end + end + + let(:user) { Fabricate(:user) } + + before do + routes.draw { get 'index' => 'anonymous#index' } + end + + describe 'MFA force functionality' do + context 'when MFA_FORCE is enabled' do + before do + allow(ENV).to receive(:[]).with('MFA_FORCE').and_return('true') + sign_in user, scope: :user + end + + context 'when user has MFA enabled' do + before do + user.update(otp_required_for_login: true) + end + + it 'allows access to normal pages' do + get :index + expect(response).to have_http_status(200) + end + end + + context 'when user does not have MFA enabled' do + before do + user.update(otp_required_for_login: false) + end + + it 'redirects to MFA setup page' do + get :index + expect(response).to redirect_to(settings_otp_authentication_path) + end + + it 'shows the required message' do + get :index + expect(flash[:warning]).to eq(I18n.t('mfa_force.required_message')) + end + + context 'when accessing MFA setup pages' do + it 'allows access to OTP authentication page' do + allow(controller.request).to receive(:path).and_return('/settings/otp_authentication') + get :index + expect(response).to have_http_status(200) + end + + it 'allows access to MFA confirmation page' do + allow(controller.request).to receive(:path).and_return('/settings/two_factor_authentication/confirmation') + get :index + expect(response).to have_http_status(200) + end + + it 'allows access to logout' do + allow(controller.request).to receive(:path).and_return('/auth/sign_out') + get :index + expect(response).to have_http_status(200) + end + end + end + end + + context 'when MFA_FORCE is disabled' do + before do + allow(ENV).to receive(:[]).with('MFA_FORCE').and_return('false') + sign_in user, scope: :user + user.update(otp_required_for_login: false) + end + + it 'allows access to normal pages' do + get :index + expect(response).to have_http_status(200) + end + end + + context 'when user is not signed in' do + before do + allow(ENV).to receive(:[]).with('MFA_FORCE').and_return('true') + end + + it 'allows access to normal pages' do + get :index + expect(response).to have_http_status(200) + end + end + end +end From 4e1d61ecd00de51b59fb5a708e1e246f013da28d Mon Sep 17 00:00:00 2001 From: Fredys Fonseca Date: Thu, 17 Jul 2025 05:54:56 +0000 Subject: [PATCH 02/10] Add admin MFA enforcement feature --- .env.development | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.development b/.env.development index 0330da8377..f4237f24eb 100644 --- a/.env.development +++ b/.env.development @@ -2,3 +2,6 @@ ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr + +#testing MFA Enforcement +MFA_FORCE=true From 16b19f48cc8decab9a2e62ef5746a19b79c38e39 Mon Sep 17 00:00:00 2001 From: Fredys Fonseca Date: Thu, 17 Jul 2025 06:36:28 +0000 Subject: [PATCH 03/10] Add admin MFA enforcement feature --- app/controllers/application_controller.rb | 1 + app/helpers/flashes_helper.rb | 2 +- spec/helpers/flashes_helper_spec.rb | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 42abe99048..d4f0ffba81 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,6 +14,7 @@ class ApplicationController < ActionController::Base include DatabaseHelper include AuthorizedFetchHelper include SelfDestructHelper + include MfaForceConcern helper_method :current_account helper_method :current_session diff --git a/app/helpers/flashes_helper.rb b/app/helpers/flashes_helper.rb index 6c5e937098..ccae52f353 100644 --- a/app/helpers/flashes_helper.rb +++ b/app/helpers/flashes_helper.rb @@ -2,6 +2,6 @@ module FlashesHelper def user_facing_flashes - flash.to_hash.slice('alert', 'error', 'notice', 'success') + flash.to_hash.slice('alert', 'error', 'notice', 'success', 'warning') end end diff --git a/spec/helpers/flashes_helper_spec.rb b/spec/helpers/flashes_helper_spec.rb index aaef7ab144..c911c6829f 100644 --- a/spec/helpers/flashes_helper_spec.rb +++ b/spec/helpers/flashes_helper_spec.rb @@ -10,6 +10,7 @@ RSpec.describe FlashesHelper do flash[:error] = 'an error' flash[:notice] = 'a notice' flash[:success] = 'a success' + flash[:warning] = 'a warning' flash[:not_user_facing] = 'a not user facing flash' # rubocop:enable Rails/I18nLocaleTexts end @@ -19,7 +20,8 @@ RSpec.describe FlashesHelper do 'alert' => 'an alert', 'error' => 'an error', 'notice' => 'a notice', - 'success' => 'a success' + 'success' => 'a success', + 'warning' => 'a warning' ) end end From 6cc6c36c449a7dfe807fbf072e21196e4e8d14b5 Mon Sep 17 00:00:00 2001 From: Fredys Fonseca Date: Thu, 17 Jul 2025 06:38:58 +0000 Subject: [PATCH 04/10] Add admin MFA enforcement feature --- config/locales/en.yml | 4 ++++ config/locales/es.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index 4df63f4c73..07cd810025 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2118,3 +2118,7 @@ en: not_supported: This browser doesn't support security keys otp_required: To use security keys please enable two-factor authentication first. registered_on: Registered on %{date} + + mfa_force: + required_message: The administrator of this site has configured as mandatory that users enable two-factor authentication due to security policies. Please configure your two-factor authentication to continue using the platform. + security_policy: Security Policy Requirement diff --git a/config/locales/es.yml b/config/locales/es.yml index 4b10f23b13..a6952fcce1 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -2117,3 +2117,7 @@ es: not_supported: Este navegador no soporta claves de seguridad otp_required: Para usar claves de seguridad, por favor habilite primero la autenticación de doble factor. registered_on: Registrado el %{date} + + mfa_force: + required_message: El administrador de este sitio ha configurado como obligatorio que los usuarios habiliten la autenticación de dos factores debido a las políticas de seguridad. Por favor, configura tu autenticación de dos factores para continuar usando la plataforma. + security_policy: Requisito de Política de Seguridad From 1c52aa76eb125cbb31303c09d5bb24d0206a1bce Mon Sep 17 00:00:00 2001 From: Fredys Fonseca Date: Wed, 23 Jul 2025 04:01:53 +0000 Subject: [PATCH 05/10] feature/require-mfa-by-admin - move docs --- .env.development | 3 - .env.production.sample | 3 + app/controllers/concerns/mfa_force_concern.rb | 4 +- config/locales/en.yml | 2 +- config/locales/es.yml | 6 +- docs/MFA_FORCE.md | 139 ------------------ .../concerns/mfa_force_concern_spec.rb | 12 +- 7 files changed, 13 insertions(+), 156 deletions(-) delete mode 100644 docs/MFA_FORCE.md diff --git a/.env.development b/.env.development index f4237f24eb..0330da8377 100644 --- a/.env.development +++ b/.env.development @@ -2,6 +2,3 @@ ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr - -#testing MFA Enforcement -MFA_FORCE=true diff --git a/.env.production.sample b/.env.production.sample index 15004b9d0d..d11c65aeaa 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -109,3 +109,6 @@ FETCH_REPLIES_MAX_SINGLE=500 # Max number of replies Collection pages to fetch - total FETCH_REPLIES_MAX_PAGES=500 + +# MFA Required for Users +REQUIRE_MULTI_FACTOR_AUTH=false \ No newline at end of file diff --git a/app/controllers/concerns/mfa_force_concern.rb b/app/controllers/concerns/mfa_force_concern.rb index c2dc4f8a98..15d4661a91 100644 --- a/app/controllers/concerns/mfa_force_concern.rb +++ b/app/controllers/concerns/mfa_force_concern.rb @@ -14,12 +14,12 @@ module MfaForceConcern return if current_user.otp_enabled? return if mfa_setup_allowed_paths? - flash[:warning] = I18n.t('mfa_force.required_message') + flash[:warning] = I18n.t('require_multi_factor_auth.required_message') redirect_to settings_otp_authentication_path end def mfa_force_enabled? - ENV['MFA_FORCE'] == 'true' + ENV['REQUIRE_MULTI_FACTOR_AUTH'] == 'true' end def mfa_setup_allowed_paths? diff --git a/config/locales/en.yml b/config/locales/en.yml index 07cd810025..243634873b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2119,6 +2119,6 @@ en: otp_required: To use security keys please enable two-factor authentication first. registered_on: Registered on %{date} - mfa_force: + require_multi_factor_auth: required_message: The administrator of this site has configured as mandatory that users enable two-factor authentication due to security policies. Please configure your two-factor authentication to continue using the platform. security_policy: Security Policy Requirement diff --git a/config/locales/es.yml b/config/locales/es.yml index a6952fcce1..c2aabea9f5 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -2116,8 +2116,4 @@ es: not_enabled: Aún no has activado WebAuthn not_supported: Este navegador no soporta claves de seguridad otp_required: Para usar claves de seguridad, por favor habilite primero la autenticación de doble factor. - registered_on: Registrado el %{date} - - mfa_force: - required_message: El administrador de este sitio ha configurado como obligatorio que los usuarios habiliten la autenticación de dos factores debido a las políticas de seguridad. Por favor, configura tu autenticación de dos factores para continuar usando la plataforma. - security_policy: Requisito de Política de Seguridad + registered_on: Registrado el %{date} \ No newline at end of file diff --git a/docs/MFA_FORCE.md b/docs/MFA_FORCE.md deleted file mode 100644 index 2a51496774..0000000000 --- a/docs/MFA_FORCE.md +++ /dev/null @@ -1,139 +0,0 @@ -# MFA Force Feature - -## Overview - -The MFA Force feature allows administrators to require all users to enable two-factor authentication (2FA) before they can access the platform. This is useful for organizations with strict security policies. - -## Configuration - -### Environment Variable - -To enable MFA forcing, set the following environment variable: - -```bash -MFA_FORCE=true -``` - -### Docker Compose - -Add the environment variable to your `.env.production` file: - -```env -MFA_FORCE=true -``` - -Or add it directly to your `docker-compose.yml`: - -```yaml -services: - web: - environment: - - MFA_FORCE=true - # ... other configuration -``` - -## Behavior - -When `MFA_FORCE=true` is set: - -1. **After Login**: Users who don't have 2FA enabled will be automatically redirected to the 2FA setup page (`/settings/otp_authentication`) - -2. **Message Display**: A warning message (using Mastodon's flash message system) is shown explaining that 2FA is required due to security policies - -3. **Access Restriction**: Users cannot access most parts of the platform until they configure 2FA - -4. **Allowed Pages**: Users can still access: - - - 2FA setup pages (`/settings/otp_authentication`) - - 2FA confirmation pages (`/settings/two_factor_authentication/confirmation`) - - Account settings (`/settings/profile`) - - Logout (`/auth/sign_out`) - - Setup pages for unconfirmed users (`/auth/setup`) - -5. **User Experience**: A clear message explains why 2FA is required and guides users through the setup process - -## User Interface - -### Message Display - -When MFA forcing is enabled, users will see: - -- **Warning Message**: "The administrator of this site has configured as mandatory that users enable two-factor authentication due to security policies. Please configure your two-factor authentication to continue using the platform." - -- **Flash Message**: Uses Mastodon's built-in flash message system with warning styling - -- **Visual Indicator**: A prominent notice on the 2FA setup page with a security icon - -### Multi-language Support - -The feature includes translations for: - -- English -- Spanish -- And other supported languages - -## Implementation Details - -### Files Modified - -1. **`app/controllers/concerns/mfa_force_concern.rb`**: Core logic for checking MFA requirements -2. **`app/controllers/application_controller.rb`**: Includes the MFA force concern -3. **`app/helpers/flashes_helper.rb`**: Updated to support warning flash messages -4. **`app/views/settings/two_factor_authentication/otp_authentication/show.html.haml`**: Updated to show the forced MFA message -5. **`app/javascript/styles/mastodon/forms.scss`**: Added styles for the MFA force notice -6. **`config/locales/en.yml`**: English translations -7. **`config/locales/es.yml`**: Spanish translations - -### Testing - -Run the tests to verify the functionality: - -```bash -bundle exec rspec spec/controllers/concerns/mfa_force_concern_spec.rb -``` - -## Security Considerations - -- **Existing Users**: Users who already have 2FA enabled are not affected -- **New Users**: All new users must configure 2FA before accessing the platform -- **Admin Access**: Administrators are also subject to this requirement -- **Graceful Degradation**: If the environment variable is not set, the feature is disabled - -## Troubleshooting - -### Common Issues - -1. **Users can't access the platform**: Ensure they complete 2FA setup -2. **Message not appearing**: Check that `MFA_FORCE=true` is set correctly -3. **Translation missing**: Add translations to the appropriate locale files - -### Disabling the Feature - -To disable MFA forcing: - -```bash -# Remove the environment variable or set it to false -MFA_FORCE=false -# or -unset MFA_FORCE -``` - -## Migration Guide - -### For Existing Instances - -1. **Backup**: Always backup your database before enabling this feature -2. **Communication**: Inform users about the new requirement -3. **Testing**: Test in a staging environment first -4. **Gradual Rollout**: Consider enabling for specific user groups first - -### For New Instances - -1. Set `MFA_FORCE=true` in your environment configuration -2. All new users will be required to set up 2FA during registration - -## Related Features - -- **Two-Factor Authentication**: The underlying 2FA system -- **Account Security**: General security features -- **User Management**: Admin tools for managing user accounts diff --git a/spec/controllers/concerns/mfa_force_concern_spec.rb b/spec/controllers/concerns/mfa_force_concern_spec.rb index 4a88825917..f1d33fa9e0 100644 --- a/spec/controllers/concerns/mfa_force_concern_spec.rb +++ b/spec/controllers/concerns/mfa_force_concern_spec.rb @@ -16,9 +16,9 @@ RSpec.describe MfaForceConcern do end describe 'MFA force functionality' do - context 'when MFA_FORCE is enabled' do + context 'when REQUIRE_MULTI_FACTOR_AUTH is enabled' do before do - allow(ENV).to receive(:[]).with('MFA_FORCE').and_return('true') + allow(ENV).to receive(:[]).with('REQUIRE_MULTI_FACTOR_AUTH').and_return('true') sign_in user, scope: :user end @@ -45,7 +45,7 @@ RSpec.describe MfaForceConcern do it 'shows the required message' do get :index - expect(flash[:warning]).to eq(I18n.t('mfa_force.required_message')) + expect(flash[:warning]).to eq(I18n.t('require_multi_factor_auth.required_message')) end context 'when accessing MFA setup pages' do @@ -70,9 +70,9 @@ RSpec.describe MfaForceConcern do end end - context 'when MFA_FORCE is disabled' do + context 'when REQUIRE_MULTI_FACTOR_AUTH is disabled' do before do - allow(ENV).to receive(:[]).with('MFA_FORCE').and_return('false') + allow(ENV).to receive(:[]).with('REQUIRE_MULTI_FACTOR_AUTH').and_return('false') sign_in user, scope: :user user.update(otp_required_for_login: false) end @@ -85,7 +85,7 @@ RSpec.describe MfaForceConcern do context 'when user is not signed in' do before do - allow(ENV).to receive(:[]).with('MFA_FORCE').and_return('true') + allow(ENV).to receive(:[]).with('REQUIRE_MULTI_FACTOR_AUTH').and_return('true') end it 'allows access to normal pages' do From 6fda7a9f56af4f66a00bf1386cfcdf148fa09517 Mon Sep 17 00:00:00 2001 From: Fredys Fonseca Date: Wed, 23 Jul 2025 04:15:15 +0000 Subject: [PATCH 06/10] feature/require-mfa-by-admin - Using ClimateControl --- app/controllers/concerns/mfa_force_concern.rb | 2 +- app/helpers/flashes_helper.rb | 2 +- .../concerns/mfa_force_concern_spec.rb | 70 +++++++++++-------- spec/helpers/flashes_helper_spec.rb | 4 +- 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/app/controllers/concerns/mfa_force_concern.rb b/app/controllers/concerns/mfa_force_concern.rb index 15d4661a91..18b2f659a9 100644 --- a/app/controllers/concerns/mfa_force_concern.rb +++ b/app/controllers/concerns/mfa_force_concern.rb @@ -14,7 +14,7 @@ module MfaForceConcern return if current_user.otp_enabled? return if mfa_setup_allowed_paths? - flash[:warning] = I18n.t('require_multi_factor_auth.required_message') + flash[:alert] = I18n.t('require_multi_factor_auth.required_message') redirect_to settings_otp_authentication_path end diff --git a/app/helpers/flashes_helper.rb b/app/helpers/flashes_helper.rb index ccae52f353..6c5e937098 100644 --- a/app/helpers/flashes_helper.rb +++ b/app/helpers/flashes_helper.rb @@ -2,6 +2,6 @@ module FlashesHelper def user_facing_flashes - flash.to_hash.slice('alert', 'error', 'notice', 'success', 'warning') + flash.to_hash.slice('alert', 'error', 'notice', 'success') end end diff --git a/spec/controllers/concerns/mfa_force_concern_spec.rb b/spec/controllers/concerns/mfa_force_concern_spec.rb index f1d33fa9e0..4181f5d8c1 100644 --- a/spec/controllers/concerns/mfa_force_concern_spec.rb +++ b/spec/controllers/concerns/mfa_force_concern_spec.rb @@ -18,8 +18,9 @@ RSpec.describe MfaForceConcern do describe 'MFA force functionality' do context 'when REQUIRE_MULTI_FACTOR_AUTH is enabled' do before do - allow(ENV).to receive(:[]).with('REQUIRE_MULTI_FACTOR_AUTH').and_return('true') - sign_in user, scope: :user + ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do + sign_in user, scope: :user + end end context 'when user has MFA enabled' do @@ -28,8 +29,10 @@ RSpec.describe MfaForceConcern do end it 'allows access to normal pages' do - get :index - expect(response).to have_http_status(200) + ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do + get :index + expect(response).to have_http_status(200) + end end end @@ -39,32 +42,42 @@ RSpec.describe MfaForceConcern do end it 'redirects to MFA setup page' do - get :index - expect(response).to redirect_to(settings_otp_authentication_path) + ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do + get :index + expect(response).to redirect_to(settings_otp_authentication_path) + end end it 'shows the required message' do - get :index - expect(flash[:warning]).to eq(I18n.t('require_multi_factor_auth.required_message')) + ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do + get :index + expect(flash[:alert]).to eq(I18n.t('require_multi_factor_auth.required_message')) + end end context 'when accessing MFA setup pages' do it 'allows access to OTP authentication page' do - allow(controller.request).to receive(:path).and_return('/settings/otp_authentication') - get :index - expect(response).to have_http_status(200) + ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do + allow(controller.request).to receive(:path).and_return('/settings/otp_authentication') + get :index + expect(response).to have_http_status(200) + end end it 'allows access to MFA confirmation page' do - allow(controller.request).to receive(:path).and_return('/settings/two_factor_authentication/confirmation') - get :index - expect(response).to have_http_status(200) + ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do + allow(controller.request).to receive(:path).and_return('/settings/two_factor_authentication/confirmation') + get :index + expect(response).to have_http_status(200) + end end it 'allows access to logout' do - allow(controller.request).to receive(:path).and_return('/auth/sign_out') - get :index - expect(response).to have_http_status(200) + ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do + allow(controller.request).to receive(:path).and_return('/auth/sign_out') + get :index + expect(response).to have_http_status(200) + end end end end @@ -72,25 +85,26 @@ RSpec.describe MfaForceConcern do context 'when REQUIRE_MULTI_FACTOR_AUTH is disabled' do before do - allow(ENV).to receive(:[]).with('REQUIRE_MULTI_FACTOR_AUTH').and_return('false') - sign_in user, scope: :user - user.update(otp_required_for_login: false) + ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'false') do + sign_in user, scope: :user + user.update(otp_required_for_login: false) + end end it 'allows access to normal pages' do - get :index - expect(response).to have_http_status(200) + ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'false') do + get :index + expect(response).to have_http_status(200) + end end end context 'when user is not signed in' do - before do - allow(ENV).to receive(:[]).with('REQUIRE_MULTI_FACTOR_AUTH').and_return('true') - end - it 'allows access to normal pages' do - get :index - expect(response).to have_http_status(200) + ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do + get :index + expect(response).to have_http_status(200) + end end end end diff --git a/spec/helpers/flashes_helper_spec.rb b/spec/helpers/flashes_helper_spec.rb index c911c6829f..aaef7ab144 100644 --- a/spec/helpers/flashes_helper_spec.rb +++ b/spec/helpers/flashes_helper_spec.rb @@ -10,7 +10,6 @@ RSpec.describe FlashesHelper do flash[:error] = 'an error' flash[:notice] = 'a notice' flash[:success] = 'a success' - flash[:warning] = 'a warning' flash[:not_user_facing] = 'a not user facing flash' # rubocop:enable Rails/I18nLocaleTexts end @@ -20,8 +19,7 @@ RSpec.describe FlashesHelper do 'alert' => 'an alert', 'error' => 'an error', 'notice' => 'a notice', - 'success' => 'a success', - 'warning' => 'a warning' + 'success' => 'a success' ) end end From 7a957b1f49def089f9a288f54ff591b7dc086bbd Mon Sep 17 00:00:00 2001 From: Fredys Fonseca Date: Wed, 23 Jul 2025 05:01:46 +0000 Subject: [PATCH 07/10] feature/require-mfa-by-admin - Update Flash --- app/controllers/concerns/mfa_force_concern.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/concerns/mfa_force_concern.rb b/app/controllers/concerns/mfa_force_concern.rb index 18b2f659a9..ca969bc086 100644 --- a/app/controllers/concerns/mfa_force_concern.rb +++ b/app/controllers/concerns/mfa_force_concern.rb @@ -4,7 +4,7 @@ module MfaForceConcern extend ActiveSupport::Concern included do - before_action :check_mfa_requirement, if: :user_signed_in? + prepend_before_action :check_mfa_requirement, if: :user_signed_in? end private From 673d875a95968999296183f646a9096fb2918fd2 Mon Sep 17 00:00:00 2001 From: Fredys Fonseca Date: Wed, 23 Jul 2025 05:36:07 +0000 Subject: [PATCH 08/10] feature/require-mfa-by-admin - Change to config for --- app/controllers/concerns/mfa_force_concern.rb | 6 +++++- config/mfa.yml | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 config/mfa.yml diff --git a/app/controllers/concerns/mfa_force_concern.rb b/app/controllers/concerns/mfa_force_concern.rb index ca969bc086..4c2c00aec3 100644 --- a/app/controllers/concerns/mfa_force_concern.rb +++ b/app/controllers/concerns/mfa_force_concern.rb @@ -19,7 +19,7 @@ module MfaForceConcern end def mfa_force_enabled? - ENV['REQUIRE_MULTI_FACTOR_AUTH'] == 'true' + mfa_config[:force_enabled] end def mfa_setup_allowed_paths? @@ -36,4 +36,8 @@ module MfaForceConcern allowed_paths.any? { |path| request.path.start_with?(path) } end + + def mfa_config + @mfa_config ||= Rails.application.config_for(:mfa) + end end diff --git a/config/mfa.yml b/config/mfa.yml new file mode 100644 index 0000000000..9e365ec9ce --- /dev/null +++ b/config/mfa.yml @@ -0,0 +1,15 @@ +# Multi-Factor Authentication configuration +default: &default + force_enabled: false + +development: + <<: *default + force_enabled: <%= ENV.fetch('REQUIRE_MULTI_FACTOR_AUTH', 'false') == 'true' %> + +test: + <<: *default + force_enabled: false + +production: + <<: *default + force_enabled: <%= ENV.fetch('REQUIRE_MULTI_FACTOR_AUTH', 'false') == 'true' %> From 1073956fbc7635c65deec6ef8ecbee7623a63de2 Mon Sep 17 00:00:00 2001 From: Fredys Fonseca Date: Wed, 23 Jul 2025 05:41:48 +0000 Subject: [PATCH 09/10] feature/require-mfa-by-admin - Refact Opt-Out --- .../auth/registrations_controller.rb | 22 +++++++++++-------- app/controllers/auth/sessions_controller.rb | 5 +++++ app/controllers/auth/setup_controller.rb | 5 +++++ app/controllers/concerns/mfa_force_concern.rb | 19 +++++----------- .../confirmations_controller.rb | 4 ++++ .../otp_authentication_controller.rb | 4 ++++ ...actor_authentication_methods_controller.rb | 4 ++++ 7 files changed, 40 insertions(+), 23 deletions(-) diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 3b42dc48ba..aa00c1726e 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -100,12 +100,13 @@ class Auth::RegistrationsController < Devise::RegistrationsController end end - private - def set_invite @invite = begin - invite = Invite.find_by(code: invite_code) if invite_code.present? - invite if invite&.valid_for_use? + if invite_code.present? + Invite.find_by(code: invite_code) + elsif params[:invite_code].present? + Invite.find_by(code: params[:invite_code]) + end end end @@ -132,17 +133,20 @@ class Auth::RegistrationsController < Devise::RegistrationsController def require_rules_acceptance! return if @rules.empty? || (session[:accept_token].present? && params[:accept] == session[:accept_token]) - @accept_token = session[:accept_token] = SecureRandom.hex - @invite_code = invite_code - - set_locale { render :rules } + session[:accept_token] = SecureRandom.hex(16) + redirect_to new_user_registration_path(accept: session[:accept_token]) end def is_flashing_format? # rubocop:disable Naming/PredicatePrefix if params[:action] == 'create' - false # Disable flash messages for sign-up + false else super end end + + def skip_mfa_force? + # Allow profile editing even when MFA is required + %w(edit update).include?(action_name) + end end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index c52bda67b0..b802940ec3 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -201,4 +201,9 @@ class Auth::SessionsController < Devise::SessionsController format.all { super } end end + + def skip_mfa_force? + # Allow logout to work even when MFA is required + action_name == 'destroy' + end end diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index 5e7b14646a..0db7fa4f33 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -37,4 +37,9 @@ class Auth::SetupController < ApplicationController def user_params params.expect(user: [:email]) end + + def skip_mfa_force? + # Allow auth setup even when MFA is required + true + end end diff --git a/app/controllers/concerns/mfa_force_concern.rb b/app/controllers/concerns/mfa_force_concern.rb index 4c2c00aec3..1a94e1fa1c 100644 --- a/app/controllers/concerns/mfa_force_concern.rb +++ b/app/controllers/concerns/mfa_force_concern.rb @@ -12,7 +12,7 @@ module MfaForceConcern def check_mfa_requirement return unless mfa_force_enabled? return if current_user.otp_enabled? - return if mfa_setup_allowed_paths? + return if mfa_force_skipped? flash[:alert] = I18n.t('require_multi_factor_auth.required_message') redirect_to settings_otp_authentication_path @@ -22,19 +22,10 @@ module MfaForceConcern mfa_config[:force_enabled] end - def mfa_setup_allowed_paths? - allowed_paths = [ - settings_otp_authentication_path, - new_settings_two_factor_authentication_confirmation_path, - settings_two_factor_authentication_confirmation_path, - settings_two_factor_authentication_methods_path, - settings_two_factor_authentication_recovery_codes_path, - destroy_user_session_path, - auth_setup_path, - edit_user_registration_path, - ] - - allowed_paths.any? { |path| request.path.start_with?(path) } + def mfa_force_skipped? + # Allow controllers to opt out of MFA force requirement + # by defining skip_mfa_force? method + respond_to?(:skip_mfa_force?) && skip_mfa_force? end def mfa_config diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index eae990e79b..d0028e9b44 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -53,6 +53,10 @@ module Settings def ensure_otp_secret redirect_to settings_otp_authentication_path if session[:new_otp_secret].blank? end + + def skip_mfa_force? + true + end end end end diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb index ca8d46afe4..97284fb41a 100644 --- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -25,6 +25,10 @@ module Settings def verify_otp_not_enabled redirect_to settings_two_factor_authentication_methods_path if current_user.otp_enabled? end + + def skip_mfa_force? + true + end end end end diff --git a/app/controllers/settings/two_factor_authentication_methods_controller.rb b/app/controllers/settings/two_factor_authentication_methods_controller.rb index a6d5c1fe2d..4aa00a37b0 100644 --- a/app/controllers/settings/two_factor_authentication_methods_controller.rb +++ b/app/controllers/settings/two_factor_authentication_methods_controller.rb @@ -24,5 +24,9 @@ module Settings def require_otp_enabled redirect_to settings_otp_authentication_path unless current_user.otp_enabled? end + + def skip_mfa_force? + true + end end end From a8f5e3fa6255204b7663683b51596c80a130d44e Mon Sep 17 00:00:00 2001 From: Fredys Fonseca Date: Wed, 23 Jul 2025 18:00:09 +0000 Subject: [PATCH 10/10] feature/require-mfa-by-admin - Using Skip_before_action --- app/controllers/auth/registrations_controller.rb | 6 +----- app/controllers/auth/sessions_controller.rb | 14 +++----------- app/controllers/auth/setup_controller.rb | 6 +----- app/controllers/concerns/mfa_force_concern.rb | 7 ------- .../confirmations_controller.rb | 5 +---- .../otp_authentication_controller.rb | 5 +---- ...two_factor_authentication_methods_controller.rb | 5 +---- 7 files changed, 8 insertions(+), 40 deletions(-) diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index aa00c1726e..0350114280 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -18,6 +18,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController skip_before_action :check_self_destruct!, only: [:edit, :update] skip_before_action :require_functional!, only: [:edit, :update] + skip_before_action :check_mfa_requirement, only: [:edit, :update] def new super(&:build_invite_request) @@ -144,9 +145,4 @@ class Auth::RegistrationsController < Devise::RegistrationsController super end end - - def skip_mfa_force? - # Allow profile editing even when MFA is required - %w(edit update).include?(action_name) - end end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index b802940ec3..d101ab252f 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -11,6 +11,7 @@ class Auth::SessionsController < Devise::SessionsController skip_before_action :require_no_authentication, only: [:create] skip_before_action :require_functional! skip_before_action :update_user_sign_in + skip_before_action :check_mfa_requirement, only: [:destroy] prepend_before_action :check_suspicious!, only: [:create] @@ -193,17 +194,8 @@ class Auth::SessionsController < Devise::SessionsController def respond_to_on_destroy respond_to do |format| - format.json do - render json: { - redirect_to: after_sign_out_path_for(resource_name), - }, status: 200 - end - format.all { super } + format.any(*navigational_formats) { redirect_to after_sign_out_path_for(:user) } + format.all { head 204 } end end - - def skip_mfa_force? - # Allow logout to work even when MFA is required - action_name == 'destroy' - end end diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index 0db7fa4f33..519452d9d9 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -8,6 +8,7 @@ class Auth::SetupController < ApplicationController before_action :set_user skip_before_action :require_functional! + skip_before_action :check_mfa_requirement def show; end @@ -37,9 +38,4 @@ class Auth::SetupController < ApplicationController def user_params params.expect(user: [:email]) end - - def skip_mfa_force? - # Allow auth setup even when MFA is required - true - end end diff --git a/app/controllers/concerns/mfa_force_concern.rb b/app/controllers/concerns/mfa_force_concern.rb index 1a94e1fa1c..ca68d3649c 100644 --- a/app/controllers/concerns/mfa_force_concern.rb +++ b/app/controllers/concerns/mfa_force_concern.rb @@ -12,7 +12,6 @@ module MfaForceConcern def check_mfa_requirement return unless mfa_force_enabled? return if current_user.otp_enabled? - return if mfa_force_skipped? flash[:alert] = I18n.t('require_multi_factor_auth.required_message') redirect_to settings_otp_authentication_path @@ -22,12 +21,6 @@ module MfaForceConcern mfa_config[:force_enabled] end - def mfa_force_skipped? - # Allow controllers to opt out of MFA force requirement - # by defining skip_mfa_force? method - respond_to?(:skip_mfa_force?) && skip_mfa_force? - end - def mfa_config @mfa_config ||= Rails.application.config_for(:mfa) end diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index d0028e9b44..00b2c68cca 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -6,6 +6,7 @@ module Settings include ChallengableConcern skip_before_action :require_functional! + skip_before_action :check_mfa_requirement before_action :require_challenge! before_action :ensure_otp_secret @@ -53,10 +54,6 @@ module Settings def ensure_otp_secret redirect_to settings_otp_authentication_path if session[:new_otp_secret].blank? end - - def skip_mfa_force? - true - end end end end diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb index 97284fb41a..9e03fcdd35 100644 --- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -6,6 +6,7 @@ module Settings include ChallengableConcern skip_before_action :require_functional! + skip_before_action :check_mfa_requirement before_action :verify_otp_not_enabled, only: [:show] before_action :require_challenge!, only: [:create] @@ -25,10 +26,6 @@ module Settings def verify_otp_not_enabled redirect_to settings_two_factor_authentication_methods_path if current_user.otp_enabled? end - - def skip_mfa_force? - true - end end end end diff --git a/app/controllers/settings/two_factor_authentication_methods_controller.rb b/app/controllers/settings/two_factor_authentication_methods_controller.rb index 4aa00a37b0..0face326e4 100644 --- a/app/controllers/settings/two_factor_authentication_methods_controller.rb +++ b/app/controllers/settings/two_factor_authentication_methods_controller.rb @@ -6,6 +6,7 @@ module Settings skip_before_action :check_self_destruct! skip_before_action :require_functional! + skip_before_action :check_mfa_requirement before_action :require_challenge!, only: :disable before_action :require_otp_enabled @@ -24,9 +25,5 @@ module Settings def require_otp_enabled redirect_to settings_otp_authentication_path unless current_user.otp_enabled? end - - def skip_mfa_force? - true - end end end