in Ruby

Sending JSON API like 401 Unauthorized error with Devise

If you are using Ruby on Rails together with Devise gem you might be wondering
how to handle unauthorized error responces in your JSON API.

JSON API prescribes that to send errors you need a root “errors” array of errors with as least a status code (as string) and a title (as string). Essentially that means to turn our error message issued by Devise into the following:

{"errors":[{"status":"401","title":"Unauthorized"}]}

The problem is that Devise does not really throw an exception that we could rescue_from from our controller. What we need is to create a custom equivalent of Devise::FailureApp:

# Our custom failure response app since we want to return JSON:API like
# messages for some APIs.
module MyApp
  class FailureApp < Devise::FailureApp
    def respond
      if request.controller_class.to_s.start_with? 'API::'
        json_api_error_response
      else
        super
      end
    end

    def json_api_error_response
      self.status        = 401
      self.content_type  = 'application/json'
      self.response_body = { errors: [{ status: '401', title: i18n_message }]}.to_json
    end
  end
end

In the respond method we can handle special cases by quering information from the request object. In our example we will respond differently in case where the controller is one of our API controllers. (This is because basing it on being a JSON requests would backfire for our AJAX endpoints. Feel free to adjust this to your needs.)

Once we have that in place we just need to tell Devise to actually use it which can be done within Devise initializer:

Devise.setup do |config|
 ...
 config.warden do |manager|
     manager.failure_app = MyApp::FailureApp
  end
  ...
end

And that’s it!

Write a Comment

Comment

  1. What’s curious about this is I keep seeing a lot of intense pushback from the devise team regarding using it with APIs:

    “As far as my understanding goes, you shouldn’t be using Devise with an API only rails app.
    Devise depends on session and cookies, and you should not be using those for an API
    An API is not the use case for Devise so these are really not bugs with the gem”

    https://github.com/plataformatec/devise/issues/4275

    Just fyi that while what you posted seems reasonable, I don’t understand why this isn’t baked right into Devise, and maybe there’s some security implications to doing this.

    • I would not use it for API only Rails app so I actually relate to that. In our case we have some APIs that does not depend on Rails sessions but we are building one for the front-end components so we have to go this way until we can replace it everything with a single SPA (the result should be a separate front-end app and API backend). Actually I think this is a reasonable use-case and wish that Devise support it better.