RESTful API authentication

In a couple of our past posts we saw how to build a private API (Part 1 - Part 2 ) that could be used as the backend for an iOS app. Let's take it a step further.

Today I will show you how to use that backend to actually let users log in and out (on the mobile device) from the system by providing a RESTful API for the authentication process. We'll create a basic app from scratch step by step. Bear with me.

First of all, let's fire the terminal and create a new rails app: rails new AuthTest. Then cd into the newly created directory. Open your Gemfile and add the following line to the end of the file as follows:

gem 'devise'  

devise is an awesome gem to deal with authentication. You can read more about it on the official Github page or watch the very good screencasts from Ryan Bates on Railscasts.

You can now run the following command in the terminal:

bundle install  
rails g devise:install  
rails g devise User  
rake db:migrate  

We have now everything we need to manage users. The rails g devise User has scaffolded a devise model called User. Let's open it (/app/models/user.rb) and modify it by adding :token_authenticatable in the devise section of the file. This will let the user authenticate thru a token that our backend will generate and that mobile client will use when querying the web application.

Now let's make some changes in the config (/config/initializers/devise.rb):

config.skip_session_storage = [:http_auth, :token_auth]  
config.token_authentication_key = :auth_token  

To understand what we just did, you can read more in the config file just above the methods you've just changed. Devise is very well documented.

We also need to generate a migration to add an authentication_token field to the Users table where the token will be stored as a string. Run rails g migration AddTokenToUsers authentication_token. Your newly generated migration file should look like this:

class AddTokenToUsers < ActiveRecord::Migration  
  def change
    add_column :users, :authentication_token, :string
  end
end  

Now let's scaffold a new resource, the one only authenticated users will be able to access. Run rails g scaffold Product name:string description:text.

Let's open products_controller.rb and modify it as follows:

class ProductsController < ApplicationController  
  before_filter :authenticate_user!
#    ...

Migrate (rake db:migrate) your database again.

Let's also delete public/index.html and modify our routes.rb file so that our root page is going to be the products controller index action:

AuthTest::Application.routes.draw do  
  resources :products
  root :to => 'products#index'
  devise_for :users
end  

If you now run your server (rails s) you'll see that your app is going to ask you to login.

http://cocoahunter-blog.s3.amazonaws.com/devise-auth-api/api-auth1.png

Obviously we currently haven't generated any users, so let's go ahead and create one. Click on sign up and complete the process. You may also add a couple of Products just to make sure everything works alright.

http://cocoahunter-blog.s3.amazonaws.com/devise-auth-api/api-auth2.png

If you've managed to make everything run up to this point, we can now move to the juicy part. To authenticate the user we'll be subclassing Devise::SessionsController with our own controller that basically runs a check on a given username and password and, if valid, returns a token. In a way, users are signed in to the backend as long as they have a valid authentication token. It's up to you the decision relative to the expiration policies of the token.

Create a new file in the /app/controllers directory and name it sessions_controller.rb. Paste in it the following code:

class SessionsController < Devise::SessionsController  
  before_filter :authenticate_user!, :except => [:create, :destroy]
  respond_to :json

  def create
    resource = User.find_for_database_authentication(:email => params[:email])
    return invalid_login_attempt unless resource

    if resource.valid_password?(params[:password])
      sign_in(:user, resource)
      resource.ensure_authentication_token!
      render :json=> {:success=>true, :auth_token=>resource.authentication_token, :email=>resource.email}
      return
    end
    invalid_login_attempt
  end

  def destroy
    resource = User.find_for_database_authentication(:email => params[:email])
    resource.authentication_token = nil
    resource.save
    render :json=> {:success=>true}
  end

  protected

  def invalid_login_attempt
    render :json=> {:success=>false, :message=>"Error with your login or password"}, :status=>401
  end
end  

Now you have to tell your routes.rb file that Devise should be using our custom controller when dealing with user sessions instead of the standard one. Modify you Routes file accordingly:

# ...
devise_for(:users, :controllers => { :sessions => "sessions" })  
# ...

If you've followed every step along the way, everything should now be in place. Fire up your server (rails s) and open another terminal window. Now you should be able to curl into youw web app and get back - if credentials are correct - the token you'll use for your subsequent queries. Try running the following (with the email and password params you used at sign up):

curl http://localhost:3000/users/sign_in --data "email=admin@example.com&password=password"  

You should get a response similar to:

{"success":true,"auth_token":"hfNklifqaFBkvokWoYzC","email":"admin@example.com"}

You can now run a query form the terminal with the following command curl http://localhost:3000/products.json --data "auth_token=hfNklifqaFBkvokWoYzC" and you should get back the JSON from your backend.

We're done! Just use this very same technique from your mobile client and store your token to use in your queries.

You can download the full source code of the above app from this Github repo.