NOTE: This article was originally posted on February 22, 2020.
You can view the original article here.
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.