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.
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.
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.