User Authentication with Grape and Devise
Asked Answered
N

3

23

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.

Nutter answered 29/10, 2014 at 6:28 Comment(1)
Really, no one is doing the same thing? Or its too long to read? OMG....Nutter
N
22

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
Nitid answered 2/11, 2014 at 16:50 Comment(0)
M
5

Although I like the question and the answer given by @MZaragoza I think it is worth noting that token_authentical has been removed from Devise for a reason! Use of the tokens are vulnerable for timing attacks. See also this post and Devise's blog Therefor I haven't upvoted @MZaragoza's answer.

In case you use your API in combination with Doorkeeper, you could do something similar, but instead of checking for the authentication_token in the User table/model you look for the token in the OauthAccessTokens table, i.e.

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

This is more safe, because that token (i.e. the actual access_token) exists only for a certain amount of time.

Note in order to be able to do this you must have a User model and OauthAccessToken model, with:

class User < ActiveRecord::Base

   has_many :oauth_access_tokens

end

class OauthAccessToken < ActiveRecord::Base
    belongs_to :user, foreign_key: 'resource_owner_id'
end

EDIT: Please also note that generally you should not include the access_token in the URL: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-bearer-16#section-2.3

Mcbrayer answered 26/3, 2015 at 14:50 Comment(1)
You should also filter by non-revoked oauth access tokens. This approach will work for expired tokens as wellOrmolu
L
0

As pointed out by @PSR, a simple token-based auth is not safe. The proper solution would use refresh tokens and access tokens as described e.g. in The Ultimate Guide to handling JWTs on frontend clients.

However, nowadays we can use SameSite and HttpOnly cookies which makes session-based authentication for webapps safe & simple.

In config/initializers/session_store.rb

# Use safe SameSite cookies. HttpOnly is already the default.
Rails.application.config.session_store :cookie_store, same_site: :strict

In the Grape API base

# Enable session middleware for auth: https://mcmap.net/q/585955/-why-doesn-39-t-session-work-in-grape-rails
use ActionDispatch::Session::CookieStore
helpers do
  def session
    env['rack.session']
  end
end

The API

helpers Devise::Controllers::SignInOut

resource :users do
  params do
    requires :user, type: Hash do
      requires :email
      requires :password
    end
  end
  post :login do
    user = User.find_by(email: params[:user][:email])
    if user&.valid_password?(params[:user][:password])
      sign_in(user)
      { user_id: user.id }
    else
      error!('Invalid email/password combination', 401)
    end
  end
end

Two caveats:

  • This adds the Session layer to the API which will cause some overhead that Grape by default would avoid. However, the safe refresh token approach would also use cookies, so I don't see a way around this. Also, this won't make or break your app.
  • Check the browser support for SameSite cookies: https://caniuse.com/same-site-cookie-attribute
Lowly answered 12/1, 2022 at 14:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.