View on GitHub

Silence

Techinically.

RubyGems GSoC Progress report on May 19th

This is my technical journal on my progress of adding two factor authentication to RubyGems tool and its respective hosting site, along with other stuff regarding authentication or authorization of them. This is also a Google Summer of Code 2018 project.

Login sessions

This week I encountered issue #1724, saying any push operation will make other login states reset. The problem is easy to re-produce. Pushing gem is a POST request to /api/v1/rubygems, a create action, which just creates a new Pusher object, validate it, and save it if no problem raised. Reason should lies in authentication process defined in ApplicationController triggered using before_action. When I check logs when every push request received, I found it.

User authorization work in RubyGems.org is implemented through Clearance, a light-weight library compared with full-featured ones like Devise. User login state is kept by an attribute stored in users table named remember_token. Every time when a new login attempt is successful, a new token will be generated and replace previous token in database.

Of course, this is not the best practice. In an ideal user authentication system, you can check all your active logins and you can disable any of them, just like what you see in your FaceBook account… But such things can only make a quite simple site far more complicated. It provides API keys for RESTful API requests authorization.

How gem do logins

We’re quite familiar with how login process happens in browser. Now we should take a look at how login executed when doing gem push command. We can see that every gem command has a respective class in path lib/rubygems/commands/ directory. Let’s take a look at PushCommand. Using newest master branch will complain a version non-match error, I use tag v2.7.7 here, since code here are not changed often.

# lib/rubygems/commands/push_command.rb
def execute
  @host = options[:host]
  sign_in @host
  send_gem get_one_gem_name
end

def send_gem name
  # ... version checks
  gem_data = Gem::Package.new(name)
  # ... command line prompts
  response = rubygems_api_request(*args) do |request|
    request.body = Gem.read_binary name
    request.add_field "Content-Length", request.body.size
    request.add_field "Content-Type",   "application/octet-stream"
    request.add_field "Authorization",  api_key
  end
  with_response response
end

We want to let it only use API keys, but here it are surely using API keys for request, not using basic authentication by username and password. Now we check function sign_in for detail.

# lib/rubygems/gemcutter_utilities.rb
def sign_in sign_in_host = nil
  sign_in_host ||= self.host
  return if api_key
  pretty_host = if Gem::DEFAULT_HOST == sign_in_host then
                  'RubyGems.org'
                else
                  sign_in_host
                end
  say "Enter your #{pretty_host} credentials."
  say "Don't have an account yet? " +
      "Create one at #{sign_in_host}/sign_up"
  email    =              ask "   Email: "
  password = ask_for_password "Password: "
  say "\n"
  response = rubygems_api_request(:get, "api/v1/api_key",
                                  sign_in_host) do |request|
    request.basic_auth email, password
  end
  with_response response do |resp|
    say "Signed in."
    set_api_key host, resp.body
  end
end

How session refreshed

Now we’ve figured out that gem use sign_in, sending username and password to server to get API key. If the key already is there, just return it. Turn eyes into what the server does.

# config/initializers/clearance.rb
class Clearance::Session
  def current_user
    return nil if remember_token.blank?
    return @current_user if @current_user
    user = user_from_remember_token(remember_token)
    @current_user = user if user&.remember_me?
  end

  def sign_in(user)
    @current_user = user
    cookies[remember_token_cookie] = user && user.remember_me!
    status = run_sign_in_stack
    unless status.success?
      @current_user = nil
      cookies[remember_token_cookie] = nil
    end
    yield(status) if block_given?
  end
end

The action remember_me! makes app updates user’s remember token. For testing purpose, comment it out and things go as we want - I will not lose my browser session after a command line push. But it cannot be really used because browser logins will also use this method. One solution I think of is override current_user method in Api::BaseController and not use sign_in in API response actions.

OTP and QR-code

My main work on GSoC is adding two factor authentications. From perspective of databases, what we need to add includes: