Skip to content

[GSoC] Multi-factor feature for RubyGems.#2369

Merged
21 commits merged into
ruby:masterfrom
ecnelises:multifactor-auth
Dec 1, 2018
Merged

[GSoC] Multi-factor feature for RubyGems.#2369
21 commits merged into
ruby:masterfrom
ecnelises:multifactor-auth

Conversation

@ecnelises
Copy link
Copy Markdown
Contributor

@ecnelises ecnelises commented Jul 27, 2018

Description:

Hello. This is my GSoC project, dedicated to add multifactor authentication to both RubyGems command program and Gemcutter (RubyGems.org).

Work for the Gemcutter part has almost been finished (see PR #1753, PR #1729 and a series of my progress reports).

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:

  • Add OTP requirement to push_command.
  • Add OTP requirement to owner_command.
  • Add OTP prompt to sign_in.
  • Support for yank_command.
  • Write related tests.

I will abide by the code of conduct.

Comment thread lib/rubygems/gemcutter_utilities.rb Outdated
request.add_field 'Authorization', api_key
end

# For compatibility to Gemcutters without mfa support
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.

Gemcutters is obsoleted name. Please use rubygems.org

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks. Here I originally wanted to include some private servers. So I'll change it to servers.

@colby-swandale
Copy link
Copy Markdown
Member

Using both otp and mfa does not seem clean, the code would be clearer if you stick with just the one name.

Comment thread lib/rubygems/gemcutter_utilities.rb Outdated
##
# Require user for extra OTP code if multifactor authentication is enabled.

def run_mfa_check
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.

run doesn't really mean anything. Maybe just mfa_check?

@colby-swandale
Copy link
Copy Markdown
Member

Can you please explain how the user flows works with 2FA. Looking at the code, does the user need to sign in with 2FA first or an error will be given?

Can we just ask the user to give their MFA token whenever they sign in to an account which has 2FA?

@ecnelises
Copy link
Copy Markdown
Contributor Author

@colby-swandale
Thanks. mfa is better known than otp, mixing them would confuse people not familiar with how mfa/2fa works.

Here we have a concept about level of mfa, inspired by npm. A user can set his level to login_only or login_and_write. login_only means when a user does login, a page requiring extra digit code will be shown. login_and_write also requires the digit code for some important operations besides login. Since I have implemented digit check for website login, add a check for MFA code to client seems more reasonable than not.

@ecnelises
Copy link
Copy Markdown
Contributor Author

A lot of gem commands need to be sign in. But if we require user input code first, operations next may also require digit code. That will make operation fail. Or retieving API key does not need multifactor auth as long as we require digit code for all important operations.

Comment thread lib/rubygems/gemcutter_utilities.rb Outdated
end
end

@mfa_level == 'mfa_login_and_write'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If there isn't a many different values for @mfa_level, shouldn't symbols be
favored over strings here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since text received is string and we turned # frozen_string_literal: true on, will that help (in performance) ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No idea, I didn't even thought about performance when I commented to be honest.
It was more about "semantics", that it seemed used as an "identifier" more than
a string.
But on the other hand, as you said this data is received as string already, if
the spec pass in theory we shouldn't need more code :-)
Anyway it was a small detail I just asked about in case it would help, feel
free to ignore.

Comment thread lib/rubygems/gemcutter_utilities.rb Outdated
end

# For compatibility to Gemcutters without mfa support
# For compatibility to servers without mfa support
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think this change belongs to another commit (the one adding this comment).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, thanks.

Comment thread lib/rubygems/commands/push_command.rb Outdated
options[:host] = value
end

add_option('--mfa CODE', 'Digit code for multifactor authentication') do |value, options|
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can this repeated code be factored into a module?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. Thanks.

Comment thread lib/rubygems/gemcutter_utilities.rb Outdated
def need_mfa?
return false if options[:suppress_mfa]
unless instance_variable_defined? :@mfa_level
response = rubygems_api_request(:get, 'api/v1/multifactor_auth') do |request|
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Won't this be adding additional api call to all requests? For default case, we should assume that mfa on api is disabled. mfa is an opt-in feature.
From server, we can return 403 or something more appropriate for cases where otp was required but not provided. Only then, client will prompt for otp and repeat the request.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here options[:supress_mfa] is just an internal option for keep compatibility with older test cases, since many of them relies on last_request. And fail message the client received is plain/text, should I just match it literally?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

for keep compatibility with older test cases, since many of them relies on last_request

Can you please explain more about this limitation? Are you sure we can't do away with options[:suppress_mfa]?

should I just match it literally?

Yeah, checking for response text would be good enough unless we come up with something more elegant.

Copy link
Copy Markdown
Contributor Author

@ecnelises ecnelises Aug 3, 2018

Choose a reason for hiding this comment

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

If we want to remove options[:suppress_mfa], just @cmd.instance_variable_set :@mfa_level, 'no_mfa' works. This method should be not so intrusive. Just now I posted a much more complicated method, like here. Sorry for my carelessness.

The reason why we need to suppress mfa in original tests is they require last_request in Gem::FakeFetcher to determine method, content, headers of last request. One more fake request for mfa status will replace it.

@ecnelises ecnelises force-pushed the multifactor-auth branch 4 times, most recently from d92565d to b02ae88 Compare August 10, 2018 12:38
@ecnelises ecnelises closed this Aug 10, 2018
@ecnelises ecnelises reopened this Aug 10, 2018
@ecnelises ecnelises changed the title [GSoC][WIP] Multi-factor feature for RubyGems. [GSoC] Multi-factor feature for RubyGems. Aug 10, 2018
Comment thread lib/rubygems/commands/owner_command.rb Outdated
request.set_form_data 'email' => owner
request.add_field "Authorization", api_key
request.add_field "OTP", options[:mfa]
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please don't duplicate code. Make a method out of common code where otp is optional argument.
Also, I agree with @colby-swandale

the code would be clearer if you stick with just the one name.

"OTP", options[:mfa] 🚫
Please use otp for everything.

Comment thread lib/rubygems/gemcutter_utilities.rb Outdated
end

if need_mfa? response
check_mfa
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

check_mfa and need_mfa? always seem to go together. Can we please call check_mfa from need_mfa? instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. Thanks, I changed it.

Comment thread lib/rubygems/gemcutter_utilities.rb Outdated
def need_otp? response
return unless response.kind_of?(Net::HTTPUnauthorized) &&
response.body.start_with?('You have enabled multifactor authentication')
unless options[:otp]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

return true if options[:otp]

@ecnelises
Copy link
Copy Markdown
Contributor Author

To push progress of this feature, I listed some implementation problems about current workflow. Can you please take a look and leave comments? Thanks!
See rubygems/rubygems.org#1725

@ecnelises
Copy link
Copy Markdown
Contributor Author

ecnelises commented Sep 23, 2018

😄 Is there any problem for current implementation status? I wrote in detail about what it will look like and how it's implemented in GSoC final report. Sorry for out of work these days, maybe I should add 2FA to yank now.

@hsbt
Copy link
Copy Markdown
Member

hsbt commented Oct 9, 2018

@indirect @sonalkr132 Can you handle this issue with rubygems.org side. I'm okay to merge this pull-request targeted RubyGems 3.0.

@hsbt
Copy link
Copy Markdown
Member

hsbt commented Nov 5, 2018

@ecnelises Do you still waiting to review this?

@hsbt
Copy link
Copy Markdown
Member

hsbt commented Dec 1, 2018

@bundlerbot r+

ghost pushed a commit 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>
@ghost
Copy link
Copy Markdown

ghost commented Dec 1, 2018

Build succeeded

@ghost ghost merged commit ee0cbc9 into ruby:master Dec 1, 2018
matzbot pushed a commit to ruby/ruby that referenced this pull request Dec 1, 2018
  * [GSoC] Multi-factor feature for RubyGems ruby/rubygems#2369

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@66118 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
@hsbt
Copy link
Copy Markdown
Member

hsbt commented Dec 1, 2018

@ecnelises @sonalkr132 Thanks for your work. I'm going to release this feature with RG 3.0 at 25, Dec 2018.

@sonalkr132
Copy link
Copy Markdown
Contributor

  • Support for yank_command.

Not really sure why @ecnelises decided to keep it separate but PR for it is now merged on rubygems.org, I guess now he can add it here.

@ecnelises
Copy link
Copy Markdown
Contributor Author

ecnelises commented Dec 1, 2018 via email

@sonalkr132
Copy link
Copy Markdown
Contributor

yeah, its cool. yank is not as often used (although very important for mfa) command.
If @hsbt really wants to see make it in one go, I hope he will be kind enough to merge it again 🙏

bannable pushed a commit to bannable/ruby that referenced this pull request Dec 10, 2018
  * [GSoC] Multi-factor feature for RubyGems ruby/rubygems#2369

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@66118 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants