How to use devise and devise_token_auth

Reading Time: 2 minutes

Have you ever had difficulties adding authentication to an API which had already set up devise authentication? I encountered the same issue while trying to apply devise_token_auth in my project.

In this post, I am going to show you how to solve this problem; presuming that you have already configured an API with CORS.

First of all, you need to install devise token auth by adding the gem to your Gemfile:

gem 'devise_token_auth'

And execute:

bundle install

We want users to authenticate via devise for our web application and devise_token_auth for the API, to do this, we will mount it to the API namespace and create another application controller:

app/controllers/api/v1/application_controller.rb

module Api
  module V1
    class ApplicationController < ActionController::API
      include DeviseTokenAuth::Concerns::SetUserByToken
      before_action :authenticate_user!
      before_action :configure_permitted_parameters, if: :devise_controller?
      
      protected

      def configure_permitted_parameters
        devise_parameter_sanitizer.permit(:sign_in, keys: [:email, :password])
      end
    end
  end
end

config/routes.rb

   namespace :api, defaults: { format: 'json' } do
      namespace :v1 do
        mount_devise_token_auth_for 'User', at: 'auth'
        ...
      end
    end

Now we need to permit some parameters to sign up by overriding the registrations controller:

app/controllers/api/v1/auth/registrations_controller.rb

module Api
  module V1
    module Auth
      class RegistrationsController < DeviseTokenAuth::RegistrationsController
        skip_before_action :verify_authenticity_token
        wrap_parameters User, include: [:name, :email, :password, :password_confirmation]

        private

        def sign_up_params
          params.require(:user).permit(:name, :email, :password, :password_confirmation)
        end

        def account_update_params
          params.require(:user).permit(:name, :email)
        end
      end
    end
  end
end

Add protect from forgery when the controller is not devise_token_auth and make authenticate_current_user return unauthorized when auth_header is not assigned in cookies then add the following code to your application controller:

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception, if: :verify_api

  def authenticate_current_user
    head :unauthorized if current_user_get.nil?
  end

  def current_user_get
    return nil unless cookies[:auth_headers]
    auth_headers = JSON.parse(cookies[:auth_headers])
    expiration_datetime = DateTime.strptime(auth_headers['expiry'], '%s')
    current_user = User.find_by(uid: auth_headers['uid'])

    if current_user &&
       current_user.tokens.key?(auth_headers['client']) &&
       expiration_datetime > DateTime.now
      @current_user = current_user
    end

    @current_user
  end

  def verify_api
    params[:controller].split('/')[0] != 'devise_token_auth'
  end

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:uid, :provider])
  end
  …
end

We need to persist the UID and provider when a user signs up which can be done by overriding the Devise registrations controller create method:

app/controllers/users/registrations_controller.rb

module Users
  class RegistrationsController < Devise::RegistrationsController

    def create
      if params['user']['uid'].nil? && params['user']['provider'].nil?
        configure_permitted_parameters
        params['user']['uid'] = params['user']['email']
        params['user']['provider'] = 'email'
      end
      super
    end
end

Skip authenticity token validation by overriding the devise session controller and adding a skip filter:

app/controllers/users/sessions_controller.rb

module Users
  class SessionsController < Devise::SessionsController
    skip_before_action :verify_authenticity_token, only: :create, raise: false
    ...
  end
end

Add this line to user controller:

app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :autenticate_user, unless: :verify_api
  …
end

Add the :validatable module and a method to generate a valid uid to your devise user model:

app/models/user.rb

class User < ApplicationRecord

  include DeviseTokenAuth::Concerns::User

  devise :database_authenticatable, :registerable, :async, :confirmable,
    :recoverable, :rememberable, :trackable, :validatable, :confirmable,
    :omniauthable
    ...
  
  before_validation :set_uid

 def self.from_omniauth(auth)
    where(provider: auth.provider.to_s, uid: auth.uid.to_s).first_or_create do |user|
      user.provider = auth.provider
      user.uid = auth.uid
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20]
      user.name = auth.info.name
      user.image = auth.info.image
      user.skip_confirmation!
    end
  end

  def set_uid
    self.uid = self.class.generate_uid if self.uid.blank?
  end

  def self.generate_uid
    loop do
      token = Devise.friendly_token
      break token unless to_adapter.find_first({ uid: token })
    end
  end
  ...
end

Add a devise token auth initializer:

config/initializers/devise_token_auth.rb

DeviseTokenAuth.setup do |config|
  config.change_headers_on_each_request = false
  config.enable_standard_devise_support = true
  bypass_sign_in = false
  ...
end

I hope this will help you!

If you want to know more about MagmaLabs, visit us here!

0 Shares:
You May Also Like
Read More

VTex Speed Configuration

Reading Time: 3 minutes VTex is a SAAS E-commerce platform that has tons of modules that help out with the implementation of…