Rails Custom Authentication using Devise, DataMapper, and a Legacy Database

Rails Custom Authentication using Devise, DataMapper, and a Legacy Database

ActiveRecord is great if your database schema evolves along with your web app from birth, but not all of us have this luxury. Many of us live in the world of corporate IT – a world of legacy databases and bureaucracies that make getting a Rails app into production hard enough, let alone getting a new schema into production. DataMapper is a common alternative ORM to use for these scenarios. DataMapper is ideally suited for legacy databases, as Martin Gamsjaeger describes:

  • DataMapper allows you to map meaningful model and property names to cryptic legacy table and column naming conventions. It allows you to do so either on a per model/property, or an app wide basis.

  • DataMapper supports lazy properties that will only be fetched when actually accessed.

  • DataMapper has seamless support for composite primary keys.

  • DataMapper only cares about the properties (columns) you explicitly declare in your models. Other columns will never be touched or read.

  • DataMapper works nicely with foreign key constraints in your database and with the help of dm-constraints it also supports creating them.

There’s some relevant documentation on http://datamapper.org/docs/legacy too.

I’m not going into depths with DM in this article; there are plenty of tutorials out there. What I am going to demonstrate is how to live with a legacy database written for an app with a horribly insecure authentication mechanism, based on a schema whose table and column names don’t match their Rails Model counterparts.

Here’s our User model:

class User
  include DataMapper::Resource
  include DataMapper::MassAssignmentSecurity

  devise :database_authenticatable, :authentication_keys => [:username]
  storage_names[:default] = 'legacy_User_table'

  property :id,                 Serial,  :field => 'UserId',           :required => true
  property :username,           String,  :field => 'LoginId',          :required => true
  property :encrypted_password, String,  :field => 'PasswordSHA1Hash', :required => true
  property :enabled,            Integer, :field => 'Enabled',          :required => true
  property :is_admin,           Integer, :field => 'IsSuperAdmin',     :required => true
  property :first_name,         String,  :field => 'Name',             :required => true
  property :last_name,          String,  :field => 'Surname',          :required => true

  attr_accessible :username, :password, :password_confirmation

  def password_salt=(password_salt)
  end

  def password_salt
  end

  def password_digest(password)
    self.class.encryptor_class.digest(password)
  end

end

You’ll notice this model overrides :authentication_keys, using :username instead of :email. I also map the table name to ‘legacy_Users_table’ since we don’t have a conveniently named ‘users’ table in our schema. Our password in this monstrosity is stored as an unsalted SHA1 hash, which then gets Base64-encoded. Really secure, huh?

For Devise to work with unsalted passwords, I’ve had to override the password_salt functions and the password digest function that Devise looks for. Here, this lives in a custom Devise encryptor class, which I define in an initializer called devise_encryptor.rb:

module Devise
  module Encryptors
    class Sha1base64 < Base

      def self.digest(password)
        sha1 = Digest::SHA1.digest(password)
        Base64.strict_encode64(sha1)
      end

      def self.salt(username)
        nil
      end

    end
  end
end

This encryptor takes the password, short-circuits the salt function, and returns the Base64-encoded SHA1 hash. There are a couple of configuration changes needed to wire this up. In /initializers/devise.rb, set:

config.encryptor = :sha1base64

This will reference the above custom encryptor class name.

There is one last workaround we have to apply to get the password_salt override in our user model to work. From the Devise source code: we have to tell Devise not to apply the schema in ORMs where the Devise declaration and schema belongs to the same class (as Datamapper and Mongoid). This goes inside devise.rb, in the Devise.config block, and is courtesy of Jared Morgan in the DataMapper mailing list:

config.apply_schema = false

The last change you’ll make is in the devise sign_in view, which you likely generated using rails g devise:views. Use the :username instead of :email for your login credentials:

<%= f.label :username %> <%= f.text_field :username %>

If all goes well, you’ll now be able to log into your shiny new Rails app, backended by a steaming pile of crap designed and maintained by monkeys.

Comments

AWS migration and transformation programs reduce cost, risk, and complexity for rapid business innovation and agility at scale. I offer a number of AWS consulting services, including an AWS migration service to quickly get you up and running. Please contact me for details on how I can help your organization.