The One With a JSON API Login Using Devise

The situation: You need to add an iOS app to your Rails application. Users can login to both locations, and you’re using Devise for authentication.

The problem: How do you authenticate users on the iPhone using an email/password created on the website? And how do you tell Devise not to redirect to a login page when you’re using a JSON API?

tl;dr Use this gist for an API JSON Devise Sessions Controller that does not redirect.

Background

Devise provides an awesome :token_authenticatable setup where you can login a user using an “auth_token” query_string.

/api/recipes?qs=sweet&auth_token=[@user.auth_token]

Then, in your Api::RecipesController, you’d do something like:

class Api::RecipesController < Api::BaseApiController
  before_filter :authenticate_user!

  respond_to :json
  def index
    @recipes = Recipe.search(params.fetch(:qs, ""))
  end
end

The above will authenticate the user by cookies/session/token, and return recipes in JSON.

But, How do you get the token?

Authentication and Login

We’ll let the user enter their email and password on the iPhone, and have the endpoint return the token in JSON if valid. We’ll want an HTTP Status code of 401 if it’s invalid.

We’ll have a Base API Controller from which we inherit

class Api::BaseApiController < ApplicationController
  respond_to :json
end

First, you’ll create an api endpoint for your session:

class Api::SessionsController < Api::BaseController
  prepend_before_filter :require_no_authentication, :only => [:create ]
  include Devise::Controllers::InternalHelpers

  def create
    build_resource
    resource = User.find_for_database_authentication(:login=>params[:user_login][:login])
    if resource.nil?
      render :json=> {:success=>false, :message=>"Error with your login or password"}, :status=>401
    end

    if resource.valid_password?(params[:user_login][:password])
      sign_in("user", resource)
      render :json=> {:success=>true, :auth_token=>resource.authentication_token, :login=>resource.login, :email=>resource.email}
    else
      render :json=> {:success=>false, :message=>"Error with your login or password"}, :status=>401
  end

The problem is that you don’t get the 401 Status code. You get a 302 redirect to the user login page. And that dog won’t hunt on an iPhone app.

WTH Devise?

Devise is relying on Warden, and warden is doing what it’s told. If not a valid login, it’ll redirect away. So, we need to tell Warden we’re going to have a custom failure like so:

warden.custom_failure!

Refactored down slightly, we get:

The Solution

Your users will receive something this this on success: ~~~ { “success”: true, “auth_token”: “dfc7ad3884e7”, “login”: “emailexample”, “email”: “email@example.com” } ~~~

And this on failure (with a status code of 401). ~~~ { “success”: false, “message”: “Error with your login or password” } ~~~

Version Disclosure: This post was valid with Devise 1.4.6 and Rails 3.0/3.1.