Skip to content

Two-factor authentication feature in UI part.#1729

Merged
sonalkr132 merged 18 commits into
rubygems:masterfrom
ecnelises:feature-2fa
Jul 4, 2018
Merged

Two-factor authentication feature in UI part.#1729
sonalkr132 merged 18 commits into
rubygems:masterfrom
ecnelises:feature-2fa

Conversation

@ecnelises
Copy link
Copy Markdown
Member

@ecnelises ecnelises commented Jun 3, 2018

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

  • New gem dependencies.
  • Migration and model methods for two-factor authentication.
  • Two-factor authentication in site's UI part.
    • Two-factor authentication settings (enabling and disabling).
    • Extra authentication checks on One-time Password (OTP).
    • Show recovery codes after user confirmed to enable 2FA.
    • Preventing an otp used twice. (see rotp manual)
    • OTP check with drifts.
    • Unit tests.
    • Integration tests.

Things to be done next:

  • Expire time of 2fa generated qr-code.
  • Two-factor authentication in sites' API part.
  • Settings on the authentication level (auth-only and auth-and-write).

@ecnelises ecnelises force-pushed the feature-2fa branch 2 times, most recently from dd0ca63 to eba6f1b Compare June 6, 2018 01:52
else
do_login
end
end
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if else logic could be simpler if 'sessions/otp_prompt' made request to a new method like mfa_create.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah. and in mfa_create check session[:mfa_user] seems can achieve the same functionality

Comment thread Gemfile.lock Outdated

BUNDLED WITH
1.16.0
1.16.1
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use bundler 1.16.0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

@sonalkr132 sonalkr132 Jun 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got t('profiles.mfa_enable.otp_auth_failed') as not found.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh! sorry, that should be t('two_factor_auths.create.otp_auth_failed')

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is semantics of flash[:notice] more suitable here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error is fine.

return unless current_user.no_auth?
flash[:error] = t('two_factor_auths.no_auth_no_access')
redirect_to edit_profile_path
end
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that really makes more sense, thanks

Comment thread app/controllers/sessions_controller.rb Outdated
end

private
def verifying_otp?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this method is used.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be deleted.


def require_mfa_enabled
return if current_user.mfa_enabled?
flash[:error] = t('two_factor_auths.no_auth_no_access')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please update name of these i18n keys as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I'm curious that is there any tool for such i18n sync/generation (between multi-lang versions)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread app/models/user.rb Outdated
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 }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would renaming no_auth to mfa_disabled or just disabled make it easier to understand?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no_auth is not representing (mfa being disabled) what is says it is (no_auth ~ no authentication?).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because two_factor_auth and two_factor_authentication is too lengty...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no_tfa or no_mfa is better.

Comment thread app/controllers/sessions_controller.rb Outdated
@user = find_user(params.require(:session))

if mfa_enabled? && @user&.mfa_enabled?
session[:mfa_user] = @user.id
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please continue using handle (session[:who]) to find users?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean store who in session and find user with who, not id? That seems better in security.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sonalkr132
Copy link
Copy Markdown
Member

You have used TwoFactor in controller name and mfa in function names. please use either one.

@request.cookies[:mfa_feature] = 'true'
end

context 'when 2fa already enabled' do
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when 2fa is enabled

end
end

context 'when 2fa not enabled' do
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when 2fa is disabled.

Comment thread test/unit/user_test.rb
assert @user.no_auth?
end
end
end
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test for mfa being disabled by default (without @user.disable_mfa! in setup).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, thanks.

@sonalkr132
Copy link
Copy Markdown
Member

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?

@ecnelises
Copy link
Copy Markdown
Member Author

Thank you for following single and double quotes as per file 😆 we will enforce one of them soon.

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'?

@sonalkr132
Copy link
Copy Markdown
Member

whether there's tool for checking whether any part in i18n of some language is missing and automatically filling them.

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.
Feel free to open a issue explaining your problem and need.

Comment thread app/models/user.rb Outdated
true
else
false
end
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this belongs in unit test.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add test for assert page.has_content?

Comment thread app/models/user.rb Outdated
if is_recovery
mfa_recovery_codes.delete(otp)
save!(validate: false)
end
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please change this to return false unless is_recovery. last line in otp_verified? could be just save!(validate: false)

Comment thread app/models/user.rb Outdated
end

def otp_verified?(otp)
if no_mfa? || verify_digit_otp(otp)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

@ecnelises ecnelises Jun 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. If remove no_mfa? here, a user who has not enabled mfa will get false for this method, seems more reasonable.

Comment thread app/models/user.rb Outdated
self.last_otp_at = Time.at(last_success).utc.to_datetime
save!(validate: false)
end
!last_success.nil?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please merge these two migration. You can fix ActiveRecord::Schema.define version: manually.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I'll use the older version number.

end

should "show OTP prompt" do
assert page.has_content? "Multifactor authentication"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to do it twice.

Comment thread config/locales/en.yml Outdated
mfa:
multifactor_auth: Multifactor authentication
disabled: You have not yet enabled multifactor authentication.
go_settings: Go to setting page.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change this to Register a new device

ecnelises added 18 commits July 4, 2018 00:03
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.
@sonalkr132 sonalkr132 merged commit 09fd787 into rubygems:master Jul 4, 2018
@sonalkr132
Copy link
Copy Markdown
Member

Thanks @ecnelises
Alt Text

@Kholoudatef-90
Copy link
Copy Markdown

error

Please help me to solve this problem, it doesn't create a .css file.

@sonalkr132
Copy link
Copy Markdown
Member

Hi @Kholoudatef-90
Welcome to github 🎉 I am afraid this is not the right place to get answers to problems you are facing. I think you will find this useful https://yihui.name/en/2017/08/so-gh-email/

@sonalkr132 sonalkr132 deployed to production July 9, 2018 18:20 Active
ghost pushed a commit to ruby/rubygems that referenced this pull request Dec 1, 2018
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>
hsbt added a commit to ruby/rubygems-server that referenced this pull request Sep 24, 2021
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants