Gabriel Hilal
7 Nov, 2015 • 6 min read

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.

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.

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:

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

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