Skip to content

Two-factor authentication feature in API part.#1753

Merged
sonalkr132 merged 16 commits into
rubygems:masterfrom
ecnelises:mfa-api
Nov 3, 2018
Merged

Two-factor authentication feature in API part.#1753
sonalkr132 merged 16 commits into
rubygems:masterfrom
ecnelises:mfa-api

Conversation

@ecnelises
Copy link
Copy Markdown
Member

@ecnelises ecnelises commented Jul 9, 2018

This PR is for the multifactor auth of the API part. The changes are added into v1 version of rubygems.org API. Current design is:

  • A new interface will be added into ProfilesController (or maybe other), getting the mfa setting status of current user (needs authentication).
  • If the mfa on write option (mfa_login_and_write) is enabled, those actions needing mfa should check OTP field in request header, and verify it.
  • If verification on OTP fails, a 401 forbidden response with prompt text are returned. Otherwise, everything goes normally.
  • Actions needing mfa are:
    • Pushing gems
    • Change ownership of gems.
  • Maybe a new interface will be added for changing mfa status through RESTful API.

I'm not sure whether the approach is compatible for the rubygems versions (not supporting mfa). I will check how rubygems client handles the operation errors.

Below are tasks.

  • Add check for OTP field in header.
  • Check for rubygems push action.
  • Check for ownership change action.
  • Check for creating deletions (yank command).
  • MFA level change. (In UI part)
  • Check for API Key acquirement.
  • Integration testing.
  • (Maybe) Support for mfa state change (enabling, disabling, etc.) through API.

def verify_with_otp
return unless @api_user.mfa_login_and_write?
otp = request.headers["HTTP_OTP"] || ''
return if @api_user&.otp_verified?(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.

is this & needed here? verify_authenticated_user ensures that user see 401 if @api_user is nil.

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. At first, I'm considering a case that verify_with_otp is put before verify_authenticated_user. But since the method relies on existence of @api_user, it must be put after verify_authenticated_user.

@sonalkr132
Copy link
Copy Markdown
Member

This looks good so far 👍 Can we please make some headway with

MFA level change. (In UI part)

@ecnelises
Copy link
Copy Markdown
Member Author

Something like this seems reasonable. @sonalkr132
change-mfa-level-plan

@sonalkr132
Copy link
Copy Markdown
Member

Something like this seems reasonable

Its fine but can be better.
Check these out:
https://codepen.io/cssinate/pen/KVdYjz
https://codepen.io/tr13ze/pen/yEdxvN
https://codepen.io/tag/dropdown/

Public pens are MIT licensed, if you are going to copy any of them, please make sure it matches our color scheme.

@ecnelises
Copy link
Copy Markdown
Member Author

@sonalkr132
Thanks, I'll check them out. I saw styles about select tags in our css file, but there's no select in the templates before.

Comment thread app/models/user.rb Outdated

def mfa_write_authorized?(otp)
return true unless mfa_login_and_write?
otp_verified?(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.

please don't do otp || '', either add default argument to mfa_write_authorized? or otp_verified?.

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 think I can replace otp || '' with otp.to_s

Comment thread app/views/profiles/edit.html.erb Outdated
<%= select_tag :level, options_for_select([
['Login and Write', 'mfa_login_and_write'],
['Login Only', 'mfa_login_only'],
['Disabled', 'no_mfa']], @user.mfa_level), :class => 'form__input form__select' %>
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.

Login only and Login and write don't make much sense to me. At least to user, we should show something more appropriate like UI Authentication and UI and API 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.

We should also consider updating function names as well. I am not a stickler for naming it UI and API Authentication but ..login_and_write is just misleading. For example, api request to show key is not a write operation and by login_only we don't mean api login. Distinction has more to do with whether we are checking something for api or ui.
Sorry, I failed to point it out previously.

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.

The name login and write or login only derives from the original RFC issue about multi-factor authentication. NPM uses these names to identify their different authentication level. These are basic elements in mfa implementation, so I just borrowed them at that time without too much hesitation.

Currently, fetching API key requires OTP only when auth level set to login and write. Truly, these would be confusing to both users and contributors who see this code. Some of other actions not wrapped with mfa now, like API key reset or reset password, but they are also critical. Should they require OTP if UI Authentication enabled? Except that, I think name around UI and API is good. Thanks.

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 did it. Auth for UI Only and Auth for UI and API is okay.

Comment thread test/integration/profile_test.rb Outdated
page.fill_in "otp", with: recoveries.sample
click_button "Disable"
page.select "Disabled"
find('#mfa-edit input[type=submit]').click
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 make a function out of above two line.

should redirect_to('the profile edit page') { edit_profile_path }
should 'disable mfa' do
refute @user.reload.mfa_enabled?
context 'when input recovery code' 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 otp is recovery code

Comment thread config/locales/en.yml Outdated
none: None
not_found: Not Found
please_sign_up: Access Denied. Please sign up for an account at https://rubygems.org
please_send_correct_otp: You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry.
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.

your request doesn't have the correct OTP code.

Won't we be showing this when OPT is missing too? if yes, please update it to say you didn't provide otp or provided otp was incorrect.

@sonalkr132
Copy link
Copy Markdown
Member

Some of other actions not wrapped with mfa now, like API key reset or reset password, but they are also critical.

In my opinion, asking mfa once during login through UI is good enough. I wish we could do the same for api as well, as pointed out by @colby-swandale

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

We are asking for otp for every critical cli components because as of now, there is no concept of session when it comes to our api. Unless we implement a way of api authentication where tokens expires after a given time, we can't really do the same for api requests.
aws cli seem to be using related workflow https://aws.amazon.com/premiumsupport/knowledge-center/authenticate-mfa-cli/

@ecnelises
Copy link
Copy Markdown
Member Author

ecnelises commented Aug 12, 2018 via email

@hsbt
Copy link
Copy Markdown
Member

hsbt commented Sep 12, 2018

ref #1725

@@ -0,0 +1,12 @@
class Api::V1::MultifactorAuthsController < Api::BaseController
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 we are using this in client any more.

Comment thread config/locales/en.yml Outdated
none: None
not_found: Not Found
please_sign_up: Access Denied. Please sign up for an account at https://rubygems.org
otp_incorrect_or_missing: You have enabled multifactor authentication but your OTP code is missing or incorrect. Please check it and retry.
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.

your OTP code is missing or incorrect.

this is unnecessarily ambiguous. Can we please add a check for otp being empty and show error message accordingly?

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. But change to this may affect rubygems client, I'll check more of it.

Comment thread config/locales/en.yml Outdated
destroy:
success: You have successfully disabled multifactor authentication.
update:
success: You have successfully updated your auth level.
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.

aren't we updating mfa level and not auth level?

Comment thread config/locales/en.yml Outdated
go_settings: Register a new device
enabled: You have enabled multifactor authentication. Please input your OTP from your authenticator or one of your active recovery codes to disable it.
disable: Disable
enabled: You have enabled multifactor authentication. Please input your OTP from your authenticator or one of your active recovery codes to change your auth level or disable it.
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.

mfa level

@user.enable_mfa!(ROTP::Base32.random_base32, :ui_and_api_mfa)
end

context "on POST to add other user as gem owner with email without OTP" 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.

please drop with email in all these context.

@sonalkr132
Copy link
Copy Markdown
Member

Rebase 💥 please.

@sonalkr132
Copy link
Copy Markdown
Member

One minor change. Looks good to merge now 👍
Have you tried this locally with ruby/rubygems#2369?

@ecnelises
Copy link
Copy Markdown
Member Author

One minor change. Looks good to merge now 👍
Have you tried this locally with rubygems/rubygems#2369?

Local output is like this:
https://paste.ofcode.org/h473UpYsJGytsarQnxjbWC

One thing I need to do: I have to comment the code checking current RubyGems client version. Or I would get

You are using a beta release of RubyGems (3.0.0.beta1) which is not
allowed to push gems. Please downgrade or upgrade to a release version.

@sonalkr132
Copy link
Copy Markdown
Member

This command needs digit code for multifactor authentication.
Code: 757891
Signed in.
Pushing gem to http://localhost:3000...
This command needs digit code for multifactor authentication.
Code: 757891
Successfully registered gem: hello (0.0.9)

If we don't delete otp here, it would be nicer not to ask for otp again.

@ecnelises
Copy link
Copy Markdown
Member Author

If we don't delete otp here, it would be nicer not to ask for otp again.

I remember we talked about it and I checked the chat history. It seems that I misunderstood your meanings. Currently, I need to do multi-factor authentication even seconds after signing in with correct OTP code. Security problem arises when such no-otp interval exists. But now we don't have it. Every execution is independent.

Only concern: If fetching newest version of gem is too slow, the OTP may expire. (Since we have tolerance for OTP in last interval, this should not be a big problem)

Thank you for notifying me about this. I'll delete first it until we found new approaches for authentication.

@sonalkr132
Copy link
Copy Markdown
Member

giphy-downsized-large

Thanks @ecnelises 🍷 ✨

@sonalkr132 sonalkr132 merged commit b2113ff into rubygems:master Nov 3, 2018
@rubygems-deployer rubygems-deployer temporarily deployed to staging November 3, 2018 14:40 Inactive
@dwradcliffe dwradcliffe temporarily deployed to production November 3, 2018 18:02 Inactive
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>
@sonalkr132 sonalkr132 mentioned this pull request Aug 3, 2019
2 tasks
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.

5 participants