Development

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!

Javascript
AngularJS Providers under the hood
Development
What is VTEX platform and why should you migrate your ecommerce to it?
Best Practices
De Código, Café y Cervezas 10 – Technical Debt
  • Tiago Cassio

    Hi, im getting “found unpermitted parameters: :format, :session”