This is a Rails 3.0 app with Mongoid as ODM. Following is the user model in which the devise is installed.
class User
include Mongoid::Document
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable
field :email
field :username
field :loginable_token
end
I'm building an API. So, for authentication I wanted to authenticate the users with :email and :password. From the GUI, its fine and working.
Now, I want to implement it via API, so I ve created a separate route /api_login.json that hits the login action with params {"username"=>"[email protected]", "password"=>"sachin", "action"=>"login", "controller"=>"api/v1/accounts", "format"=>"json"}
When I try to authenticate with the warden.authenticate!(:scope => :user) method before sign_in, it blows up with the following backtrace.
(rdb:1) l
[3, 12] in /Users/sts-151sts-151/Dev/cloudfactory/app/controllers/api/v1/accounts_controller.rb
3 respond_with(@current_account)
4 end
5
6 def login
7 debugger
=> 8 resource = warden.authenticate!(:scope => :user)
9 sign_in(:user, resource)
10 if user == nil
11
12 respond_with(User.account.apps.first)
(rdb:1) p params
{"username"=>"[email protected]", "password"=>"sachin", "action"=>"login", "controller"=>"api/v1/accounts", "format"=>"json"}
(rdb:1) rs = warden.authenticate!(:scope => :user)
*** Unknown command: "rs = warden.authenticate!(:scope => :user)". Try "help".
(rdb:1) p rs = warden.authenticate!(:scope => :user)
1) /api/v1/lines POST send back the default app
Failure/Error: last_response.status.should eql(200)
expected 200
got 401
(compared using eql?)
# ./spec/api/v1/accounts_spec.rb:46:in `block (3 levels) in <top (required)>'
The test is via default rack-test way.
What is correct way to authenticate?
The parameters hash need to nested inside user. For example, the username should be at params[:user][:username]. And please use the mailing list for questions next time. Thanks.
@josevalim!
I was not going to ask this here but I thought that moving this to mailing list now will duplicate the issue.
So, apology coz the problem still persists.
Now I'm passing/requesting with {"user" => {"email"=>"[email protected]", "password"=>"sachin"}, "action"=>"login", "controller"=>"api/v1/accounts", "format"=>"json"}. But still the same error.
I think I am not calling the warden.authenticate! method properly.
What params do I need to pass explicitly to that warden.authenticate! method? Its called in this action:
def login
resource = warden.authenticate!(:scope => :user)
sign_in(:user, resource)
respond_with(User.account)
end
Ah, notice that Devise will only accept the parameters if they come from the registered controller. Here is the code:
https://github.com/plataformatec/devise/blob/master/lib/devise/strategies/authenticatable.rb#L93
This is to avoid you being able to sign up from any controller using email and password credentials. To fix that, you can either monkey patch this method or change pass the :controllers => { :sessions => ... } option when calling devise_for.
It is important to notice that signing in through parameters were never intended to be used as you were trying. At this point, it is worth questioning why are you not using the sessions controller that ships with Devise itself.
Ah, its quite a long.
I am going this way for the API interface.
From the GUI, its that way and its working. But from the API how can I make use of the same sessions controller?
Coz the API is nested beneath 2 layer /api/v1/ !
And I didn't understand what you are saying in this line
change pass the :controllers => { :sessions => ... } option when calling devise_for
In theory the API should use something like Basic HTTP. But anyway, duplicating the logic here seems to be wrong. Couldn't you simply make /api/v1/accounts point to the sessions controller?
You mean to point /api/v1/accounts to the GUI sessions controller or the custom sessions controller for the api only at /api/v1/controllers/sessions_controller.rb file?
Why would the controller context affect whether or not I can use warden? Nothing in devise works outside it's original context. So frustrating. Every subtle customization of this library takes hours of digging and debugging.
Is there a straightforward solution to doing something so simple - authenticating a user based on login parameters outside the supplied devise controller? The suggestion to use the devise controller is ridiculous, especially for such a supposedly flexible and general purpose library.
@crystalneth you are aware you are commenting on a two years old issue, right? If you want to authenticate a user outside of a Devise controller, just call sign_in(@user) and done. Also, try not to take your frustration on the people that write the software you are using in the first place. ;)
sign_in(@user) doesn't check authentication information - it just signs in the user.
I need to check the auth parameters, which should be a pretty simple task for an authentication library. I was unable to figure out a reasonably clean way to do this and it seems I would need to dig deep into devise and warden to make sense of it.
All I want to do is warden.authenticate! and have it run through the same strategies as in the devise controller. Is there a reason I shouldn't be able to do that from any controller??
And yes, this is two years old, but there is no clear answer.
sign_in(@user) doesn't check authentication information - it just signs in the user.
I am aware. That's what you call if you want to have custom authentication rules. It was not clear what you wanted to achieve from your previous comment.
All I want to do is warden.authenticate! and have it run through the same strategies as in the devise controller. Is there a reason I shouldn't be able to do that from any controller??
Now that you worded the problem clearly... When you call authenticate_user! from any controller, Devise runs almost all strategies but not all. The reason is: you don't want people to sign in on any page by posting their email and password. You want that only for some specific controllers. We enable such strategies by explicitly calling allow_params_authentication! in your controller, before you call authenticate_user!, as you are expected to find in the SessionsController.
And if for some reason you can't call allow_params_authentication! because you are, for example, in a Sinatra app, the implementation is straight-forward: https://github.com/plataformatec/devise/blob/4681f81ce6b643cd55d7404500ee02ab18bab5df/lib/devise/controllers/helpers.rb#L96
I made this work without the "!" "warden.authenticate"
[alert] Off topic [/alert]
"So frustrating. Every subtle customization of this library takes hours of digging and debugging."
@crystalneth.....try not to be thankless for free software that you are using. Nobody forced you to use it, and if you can create something better, feel free to use that, and may be share with others as well.
Thank you @josevalim for such useful piece of software. Had he tried the authentication libraries prior to when devise arrived, he would have known ;)
Awesome job!
:heart: :green_heart: :blue_heart: :yellow_heart: :purple_heart:
@josevalim Thank you, Jose!
An easy way to check if a password is valid is to use the
user.valid_password?("GEx9oZRrLVqGbhCuY2QX")
routine. It will return true if the password is valid for the specific password. Of course, it doesn't check other strategies, but depending on what you are trying to do, this may be a simple solution.
Despite being an only issue and maybe a bit contentious, this helped me three years later!
prepend_before_filter :allow_params_authentication!, only: :create
Adding the above line to my controller helped me wrap up my json authentication API. Thanks to everyone in this thread!
thank you @josevalim, you seriously rock! like thank you so much.
From my experience, even prior to custom encryption based on old password salt, I converted the whole database to be authenticate-able with this devise gem. Using the sha1 ( =)) ) to generate our custom salt. Devise looks complex but it gives a damn big scope to put your work onto it but keep the system safe.
I would like to thanks to this free software and the contributors and @josevalim for this such beautiful gem.
Was a solution ever found for this? I'm having the same problem trying to use the following JSON-API schema to authenticate:
{
"data" => {
"type" => "user-session",
"attributes" => {
"email" => user.email,
"password" => user.password
}
}
}
I'll have another go at trying to dig into Devise code tomorrow (maybe I'm missing something obvious, but already spent half a day trying to figure this out).
@habitullence I cannot remember what I was working on when I got this working but did you try the before filter allow_params_authentication?
prepend_before_filter :allow_params_authentication!, only: :create
Thanks @RyanRusnak, AFAIK this doesn't allow the format to be changed.
Yea i know this issue was opened in 2011, but for what is worth, this completely solved the problem I was having, which was that I was not sending the data in the right shape - i.e. {email: email, password: password} is wrong; {user: {email: email, password: password}} is correct.
I will going to summary the solution for beginner. Nothing else :) Thanks everyone :)
user = User.find_by(email: params[:email].downcase)
if user && user.valid_password?(params[:password])
# access token generation logic here
{token: access_token}
else
error!('Unauthorized.', 401)
end
@StevenXL You saved me! Thanks
@lalit-nga your solution does not cover confirmable, lockable and similar modules.
The correct way for doing this in my mind the following:
class User < ApplicationRecord
def self.find_and_validate_for_authentication(params)
user = find_for_database_authentication(params.except(:password)) || new(params)
if user.new_record?
user.errors.add(:email, :invalid_email_or_password)
elsif !user.valid_password?(params[:password])
user.errors.add(:email, :invalid_email_or_password)
elsif !user.active_for_authentication?
user.errors.add(:email, user.inactive_message)
end
user
end
end
I think this may solve your problem
self.user = warden.authenticate!(auth_options)
Most helpful comment
@crystalneth you are aware you are commenting on a two years old issue, right? If you want to authenticate a user outside of a Devise controller, just call
sign_in(@user)and done. Also, try not to take your frustration on the people that write the software you are using in the first place. ;)