Draft: Ensure that email_otp_required_after is always in a valid state
What does this MR do and why?
Email OTP can be mandatory, permitted, or prohibited, based on various
conditions. email_otp_required_after
drives whether the user
experiences the Email OTP flow. This MR ensures that it is updated
when relevant.
2FA (App-based TOTP or WebAuthN) can be required by either an Instance setting or at a Group setting. Email OTP can be required as a minimum by an Instance setting.
- When users enable 2FA under a mandatory 2FA policy: Email OTP must be disabled and no longer permitted as fallback. This ensures we don't degrade security under the mandatory 2FA policy. However, until they add 2FA, the user still need to do Email-based OTP (if enrolled). This also ensures we don't leave their account less protected while under that policy.
- When users disable 2FA, and the instance requires minimum email OTP: Email OTP must be automatically re-enabled.
Part of issue: https://gitlab.com/gitlab-org/gitlab/-/issues/570173+s
Builds on Add user enrollment restriction logic for Email... (!208412) • Nick Malcolm • 18.6 and replaces Draft: Add enrollment management for Email-base... (!207777 - closed) • Nick Malcolm • 18.6.
Implementation Details
To achieve this we set email_otp_required_after
to a valid state:
- When the User is created or updated
-
User::BuildService
already setsemail_otp_required_after
when appropriate. (Prior MR). - If a user has
disable_two_factor!
called directly, we callupdate_email_otp_required_after_based_on_restrictions
(to turn on Email OTP if needed) -
Users::UpdateService
callsset_email_otp_required_after_based_on_restrictions
, prior to theUser#save
call.
-
- When App-based TOTP is created or removed
- Create:
Profiles::TwoFactorAuthsController
callsUsers::UpdateService
. Implemented there. -
TwoFactor::DestroyService
:- calls
Users::UpdateService
. Implemented there. - Also, in that block, calls
User#disable_two_factor!
. Implemented there for safety.
- calls
-
TwoFactor::DestroyOtpService
: not sure what the difference is (this one might be for admins / API?) but it also callsUsers::UpdateService
.
- Create:
- When WebAuthN is created or removed
- Create: As with App-based, on first create it will generate backup codes via
Users::UpdateService
. -
Webauthn::DestroyService
calls::Users::UpdateService
- Also, in that block, calls
User#destroy_webauthn_device
- Create: As with App-based, on first create it will generate backup codes via
- Finally, during sign in as a last-minute time-of-use safeguard
- We only do this for valid sign ins when deciding whether to show them the Email OTP flow.
Why not have a validate
on the User
record?
In the original MR we did. One reason this does not is due to how conditions that would make User
invalid arise, namely that User can become "invalid" even when it's not touched:
- When
User
changes, e.g. OTP (en|dis)abled - that'd be easy - When
UserDetail
changes - we could introduce atouch
to the parent user record, but that's coupling them - When a
WebAuthN
is created or destroyed - again, we could add atouch
to the parent user record but it's coupling - When an admin changes
Gitlab::CurrentSettings.update!(require_minimum_email_based_otp_for_users_with_passwords: true)
- well, then many/every user becomes invalid unless we also write a background job to go and set them to the correct state
Testing
bspec spec/models/concerns/authn/email_otp_enrollment_spec.rb \
spec/models/user_spec.rb \
spec/requests/verifies_with_email_spec.rb \
spec/services/users/update_service_spec.rb \
spec/services/two_factor/destroy_service_spec.rb \
spec/services/webauthn/destroy_service_spec.rb
The testing approach is a hybrid:
- Full specs for the new Concern
- A mix of behavioral tests for the Model, Request, and Services spec:
- validate that the concern is called as expected, and
- one or two representative sample specs to provide more end-to-end coverage
References
Screenshots or screen recordings
Before | After |
---|---|
To get Email-based OTP configured (not this MR - already in master)
- Log in as admin and change the following settings (to match GitLab.com):
- Under Admin > Settings > General > Sign-up restrictions:
- Turn off "Require admin approval for new sign-ups".
- Set "Email confirmation settings" to "Hard".
- Click "Save changes".
- Under Admin > Settings > General > Sign-in restrictions:
- Turn on "Require email verification when account is locked.".
- Click "Save changes".
- Under Admin > Settings > General > Sign-up restrictions:
To test this MR's logic
There is no UI for enabling and disabling Email OTP yet. That will be in https://gitlab.com/gitlab-org/gitlab/-/work_items/575703.
- Enable the flag for a user:
Feature.enable(:email_based_otp, user)
- Enrol them in Email-based OTP:
user.update!(email_otp_required_after: Time.current)
- Sign in & out. Note that you have to do Email OTP. (Use
/rails/letter_opener
to get the code) - Add user to group with 2FA enforcement
- Sign in - note that you still do Email OTP.
- Enable 2FA. Observe
email_otp_required_after
became nil. Sign out & in again. You have to do 2FA, not email OTP. - Remove the user from the group
- Disable 2FA → observe Email OTP is not required (because the instance doesn't require it)
- Update the application setting to enforce Email-based OTP:
Gitlab::CurrentSettings.update!(require_minimum_email_based_otp_for_users_with_passwords: true)
.- Note that at this point
email_otp_required_after
is still nil
- Note that at this point
- Sign out & in. Note that Email OTP was required
- Disable the instance setting:
Gitlab::CurrentSettings.update!(require_minimum_email_based_otp_for_users_with_passwords: false)
- Note the user can have any combination of
email_otp_required_after
and app-based TOTP / webauthn. There are no restrictions on the user.- In https://gitlab.com/gitlab-org/gitlab/-/work_items/575703+s we will implement the UI
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
-
Test the MR's logic as per the above steps -
Wait for Add user enrollment restriction logic for Email... (!208412) to be merged, then target this MR at master
-
Get Authentication review specifically