48

I am trying to build a rails API for an iphone app. Devise works fine for logins through the web interface but I need to be able to create and destroy sessions using REST API and I want to use JSON instead of having to do a POST on the sessions controller and having to parse the HTML and deal with a redirect.

I thought I could do something like this:

class Api::V1::SessionsController < Devise::SessionsController  
  def create
    super
  end  
  def destroy
    super
  end  
end

and in config/routes.rb I added:

namespace :api do
  namespace :v1 do
    resources :sessions, :only => [:create, :destroy]
  end
end

rake routes shows the routes are setup properly:

   api_v1_sessions POST   /api/v1/sessions(.:format)     {:action=>"create", :controller=>"api/v1/sessions"}
    api_v1_session DELETE /api/v1/sessions/:id(.:format) {:action=>"destroy", :controller=>"api/v1/sessions"}

When I POST to /user/sessions everything works fine. I get some HTML and a 302.

Now if I POST to /api/v1/sessions I get:

Unknown action AbstractController::ActionNotFound

curl -v -H 'Content-Type: application/json' -H 'Accept: application/json'   -X POST http://localhost:3000/api/v1/sessions   -d "{'user' : { 'login' : 'test', 'password' : 'foobar'}}"
dylanfm
  • 6,292
  • 5
  • 28
  • 29
Akshay Kumar
  • 1,074
  • 1
  • 10
  • 13

7 Answers7

42

This is what finally worked.

class Api::V1::SessionsController < Devise::SessionsController  
  def create  
    respond_to do |format|  
      format.html { super }  
      format.json {  
        warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")  
        render :status => 200, :json => { :error => "Success" }  
      }  
    end  
  end  
  def destroy  
    super  
  end  
end  

Also change routes.rb, remember the order is important.

devise_for :users, :controllers => { :sessions => "api/v1/sessions" }
devise_scope :user do
  namespace :api do
    namespace :v1 do
      resources :sessions, :only => [:create, :destroy]
    end
  end
end

resources :users
Akshay Kumar
  • 1,074
  • 1
  • 10
  • 13
  • 2
    could you elaborate a bit more? Are you passing in login and password to authenticate? – John Hinnegan Feb 20 '11 at 00:43
  • One thing to add to this: I originally had a custom login view under app/views/devise/sessions/new.html.erb. I had to move this to a new view folder to match the custom controller name/namespace. – Matt Huggins Aug 29 '11 at 19:59
10

I ended up using a combination of @akshay's answer and @mm2001's answer.

class Api::SessionsController < Devise::SessionsController
  def create
    warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#failure")
    render :json => {:success => true}
  end

  def destroy
    Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
    render :json => {}
  end

  def failure
    render :json => {:success => false, :errors => ["Login Failed"]}
  end
end

... and in the devise initializer, I had to do this to get the #create method to use my :recall handler

# config/initializers/devise.rb
config.navigational_formats = [:"*/*", "*/*", :html, :json]

This is with Devise 1.5.1 and Rails 3.1.

declan
  • 5,605
  • 3
  • 39
  • 43
  • 4
    +1 for the info on `config.navigational_formats`. One thing that may make this more robust in case you happen to update devise; You can just override the Devise::SessionsController.auth_options to return your failure method and then use the default implementations for everything else instead of implementing create and destroy yourself (assuming the default response is sufficient). `def auth_options super.merge({:recall => "#{controller_path}#failure" }) end` – plainjimbo May 02 '12 at 19:32
  • 1
    Oh, by the ways, I'm on devise 2.0.4 and rails 3.2. So that may affect whether others can do what I did. – plainjimbo May 02 '12 at 19:35
  • 1
    This works great, but I suggest returning a 401 for failure condition. `render :json => {:success => false, :errors => ["Login Failed"]}, status: 'unauthorized'` – mahemoff Oct 03 '12 at 02:40
  • 1
    @mahemoff: excellent advice! however, it seems that status needs to be a symbol. `render :json => {:success => false, :errors => ["Login Failed"]}, status: :unauthorized` – Benjamin Atkin Nov 24 '13 at 09:58
  • http://blog.observationpoint.org/post/52084218731/using-json-with-devise-invitable-for-ajax-invitations – aceofspades Jun 08 '14 at 21:49
9

A recent answer here: http://jessehowarth.com/devise has some more detail (per Using Devise 1.3 to authenticate JSON login requests)

Community
  • 1
  • 1
mm2001
  • 6,427
  • 5
  • 39
  • 37
5

I solved the problem by creating a small service that dispenses authentication tokens. I wrote a blog post about it: http://matteomelani.wordpress.com/2011/10/17/authentication-for-mobile-devices/. You can also get the code here: https://github.com/matteomelani/Auth-Token-Service-Prototype.

Matteo Melani
  • 2,706
  • 2
  • 24
  • 30
  • It seems like there is a security flaw with this technique in that it circumnavigates the Lockable module, maybe I'm wrong about this... – user160917 Jul 09 '12 at 19:03
1

From the rdoc for devise's #devise_scope:

Sets the devise scope to be used in the controller. If you have custom routes, you are required to call this method (also aliased as :as) in order to specify to which controller it is targetted.

as :user do
  get "sign_in", :to => "devise/sessions#new"
end

Notice you cannot have two scopes mapping to the same URL. And remember, if you try to access a devise controller without specifying a scope, it will raise ActionNotFound error.

It looks like you need to wrap it in a #as block:

as :user do
  namespace :api do
    namespace :v1 do
      resources :sessions, :only => [:create, :destroy]
    end
  end
end
Michael Nutt
  • 178
  • 1
  • 8
  • According to this URL https://github.com/plataformatec/devise/wiki/How-To:-Change-the-default-sign_in-and-sign_out-routes The way he layed his code is correct. I am also facing the same bug as him. – Pier-Olivier Thibault Feb 01 '11 at 06:12
  • No, it's not. The rdoc pretty clearly states that if you subclass a devise controller, the route in which you call it needs to be wrapped in an #as or a #devise_scope block, or else you'll get an ActionNotFound error. – Michael Nutt Feb 01 '11 at 18:52
1

An alternative solution to creating/destroying sessions is to use Devise's token_authenticatable module, and then update the other functions in your API so that they take the token as a mandatory parameter. This is arguably a more ReSTful design, since it retains statelessness (i.e., there's no session state anywhere). Of course, this advice holds for your JSON API, but I wouldn't recommend the same for your HTML UI (long token strings in your browser's URL bar are not a pretty sight).

See here for an example.

jonsca
  • 10,218
  • 26
  • 54
  • 62
  • 1
    This was removed from Devise in v3.1.0, FWIW. http://blog.plataformatec.com.br/2013/08/devise-3-1-now-with-more-secure-defaults/ – turboladen Oct 03 '13 at 16:37
0

Not sure navigation-formats should be used, an API isn't really that...

Per this blog post just add

respond_to :html, :json

to your controllers.

aceofspades
  • 7,568
  • 1
  • 35
  • 48