Use Rails as JSON-API Backend, part #2

part #2: Authentication

In part #1 of this series we have built a CRUD-JSON controller. In this part we will introduce an authentication-layer, so that just users with a valid token have access. As always: in the real world you WANT! to use SSL if sensitive information go over the wire.

James Harton has made an interesting post how to implement API authentication on your own, but here we'll stick with the well known gem Devise. Token authentication has been removed from Devise. In this gist José Valim explains how the functionality can be added. Gonzalo Bulnes made a gem out of that: simple_token_authentication. Add these 2 gems to our Gemfile:

# Gemfile
...
gem 'devise'  
gem 'simple_token_authentication'  

Please follow the getting started guide to setup Devise.


We go with the minimal amount of modules of Devise, so the User model looks like:

# app/models/user.rb

class User < ActiveRecord::Base  
  devise :database_authenticatable, :registerable, :validatable
end  

and the corresponding migration:

class AddDeviseToUsers < ActiveRecord::Migration  
  def self.up
    change_table(:users) do |t|
      ## Database authenticatable
      t.string :email,              null: false
      t.string :encrypted_password, null: false
    end

    add_index :users, :email, unique: true
  end

  def self.down
    ...
  end
end  

Fine. Now we setup the simple token authentication. The migration:

class AddSimpleTokenAuthenticationToUsers < ActiveRecord::Migration  
  def change
    add_column :users, :authentication_token, :string, null: false
    add_index  :users, :authentication_token, unique: true
  end
end  

Add it to the User model:

# app/models/user.rb

class User < ActiveRecord::Base  
  ...
  acts_as_token_authenticatable
end  

In order to protect our whole API from access without a valid token we extend the application controller. Simple token authentication tries to sign in the user (via X-User-Email and X-User-Token). Because this is the only way we want to allow authentication we disable the fallback to Devise.
With the authenticate_user! before filter we make sure that an error is returned if there is no signed in user.

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base  
  ...
  acts_as_token_authentication_handler_for User, fallback_to_devise: false
  before_filter :authenticate_user!
end  

What if we want to have some actions unprotected? We skip the before filter for them.
Let's unprotect the index, show and create actions of the users controller:

# app/controllers/api/v1/users_controller.rb

class Api::V1::UsersController < ApplicationController  
  skip_before_filter :authenticate_user!, only: [:index, :show, :create]
  ...
  def user_params
    params.require(:user).permit(
      :email,
      :password,
      :name,
      :age)
  end
end  

Let's try it!
First, clean your database.
Fire up your Rails server. Let's play in the bash:

# create a user
curl  -H "Accept: application/json" -H "Content-type: application/json" http://127.0.0.1:3000/api/v1/users -X POST -d '{"user":{"name":"Dirk", "age": 33, "email": "foo@bar.com", "password": "password"}}'  
--> Completed 201, {"user":{"id":1,"name":"Dirk","age":33}}

# retrieve the users collection
curl  -H "Accept: application/json" -H "Content-type: application/json" http://127.0.0.1:3000/api/v1/users  
--> Completed 200, {"users":[{"id":1,"name":"Dirk","age":33}]

# update the user
curl  -H "Accept: application/json" -H "Content-type: application/json" http://127.0.0.1:3000/api/v1/users/1 -X PATCH -d '{"user":{"age": 34}}'  
--> Completed 401 Unauthorized, {"error":"You need to sign in or sign up before continuing."}

Perfect, exactly what we wanted. The update action requires a signed in user.
Fire up a rails console and get the authentication token of the user (it is created by the gem automatically on save). In the real world we would have an own API endpoint for zhis step.

User.last.authentication_token  
=> "jc6gifTyzY1TBz_G-sam"

Let's pass the right headers:

# update the user (right syntax with Token and Email headers)
curl  -H "Accept: application/json" -H "Content-type: application/json" -H "X-User-Token: jc6gifTyzY1TBz_G-sam" -H "X-User-Email: foo@bar.com" http://127.0.0.1:3000/api/v1/users/1 -X PATCH -d '{"user":{"age": 34}}'  
--> Completed 204

# read the user
curl  -H "Accept: application/json" -H "Content-type: application/json" http://127.0.0.1:3000/api/v1/users/1  
--> Completed 200, {"user":{"id":1,"name":"Dirk","age":34}}

# delete the user
curl  -H "Accept: application/json" -H "Content-type: application/json" http://127.0.0.1:3000/api/v1/users/1 -X DELETE  
--> Completed 401 Unauthorized, {"error":"You need to sign in or sign up before continuing."}
(perfect, exactly what we wanted. this method needs a signed in user)

# delete the user (right syntax with Token and Email headers)
curl  -H "Accept: application/json" -H "Content-type: application/json" -H "X-User-Token: jc6gifTyzY1TBz_G-sam" -H "X-User-Email: foo@bar.com" http://127.0.0.1:3000/api/v1/users/1 -X DELETE  
--> Completed 204

At the moment you could update and delete foreign users if you are signed in. In part #3 we will have a look at authorization and how to restrict the possibilities of a user in the system.