March 2, 2012

Migrating Monolith Rails 2.x Apps to Rails 3.x

This is a follow up on my post made on 30th October 2011, Migrating Monolith Rails 2.x Apps. I’m going to walk through the basic steps necessary to make rubycas work with existing Devise authentication data, more specific Devise v1.0.11.

To enable rubycas-server to work with your existing user data we are going to create a custom authenticator:

# encoding: UTF-8
require 'casserver/authenticators/sql'

require 'devise/encryptors/base'
require 'devise/encryptors/sha1'

class CustomAuthenticator < CASServer::Authenticators::SQL
  # snip from devise lib
  def secure_compare(a, b)
    return false unless a.present? && b.present?
    return false unless a.bytesize == b.bytesize
    l = a.unpack "C#{a.bytesize}"

    res = 0
    b.each_byte { |byte| res |= byte ^ l.shift }
    res == 0
  end

  # copied from devise.rb initializer
  DEVISE_STRETCHES = 7
  DEVISE_PEPPER = 'my-devise-pepper'

  def password_digest(password, password_salt)
    Devise::Encryptors::Sha1.digest(password, DEVISE_STRETCHES, password_salt, DEVISE_PEPPER)
  end

  def valid_for_authentication?(user, incoming_password)
    secure_compare(password_digest(incoming_password,user.password_salt), user.encrypted_password)
  end

  def validate(credentials)
    read_standard_credentials(credentials)
    raise_if_not_configured

    user_model = self.class.user_model

    username_column = @options[:username_column] || "username"

    $LOG.debug "#{self.class}: [#{user_model}] " + "Connection pool size: #{user_model.connection_pool.instance_variable_get(:@checked_out).length}/#{user_model.connection_pool.instance_variable_get(:@connections).length}"
    results = user_model.find(:all, :conditions => ["#{username_column} = ?", @username])
    user_model.connection_pool.checkin(user_model.connection)

    if results.size > 0
      $LOG.warn("Multiple matches found for user '#{@username}'") if results.size > 1
      user = results.first
      unless @options[:extra_attributes].blank?
        if results.size > 1
          $LOG.warn("#{self.class}: Unable to extract extra_attributes because multiple matches were found for #{@username.inspect}")
        else
          extract_extra(user)
              log_extra
        end
      end
      return valid_for_authentication? user, @password
    else
      return false
    end
  end
end

All I did above is to extract the valid_for_authentication? method from Devise 1.0.11 into a rubycas authentication class.

Ideally we should keep the pepper and stretches configuration in a separate file. Requiring your devise.rb initializer however does not work for Devise 1.0.11 because it assumes you are running a Ruby on Rails application which is not the case here. I’d probably put it into a Yaml file and load it on demand - the implementation for that is simple so I skipped it here.

Please note that the above example assumes you configured Devise to use the SHA1 hashing algorithm. An example snippet from your devise.rb initializer could look like this:

Devise.setup do |config|
  # snip
  config.pepper = "my-devise-pepper"
  config.stretches = 7
  config.encryptor = :sha1
  # snip
end

Now all you need to do to use your custom authenticator is to update your config.yml:

authenticator:
  class: CustomAuthenticator
  database:
    adapter: mysql
    database: rails_app_database
    username: username
    password: password
    host: localhost
  user_table: users
  username_column: email

and to restart your rubycas server. You should now be able to log-in using your existing user data.

Note

The changes necessary to support more recent versions of Devise should be similar. I have not checked yet, but might do so in the future. I might update this post with another example for Devise 2.0.

Next Up:

Redirect all log-in requests on the old Rails App to use our newly setup rubycas-server

© Raphael Randschau 2010 - 2022 | Impressum