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 the devise_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 a before_action ensures that only logged users can send an invitation. This action accepts the params defined in invite_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 the invitation_token in the URL.
  • update: Finally, the update action will receive the accept_invitation_params, which includes the password, password_confirmation and the invitation_token sent to the API client. In case you want to update any other attribute, you can include it in the accept_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.