Two-factor authentication feature in UI part.#1729
Conversation
dd0ca63 to
eba6f1b
Compare
| else | ||
| do_login | ||
| end | ||
| end |
There was a problem hiding this comment.
This if else logic could be simpler if 'sessions/otp_prompt' made request to a new method like mfa_create.
There was a problem hiding this comment.
yeah. and in mfa_create check session[:mfa_user] seems can achieve the same functionality
|
|
||
| BUNDLED WITH | ||
| 1.16.0 | ||
| 1.16.1 |
There was a problem hiding this comment.
Do I need to specify it in Gemfile? Or just downgrade my Bundler locally and Gemfile.lock will be changed? Also, is there any difference between the two versions?
There was a problem hiding this comment.
just downgrade my Bundler locally and Gemfile.lock will be changed?
yes.
is there any difference between the two versions?
changing bundler version here would mean we need to update bundler on our servers. we prefer to do that separately (not as part of any PR)
There was a problem hiding this comment.
I've got it done. Thanks
| flash[:success] = t('.disable_success') | ||
| current_user.disable_mfa! | ||
| else | ||
| flash[:error] = t('profiles.mfa_enable.otp_auth_failed') |
There was a problem hiding this comment.
I got t('profiles.mfa_enable.otp_auth_failed') as not found.
There was a problem hiding this comment.
oh! sorry, that should be t('two_factor_auths.create.otp_auth_failed')
There was a problem hiding this comment.
Is semantics of flash[:notice] more suitable here?
| return unless current_user.no_auth? | ||
| flash[:error] = t('two_factor_auths.no_auth_no_access') | ||
| redirect_to edit_profile_path | ||
| end |
There was a problem hiding this comment.
Perhaps, require_mfa_enabled and require_mfa_disabled are better names for these functions. Also, it would make more sense to use current_user.mfa_enabled? here instead of current_user.no_auth?.
There was a problem hiding this comment.
that really makes more sense, thanks
| end | ||
|
|
||
| private | ||
| def verifying_otp? |
There was a problem hiding this comment.
I don't think this method is used.
There was a problem hiding this comment.
That should be deleted.
|
|
||
| def require_mfa_enabled | ||
| return if current_user.mfa_enabled? | ||
| flash[:error] = t('two_factor_auths.no_auth_no_access') |
There was a problem hiding this comment.
Can we please update name of these i18n keys as well?
There was a problem hiding this comment.
Yes. I'm curious that is there any tool for such i18n sync/generation (between multi-lang versions)?
There was a problem hiding this comment.
I am not sure what do you mean by i18n sync/generation. Can you please elaborate?
Can we please update name of these i18n keys as well?
no_auth_no_access -> access_denied (or something more appropriate).
There was a problem hiding this comment.
Maybe my words are not accurate, I just wanted to know whether there's tool for checking whether any part in i18n of some language is missing and automatically filling them.
Thanks for the advice, I'll do checks about all i18n names I created.
| validates :password, length: { within: 10..200 }, allow_nil: true, unless: :skip_password_validation? | ||
| validate :unconfirmed_email_uniqueness | ||
|
|
||
| enum mfa_level: { no_auth: 0, auth_only: 1, auth_and_write: 2 } |
There was a problem hiding this comment.
Would renaming no_auth to mfa_disabled or just disabled make it easier to understand?
There was a problem hiding this comment.
Since Rails enums generates methods like no_auth? and no_auth! for class User automatically, I think using disabled will be a little bit ambiguous
There was a problem hiding this comment.
no_auth is not representing (mfa being disabled) what is says it is (no_auth ~ no authentication?).
There was a problem hiding this comment.
(Also reply for the comment on mfa/two_factor_auth names)
Are tfa and two_factor_auth easily recognized as the same? If so, I think something like no_tfa is better name.
There was a problem hiding this comment.
Because two_factor_auth and two_factor_authentication is too lengty...
| @user = find_user(params.require(:session)) | ||
|
|
||
| if mfa_enabled? && @user&.mfa_enabled? | ||
| session[:mfa_user] = @user.id |
There was a problem hiding this comment.
Can we please continue using handle (session[:who]) to find users?
There was a problem hiding this comment.
You mean store who in session and find user with who, not id? That seems better in security.
There was a problem hiding this comment.
Since we should not store passwords in session, here we just needs to store session[:who]. But in find_user, session is just a param, not real sessions. So it seems that we do this to just hide user's id.
|
You have used |
| @request.cookies[:mfa_feature] = 'true' | ||
| end | ||
|
|
||
| context 'when 2fa already enabled' do |
| end | ||
| end | ||
|
|
||
| context 'when 2fa not enabled' do |
| assert @user.no_auth? | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Please add a test for mfa being disabled by default (without @user.disable_mfa! in setup).
|
Thank you for following single and double quotes as per file 😆 we will enforce one of them soon. Can you please write integration tests as well? |
It would be so careful to find that! I also want to know if we have such spec on commit messages. (Forgive me for recent commits with bunch of unrelated changes, I'll take care of it) Integration tests are similar to tests on controllers, it focuses on 'a feature' or 'a workflow'? |
Not that I know of, however it should be too difficult to come up with a script of your own. We don't really need to create keys in all languages as rails falls back to default language where keys in other languages are missing. Since #1522 we started requiring that all keys be created. |
| true | ||
| else | ||
| false | ||
| end |
There was a problem hiding this comment.
Please move the code else to a new method. Also, we would want to do mfa_recovery_codes.include?(otp) at last, ie this logic should be in else and totp.verify.. should be elsif.
|
|
||
| should 'disable mfa by default' do | ||
| refute @user.mfa_enabled? | ||
| end |
There was a problem hiding this comment.
Right. I forgot to move it.
| should redirect_to('the dashboard') { dashboard_path } | ||
| should "clear user name in session" do | ||
| assert @controller.session[:mfa_user].nil? | ||
| end |
There was a problem hiding this comment.
Please add an assert in this and one above (when OTP is correct), for user being logged in.
| should respond_with :success | ||
| should "save user name in session" do | ||
| assert @controller.session[:mfa_user] == @user.handle | ||
| end |
There was a problem hiding this comment.
Please add test for assert page.has_content?
| if is_recovery | ||
| mfa_recovery_codes.delete(otp) | ||
| save!(validate: false) | ||
| end |
There was a problem hiding this comment.
please change this to return false unless is_recovery. last line in otp_verified? could be just save!(validate: false)
| end | ||
|
|
||
| def otp_verified?(otp) | ||
| if no_mfa? || verify_digit_otp(otp) |
There was a problem hiding this comment.
I don't think no_mfa? is needed here. As far as I can tell, opt_verified? is always called only after ensuring that mfa is enabled.
There was a problem hiding this comment.
Yes. If remove no_mfa? here, a user who has not enabled mfa will get false for this method, seems more reasonable.
| self.last_otp_at = Time.at(last_success).utc.to_datetime | ||
| save!(validate: false) | ||
| end | ||
| !last_success.nil? |
There was a problem hiding this comment.
if block here could be changed return false unless last_success. IMHO, guard statements are more readable.
| def change | ||
| add_column :users, :last_otp_at, :datetime | ||
| end | ||
| end |
There was a problem hiding this comment.
Please merge these two migration. You can fix ActiveRecord::Schema.define version: manually.
There was a problem hiding this comment.
OK. I'll use the older version number.
| end | ||
|
|
||
| should "show OTP prompt" do | ||
| assert page.has_content? "Multifactor authentication" |
There was a problem hiding this comment.
You don't need to do it twice.
| mfa: | ||
| multifactor_auth: Multifactor authentication | ||
| disabled: You have not yet enabled multifactor authentication. | ||
| go_settings: Go to setting page. |
There was a problem hiding this comment.
Please change this to Register a new device
Add methods and migrations related to 2fa on user model.
Check user's auth settings before entering 2fa settings.
Correct warnings from Rubocop.
Fix bugs when checking users' auth state.
Change 2fa issuer to host of request. Add text 2fa key for manual input.
- Bundler version is reverted back to 1.16.0 in Gemfile.lock. - Add feature flag on 2FA, just set 'mfa_feature=true' in cookie to turn it on. - Move OTP post action into a single method `mfa_create`. - Change two filter names in 2fa settings controller to make it more readable.
- Test OTP requirement in login. - Test model methods of user on verifying OTP. - Test settings of 2fa.
Remove unused 2fa controller method and i18n entry.
- Locale item names is changed from 'two_factor_auths' to 'multifactor_auths'. - `TwoFactorAuths` are renamed into `MultifactorAuths`. - '2fa' in test descriptions is changed into 'mfa'. - `mfa_level` enum cases are now `no_mfa`, `mfa_login_only` and `mfa_login_and_write`.
- Add integration tests for user login with mfa and 2fa settings. - Move mfa default state test into user test. - Add controller test for otp prompt when user login with mfa enabled.
- Merge two mfa related migrations into one. - Refactor `otp_verified?` method using guard clauses. - Remove duplicate test case on OTP prompt. - Default returns false for `otp_verified?` if mfa not enabled yet.
|
Thanks @ecnelises |
|
Hi @Kholoudatef-90 |
2369: [GSoC] Multi-factor feature for RubyGems. r=hsbt a=ecnelises # Description: Hello. This is my GSoC project, dedicated to add multifactor authentication to both RubyGems command program and Gemcutter ([RubyGems.org](https://rubygems.org)). Work for the Gemcutter part has almost been finished (see [PR #1753](rubygems/rubygems.org#1753), [PR #1729](rubygems/rubygems.org#1729) and a series of [my progress reports](https://ecnelises.github.io/)). ## Content: This PR will contain my changes to RubyGems client, adding multifactor auth for `gem push`, `gem signin` and `gem owner` commands. Since no command for editing profile, adding command for changing multifactor auth settings seems unnecessary. ## Workflow: - User set up multifactor auth well in the site. (into `mfa_login_and_write` level) - When user does the actions requiring MFA, an OTP prompt is shown. Or user can add `--otp` option into command, like `gem push mygem-0.0.0.gem --otp 123456`. - If the OTP is incorrect, operation fails with failure text. ______________ # Tasks: - [x] Add OTP requirement to `push_command`. - [x] Add OTP requirement to `owner_command`. - [x] Add OTP prompt to `sign_in`. - [ ] Support for `yank_command`. - [x] Write related tests. I will abide by the [code of conduct](https://github.com/rubygems/rubygems/blob/master/CODE_OF_CONDUCT.md). Co-authored-by: Qiu Chaofan <fwage73@gmail.com> Co-authored-by: SHIBATA Hiroshi <hsbt@ruby-lang.org>
2369: [GSoC] Multi-factor feature for RubyGems. r=hsbt a=ecnelises # Description: Hello. This is my GSoC project, dedicated to add multifactor authentication to both RubyGems command program and Gemcutter ([RubyGems.org](https://rubygems.org)). Work for the Gemcutter part has almost been finished (see [PR #1753](rubygems/rubygems.org#1753), [PR #1729](rubygems/rubygems.org#1729) and a series of [my progress reports](https://ecnelises.github.io/)). ## Content: This PR will contain my changes to RubyGems client, adding multifactor auth for `gem push`, `gem signin` and `gem owner` commands. Since no command for editing profile, adding command for changing multifactor auth settings seems unnecessary. ## Workflow: - User set up multifactor auth well in the site. (into `mfa_login_and_write` level) - When user does the actions requiring MFA, an OTP prompt is shown. Or user can add `--otp` option into command, like `gem push mygem-0.0.0.gem --otp 123456`. - If the OTP is incorrect, operation fails with failure text. ______________ # Tasks: - [x] Add OTP requirement to `push_command`. - [x] Add OTP requirement to `owner_command`. - [x] Add OTP prompt to `sign_in`. - [ ] Support for `yank_command`. - [x] Write related tests. I will abide by the [code of conduct](https://github.com/rubygems/rubygems/blob/master/CODE_OF_CONDUCT.md). Co-authored-by: Qiu Chaofan <fwage73@gmail.com> Co-authored-by: SHIBATA Hiroshi <hsbt@ruby-lang.org>


This pull request is related to Issue 1725, and my GSoC 2018 project, which has implemented features below.
Things to be done next: