NOTE: This article was originally posted on February 22, 2020.

You can view the original article here.


Devise

Devise is great for authenticating Rails applications. You’d be hard-pressed to find a Rails developer who hasn’t used Devise as their authentication strategy at some point.

That’s great, unless you’re building an API with the --api-only flag in Rails 5+.

Devise doesn’t officially support API-only Rails apps, and while it can be integrated error-free, the results are sometimes less than stellar. One of the biggest complaints is the lack of Devise’s flash messages in response payloads.

Let’s walk through a quick example of how to get some basic status messages returned as JSON:

Step 1: Setup a new Rails app

Go ahead and get a new app set up, and install Devise:

$ rails new my_api --api

You’ll need to add the following to your Gemfile, then run bundle install:

gem 'devise'

After that, run:

rails generate devise:install

This should give you a list of instructions to complete. Be sure not to skip any! The Devise GitHub page has more info if you get stuck. You’ll also need to configure your routes at this point.

Step 2: Install JWT authentication gem

Because Devise uses session cookies by default and API Rails apps do not, it’s best to use some sort of JWT (JSON Web Token) authentication. I’m a big fan of devise-jwt since the developer gives a very strong reasoning for his decisions. Check it out on his blog. ` Add to the Gemfile, then bundle install:

gem 'devise-jwt', '~> 0.5.9'

Next, you’ll need to configure a second secret in config/initializers/devise.rb:

Devise.setup do |config|
  # ...
  config.jwt do |jwt|
    jwt.secret = ENV['DEVISE_JWT_SECRET_KEY']
  end
end

I made a second secret with rake secret as recommended for security purposes.

In that same file, you’ll also want to change the navigational_formats setting to disable flash messages (since Rails won’t load the middleware required to display them in API-only mode):

config.navigational_formats = []

Finally, you’ll need to configure your model (most likely User) to have a JWT revocation strategy. There are a couple, so I’ll defer to the install instructions on this.

Step 3: Implement a custom Devise failure app

Devise uses a Devise::FailureApp class to correctly display flash messages when signing in/signing out. We can use this to generate a JSON payload with the original flash messages for signing in.

Create a new model in models/devise named api_failure_app.rb that contains the following:

module Devise
  class ApiFailureApp < Devise::FailureApp
    def respond
      json_api_error_response
    end

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

This code basically hijacks the old respond message which would have set a flash message, and instead outputs that response as a JSON payload.

Making a POST request to your authentication route with an empty body should return the following:

{
  "errors": [
    {
      "message": "You need to sign in before continuing."
    }
  ]
}

If you want to take it a step further, devise-jwt returns the token in Authorization headers which I didn’t particularly like, so you can also force that into the response body like so (you’ll want to configure routes to use your session controller over Devise’s if you do this):

class SessionsController < Devise::SessionsController
  skip_before_action :authenticate_user!
  private
  def respond_with(*args)
    render json: { token: request.env['warden-jwt_auth.token'] }  
  end
  def respond_to_on_destroy
    head :no_content
  end
end

Hopefully this is helpful with a simple change you’ll be able to utilize some of Devise’s flash messages with your authentication workflow for API-only Rails apps.


Tyler Porter

I'm a Ruby on Rails developer professionally interested in cycling, hiking, baseball, and video game development on the side. Most of my projects attempt to integrate one or more of these.