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!