ruby-on-railsapiruby-on-rails-4deviseruby-grape

User Authentication with Grape and Devise


I have difficulties to understand and also properly implement User Authentication in APIs. In other words, I have serious problem to understand the integration of Grape API with front-end frameworks such as Backbone.js, AngularJS or Ember.js.

I'm trying to pivot all different approaches and read a lot about that, but Google returns me truly bad resources and it seems to me, like there is no really good article on this topic - Rails and User authentication with Devise and front-end frameworks.

I will describe my current pivot and I hope you can provide me some feedback on my implementation and maybe point me to the right direction.

Current implementation

I have backend Rails REST API with following Gemfile(I will purposely shorten all file code)

gem 'rails', '4.1.6'
gem 'mongoid', '~> 4.0.0'
gem 'devise'
gem 'grape'
gem 'rack-cors', :require => 'rack/cors'

My current implementation has only APIs with following Routes(routes.rb):

api_base      /api        API::Base
     GET        /:version/posts(.:format)
     GET        /:version/posts/:id(.:format)
     POST       /:version/posts(.:format)
     DELETE     /:version/posts/:id(.:format)
     POST       /:version/users/authenticate(.:format)
     POST       /:version/users/register(.:format)
     DELETE     /:version/users/logout(.:format)

I created have following model user.rb

class User
  include Mongoid::Document
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  field :email,              type: String, default: ""
  field :encrypted_password, type: String, default: ""

  field :authentication_token,  type: String

  before_save :ensure_authentication_token!

  def ensure_authentication_token!
    self.authentication_token ||= generate_authentication_token
  end

  private

  def generate_authentication_token
    loop do
      token = Devise.friendly_token
      break token unless User.where(authentication_token: token).first
    end
  end   
end

In my controllers I created following folder structure: controllers->api->v1 and I have created following shared module Authentication (authentication.rb)

module API
  module V1
    module Authentication
      extend ActiveSupport::Concern

      included do
        before do
           error!("401 Unauthorized", 401) unless authenticated?
         end

         helpers do
           def warden
             env['warden']
           end

           def authenticated?
             return true if warden.authenticated?
             params[:access_token] && @user = User.find_by(authentication_token: params[:access_token])
           end

           def current_user
             warden.user || @user
           end
         end
       end
     end
   end
end

So every time when I want to ensure, that my resource will be called with Authentication Token, I can simply add this by calling: include API::V1::Authentication to the Grape resource:

module API
  module V1
    class Posts < Grape::API
      include API::V1::Defaults
      include API::V1::Authentication

Now I have another Grape resource called Users(users.rb) and here I implement methods for authentication, registration and logout.(I think that I mix here apples with pears, and I should extract the login/logout process to another Grape resource - Session).

module API
  module V1
    class Users < Grape::API
      include API::V1::Defaults

      resources :users do
        desc "Authenticate user and return user object, access token"
        params do
           requires :email, :type => String, :desc => "User email"
           requires :password, :type => String, :desc => "User password"
         end
         post 'authenticate' do
           email = params[:email]
           password = params[:password]

           if email.nil? or password.nil?
             error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
             return
           end

           user = User.find_by(email: email.downcase)
           if user.nil?
              error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
              return
           end

           if !user.valid_password?(password)
              error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
              return
           else
             user.ensure_authentication_token!
             user.save
             status(201){status: 'ok', token: user.authentication_token }
           end
         end

         desc "Register user and return user object, access token"
         params do
            requires :first_name, :type => String, :desc => "First Name"
            requires :last_name, :type => String, :desc => "Last Name"
            requires :email, :type => String, :desc => "Email"
            requires :password, :type => String, :desc => "Password"
          end
          post 'register' do
            user = User.new(
              first_name: params[:first_name],
              last_name:  params[:last_name],
              password:   params[:password],
              email:      params[:email]
            )

            if user.valid?
              user.save
              return user
            else
              error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
            end
          end

          desc "Logout user and return user object, access token"
           params do
              requires :token, :type => String, :desc => "Authenticaiton Token"
            end
            delete 'logout' do

              user = User.find_by(authentication_token: params[:token])

              if !user.nil?
                user.remove_authentication_token!
                status(200)
                {
                  status: 'ok',
                  token: user.authentication_token
                }
              else
                error!({:error_code => 404, :error_message => "Invalid token."}, 401)
              end
            end
      end
    end
  end
end

I realize that I present here a ton of code and it might not make sense, but this is what I currently have and I'm able to use the authentication_token for calls against my API which are protected by module Authentication.

I feel like this solution is not good, but I really looking for easier way how to achieve user authentication through APIs. I have several questions which I listed below.

Questions

  1. Do you think this kind of implementation is dangerous, if so, why? - I think that it is, because of the usage of one token. Is there a way how to improve this pattern? I've also seen implementation with separate model Token which has expiration time, etc. But I think this is almost like reinventing wheel, because for this purpose I can implement OAuth2. I would like to have lighter solution.
  2. It is good practice to create new module for Authentication and include it only into resources where it is needed?
  3. Do you know about any good tutorial on this topic - implementing Rails + Devise + Grape? Additionally, do you know about any good open-source Rails project, which is implemented this way?
  4. How can I implement it with different approach which is more safer?

I apologize for such a long post, but I hope that more people has the same problem and it might help me to find more answers on my questions.


Solution

  • Add token_authenticable to devise modules (works with devise versions <=3.2)

    In user.rb add :token_authenticatable to the list of devise modules, it should look something like below:

    class User < ActiveRecord::Base
    # ..code..
      devise :database_authenticatable,
        :token_authenticatable,
        :invitable,
        :registerable,
        :recoverable,
        :rememberable,
        :trackable,
        :validatable
    
      attr_accessible :name, :email, :authentication_token
    
      before_save :ensure_authentication_token
    # ..code..
    end
    

    Generate Authentication token on your own (If devise version > 3.2)

    class User < ActiveRecord::Base
    # ..code..
      devise :database_authenticatable,
        :invitable,
        :registerable,
        :recoverable,
        :rememberable,
        :trackable,
        :validatable
    
      attr_accessible :name, :email, :authentication_token
    
      before_save :ensure_authentication_token
    
      def ensure_authentication_token
        self.authentication_token ||= generate_authentication_token
      end
    
      private
    
      def generate_authentication_token
        loop do
          token = Devise.friendly_token
          break token unless User.where(authentication_token: token).first
        end
      end
    

    Add migration for authentiction token

    rails g migration add_auth_token_to_users
          invoke  active_record
          create    db/migrate/20141101204628_add_auth_token_to_users.rb
    

    Edit migration file to add :authentication_token column to users

    class AddAuthTokenToUsers < ActiveRecord::Migration
      def self.up
        change_table :users do |t|
          t.string :authentication_token
        end
    
        add_index  :users, :authentication_token, :unique => true
      end
    
      def self.down
        remove_column :users, :authentication_token
      end
    end
    

    Run migrations

    rake db:migrate

    Generate token for existing users

    We need to call save on every instance of user that will ensure authentication token is present for each user.

    User.all.each(&:save)

    Secure Grape API using auth token

    You need to add below code to the API::Root in-order to add token based authentication. If you are unware of API::Root then please read Building RESTful API using Grape

    In below example, We are authenticating user based on two scenarios – If user is logged on to the web app then use the same session – If session is not available and auth token is passed then find user based on the token

    # lib/api/root.rb
    module API
      class Root < Grape::API
        prefix    'api'
        format    :json
    
        rescue_from :all, :backtrace => true
        error_formatter :json, API::ErrorFormatter
    
        before do
          error!("401 Unauthorized", 401) unless authenticated
        end
    
        helpers do
          def warden
            env['warden']
          end
    
          def authenticated
            return true if warden.authenticated?
            params[:access_token] && @user = User.find_by_authentication_token(params[:access_token])
          end
    
          def current_user
            warden.user || @user
          end
        end
    
        mount API::V1::Root
        mount API::V2::Root
      end
    end