I’ve recently been working on a project for [client name withheld pending approval from legal dept] where we’re storing a lot of data in SimpleDB and when it came time to add an authentication layer, I naturally turned to Devise because I’ve used it before and it’s a great fit for the needs of this project. Unfortunately Devise doesn’t work with SimpleDB right out of the box. It took a little work and the creation of a new gem to get things working, but now it should be easy to get them to play nicely together in any project.

Generate and configure a new app

First let’s generate a new sample project. I’m going to use ‘-O’ to skip ActiveRecord since we’ll be using SimpleDB, and ‘-T’ to skip Test::Unit since I prefer Rspec.

$ rails new simpledevise -O -T

After the project is generated add this to the Gemfile :

gem "aws-sdk", "~> 1.8.0"
gem "devise", "~> 2.2.0"
gem "orm_adapter_aws", "~> 0.0.1"

Then install the gems by running:

$ bundle install

Then generate a devise config

$ rails generate devise:install

Then you need to edit config/initializers/devise.rb and remove the following line

require 'devise/orm/false'

The AWS adapter is already being loaded since we have it in the Gemfile.

We also need to create an initializer so we can setup the AWS library with the correct credentials. Put this into config/initializers/aws.rb

AWS_SECRET_KEY_ID='abc'
AWS_SECRET_KEY   ='123'
AWS.config({:access_key_id => AWS_SECRET_KEY_ID, :secret_access_key => AWS_SECRET_KEY})
AWS::Record.domain_prefix = "simpledevise-#{Rails.env}-"

Of course, you’ll need to tweak that with your own keys, and you may want to change the domain prefix.

Lay some ground work

Before getting straight into the authentication piece we first need to have a resource to protect. Really we just need to route the root_path to a valid controller action. So, we’ll just create a controller for that.

$ rails g controller static_pages home

Also be sure to delete public/index.html.

Now edit config/routes.rb and add this :

root :to => "static_pages#home"

At this point you should be able to start your local server and see some visible progress.

$ rails s

And then when you visit http://localhost:3000/ you should see a very basic home page.

Generate a User model and authentication routes

Finally! We can get to the part where we generate a User and use it to sign up or log in.

$ rails g devise user

If you were paying attention to the output of that last command, you may have noticed that it didn’t generate a model for us, it just added a route. That’s because we don’t have a traditional ORM hooked up to Rails. So, let’s just create a new file at app/models/user.rb.

class User < AWS::Record::Model
  # Setup the attributes for the model
  # Many of these are managed by Devise

  ## Database authenticatable
  string_attr :email
  string_attr :encrypted_password

  ## Recoverable
  string_attr   :reset_password_token
  datetime_attr :reset_password_sent_at

  ## Rememberable
  datetime_attr :remember_created_at

  ## Trackable
  integer_attr  :sign_in_count
  datetime_attr :current_sign_in_at
  datetime_attr :last_sign_in_at
  string_attr   :current_sign_in_ip
  string_attr   :last_sign_in_ip

  ## Confirmable
  # string_attr   :confirmation_token
  # datetime_attr :confirmed_at
  # datetime_attr :confirmation_sent_at
  # string_attr   :unconfirmed_email # Only if using reconfirmable

  ## Lockable
  # integer_attr  :failed_attempts, :default => 0 # Only if lock strategy is :failed_attempts
  # string_attr   :unlock_token # Only if unlock strategy is :email or :both
  # datetime_attr :locked_at

  ## Token authenticatable
  # string_attr :authentication_token

  timestamps


  # Include some validation funcitons needed by Devise
  include ActiveModel::Validations
  include ActiveModel::Validations::Callbacks

  # Some methods to fake out Devise
  def self.validates_uniqueness_of(arg1,arg2)
  end

  def save(*args)
    super()
  end

  # Now include devise model methods and 'configure' devise for this model
  extend Devise::Models
  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable,
  # :lockable, :timeoutable and :omniauthable
  # :recoverable, :rememberable,
  devise :database_authenticatable, :registerable,
         :trackable, :validatable

  
end

Whoa! That’s a lot of code. Let’s step through and look at what’s going on.

First we are extending the AWS::Record::Model class to handle the interface with SimpleDB. Then since SimpleDB doesn’t have a static schema we have to declare the attributes and their types in the model itself. I’m only using some of the attributes that you’d normally find in a Devise model, because I only need some of the features. The others are still there, just commented out, and you could use them if you need them.

Next we include some validation methods from ActiveModel and ActiveRecord, then we have a couple of helper functions. One to keep Devise from complaining that User doesn’t respond to validates_uniqueness_of and one to handle the fact that Devise tries to save the record while skipping callbacks part of the time. (Dont’ worry, in the next installment we’ll look at handing the uniqueness validation.) And finally we include and configure Devise.

At this point we need to do the equivalent of rake db:create; rake db:migrate; for SimpleDB. Fire up a rails console and run this:

> User.create_domain

Now, we’re finally ready to try to sign up for our app. Visit http://localhost:3000/users/sign_up, fill out the form, and click the ‘Sign up’ button. If everything went according to plan you should have been redirected to the temporary home page, and in a console you should get this:

> User.count
 => 1

Wrap up

You can find the code for this example on Github.

Also on Github is the orm_adapter_aws gem.

In future posts I’ll be looking at adding Rspec and doing some testing, as well as extending this example with a basic user management area, administrative privileges, general cleanup, making it look nicer, and deploying it to Heroku. Big thanks to [client name withheld pending approval from legal dept] for allowing me to build and release the orm_adapter_aws gem in the course of the project that I’m doing for them.