Integrating Devise Invitable into Devise Token Auth
You might be familiar with Devise, a popular authentication solution that brings useful modules to your rails application, such as :confirmable
, :recoverable
, :registerable
, :rememberable
, among others. There is also the Devise Invitable gem that adds the :invitable
extra module to it, allowing registered users to invite people to the system.
However, if your application has the only purpose to be an API, you might want to go with Devise Token Auth. It is a token base authentication designed to work with jToker and ng-token-auth, facilitating the integration of Devise with API clients. This gem is based on Devise, but overrides some original methods and controllers to adjust it for API purpose.
At the time this article was written, if you try to use the :invitable
module with Devise Token Auth, it will raise an error. So, this post provides some details about the problem and explains how to solve it.
The Problem
When you try to use the :invitable
module with Devise Token Auth you will face the following error:
ArgumentError in Devise::InvitationsController#create wrong number of arguments (1 for 0)
Devise Token Auth overrides the Devise controllers, but it is not expecting the InvitationsController
included by Devise Invitable. The problem lies in the authenticate_user!
method: while devise_token_auth
does not expect any params, the original devise
is expecting a hash.
- more details can be found in this issue.
The Solution
In order to solve the above problem we need to override some methods in the InvitationsController
to match the way Devise Token Auth works under the bonnet.
Routes
Let’s start telling devise_token_auth
to not mount the routes for invitations. We need to mount it using the default devise_for
, setting the controller that will override the original methods:
namespace :api do
mount_devise_token_auth_for 'User', at: 'auth', skip: [:invitations]
devise_for :users, path: "auth", only: [:invitations],
controllers: { invitations: 'api/invitations' }
end
Override Invitable Methods
In order to keep our code organised, we can create a new file in the controllers/concerns
folder, called invitable_methods.rb
. The following methods must be changed to match the way devise_token_auth
works.
- authenticate_user!: This method is raising the above error. We need to change it to match the method in
devise_token_auth
gem. - authenticate_inviter!: Originally it calls the
authenticate_user!
, but since we are changing it and replacing the controller, we can avoid this extra method. - resource_class(m = nil): Similar to
authenticate_user!
, we need to change it to match thedevise_token_auth
method. - resource_from_invitation_token: This method returns a flash message, but in our case we just need the
json
format.
module InvitableMethods
extend ActiveSupport::Concern
def authenticate_inviter!
# use authenticate_user! in before_action
end
def authenticate_user!
return if current_user
render json: {
errors: ['Authorized users only.']
}, status: :unauthorized
end
def resource_class(m = nil)
if m
mapping = Devise.mappings[m]
else
mapping = Devise.mappings[resource_name] || Devise.mappings.values.first
end
mapping.to
end
def resource_from_invitation_token
@user = User.find_by_invitation_token(params[:invitation_token], true)
return if params[:invitation_token] && @user
render json: { errors: ['Invalid token.'] }, status: :not_acceptable
end
end
Invitations Controller
We have successfully overridden all the methods that were incompatible with devise_token_auth
, so we just need to create our InvitationsController
and include the above InvitableMethods
.
Our controller needs three actions:
- create: Used to send the invitaton mail to an user. The
authenticate_user!
in abefore_action
ensures that only logged users can send an invitation. This action accepts the params defined ininvite_params
. - edit: When the invited user clicks on the link received by email, the
resource_from_invitation_token
will find and set the@user
, and then render the edit action. Since there are no views to set the password in our Rails API, we need to redirect the user to the API client, sendding theinvitation_token
in the URL. - update: Finally, the update action will receive the
accept_invitation_params
, which includes thepassword
,password_confirmation
and theinvitation_token
sent to the API client. In case you want to update any other attribute, you can include it in theaccept_invitation_params
.
module Api
class InvitationsController < Devise::InvitationsController
include InvitableMethods
before_action :authenticate_user!, only: :create
before_action :resource_from_invitation_token, only: [:edit, :update]
def create
User.invite!(invite_params, current_user)
render json: { success: ['User created.'] }, status: :created
end
def edit
redirect_to "#{client_api_url}?invitation_token=#{params[:invitation_token]}"
end
def update
user = User.accept_invitation!(accept_invitation_params)
if @user.errors.empty?
render json: { success: ['User updated.'] }, status: :accepted
else
render json: { errors: user.errors.full_messages },
status: :unprocessable_entity
end
end
private
def invite_params
params.permit(user: [:email, :invitation_token, :provider, :skip_invitation])
end
def accept_invitation_params
params.permit(:password, :password_confirmation, :invitation_token)
end
end
end
Conclusion
At the time this article was written, the use of Devise Invitable with Devise Token Auth was not possible, since the :invitable
module raised an error. This article discussed the problem and explained in details a workaround to solve the issue. After the above steps you should be able to successfully add the invitable feature to your Rails API.
Post by: Gabriel Hilal
Originally published at gabrielhilal.com on November 7, 2015.