I'm building an app that uses Devise to manage user state. I'm also building an API in that same app that receives a username and password from a POST request
What I'm trying to accomplish is:
Get the user by username from the database (done, straightforward)
Use Devise::Models::DatabaseAuthenticatable to take the password the user passed in, encrypt it, compare it against the encrypted_password field on the User model and if they're the same, proceed with the rest of my code
The second bullet above is what I'm having trouble with. In a console, I can't seem to get an instance of the module Devise::Models::DatabaseAuthenticatable to try the various instance methods that you can find here in the docs.
Any help would be greatly appreciated!
If I understood your question correctly, you can use .valid_password? devise method. Something like that:
#assuming you'll receive nested params like user[email], user[password]...
user_params = params.require(:user).permit(:email, :password)
user = User.find_by(email: user_params[:email])
return head 403 if user.nil?
valid = user.valid_password?(user_params[:password]) #true or false...
return head 403 unless valid
sign_in(user) #devise helper: if you want to sign in that user
You can also check another approachs, like devise token auth gem.
Related
I let users log in initially without confirming their email address - but after 7 days, if they haven't confirmed - I block access until they confirm their address.
(Note - this is achieved by setting config.allow_unconfirmed_access_for = 7.days in the Devise initialiser)
If they hit the 'grace' limit (e.g. they don't confirm and 7 days pass) then I want to:
send them to a page which explains what is going on (I can do this
part)
automatically re-send the confirmation email
to do #2 I need to access the user to get the email address.
Devise obviously 'knows' who the user is - that's how it knows they have passed the confirmation expiry.
If the user has just tried to log in, then I can get this by looking in the params.
However if the user already has a live login token in their session, then when they pass the magical week - they'll suddenly start being rejected by devise. How do I access the user in this case?
#based on
#https://github.com/plataformatec/devise/wiki/How-To:-Redirect-to-a-specific-page-when-the-user-can-not-be-authenticated
#https://stackoverflow.com/questions/9223555/devise-with-confirmable-redirect-user-to-a-custom-page-when-users-tries-to-sig
class CustomFailure < Devise::FailureApp
def redirect_url
if warden_message == :unconfirmed
user = User.find_by_email(params.dig(:user,:email))
user&.send_confirmation_instructions
if user.nil?
#if the user had a valid login session when they passed the grace period, they will end up here
!! how do I get the user in this scenario !!
end
confirmation_required_info_path(params: {found: !user.nil?})
elsif warden_message == :invalid
new_user_session_path(user:{email: params.dig(:user,:email)})
else
super
end
end
# You need to override respond to eliminate recall
def respond
if http_auth?
http_auth
else
redirect
end
end
end
This achieves goal #1, but it only achieves goal #2 if if the failure is the result of new signup
is there a direct way to access the user when they have a live session, but have passed the expiry date?
(current_user is not available, env['warden'].user is nil)
thank you
Rails 5.0.6
devise 4.2
edit: Updating to clarify with an example scenario where I need help:
day 0: User signs up with email/password. I let them in without confirming their email. They have a 7-day grace period to confirm their email.
day 2: They log out
day 7 (morning): They log in again
day 7 (later in the day): They do some action. Their login token is still valid - devise recognises it, finds their user record and checks if they have confirmed their email address. They have not - so devise refuses to authorise the action, giving the error :unconfirmed
In this scenario - they come through to the failure app. I will redirect them to a page which says 'you have passed your 7-day grace period, you really need to confirm your email address now'.
In the failure app, I want to know what their email address is so that I can automatically re-send the confirmation email. How do I get this?
Note - in this scenario, devise has refused authorisation. current_user is nil. However Devise clearly 'knows' who the user is - because it was able to look up their record in the database, and check that they had gone past the grace period for unconfirmed email addresses. How do I access that same 'knowledge'
I think there are better ways of achieving the same result without creating a Devise::FailureApp:
This could be achieved by overriding the confirmed? method from Devise's resource extension present in the Confirmable module.
A simple example would be:
Add a delayed_confirmation_expiry_date datetime field to your model's table, via migration.
This field will be used to store the expiry datetime when the user first registers into your app. You will have to override the SessionsController#create method for that, so it can call the #delay_confirmation! method on your resource.
Add inside your User model equivalent :
# Will update the field you have added with the new temporary expiration access datetime
def delay_confirmation!(expiry_datetime=7.days.from_now)
self.delayed_confirmation_expiry_date = expiry_datetime
self.save
end
# Override that will make sure that, once the user is confirmed, the delayed confirmation information is cleared
def confirm(args={})
clear_delay_confirmation!
super
end
# Self-explanatory
def clear_delay_confirmation!
self.delayed_confirmation_expiry_date = nil
self.save
end
# Used on your controllers to show messages to the user warning him about the presence of the confirmation delay
def confirmation_is_delayed?
self.confirmed? && !self.confirmed_at.present?
end
# Overrides the default implementation to allow temporary access for users who haven't confirmed their accounts within the time limit
def confirmed?
if !self.confirmation_is_delayed?
super
else
self.delayed_confirmation_expiry_date >= DateTime.now.in_time_zone
end
end
Actually I am using devise for login and registration and its working fine, it send a confirmation email which is really good. But I need to send confirmation email in specific cases. I am passing user type in URL and on behalf of that I want to set mail. I have two type of users, one will be confirm their account their self reset of the users can not confirm their account only admin can approve their accounts. I have override the create method
def create
super
if params[:type]=='xyz
#user.skip_confirmation_notification!
end
end
but it sends mail in both cases. Please tell where am wrong.
So according to devise confirmable module you can skip to send confirmation and email by following code.
def confirm_your_type_user_without_confirmation_email
# check your condition here and process the following
self.skip_confirmation!
self.confirm!
# condition may end here
end
Now lets call it on create hook.
class User < ActiveRecord::Base
after_create :confirm_your_type_user_without_confirmation_email
....
end
for more reference you may check this:
Devise Confirmable module
The solution should be something similar as I mentioned here above. And its best practice to avoid controller to handle these responsibilities, because its not something your controller should take. :)
I hope my answer will give you some way to solve your problems! Thanks!
So, I am somewhat new to rails and devise, so I apologize in advance if this is a basic question. I couldn't find any information on this anywhere, and I searched thoroughly. This also makes me wonder if Devise is the right tool for this, but here we go:
I have an app where devise user authentication works great, I got it, implemented it correctly and it works.
In my app, users can belong to a group, and this group has a password that a user must enter to 'join' the group.
I successfully added devise :database_authenticatable to my model, and when I create it an encrypted password is created.
My problem, is that I cannot authenticate this! I have a form where the user joins the group, searching for their group, then entering the password for it.
This is what I tried:
def join
#home = Home.find_for_authentication(params[:_id]) # method i found that devise uses
if #home.valid_password?(params[:password]);
render :json => {success: true}
else
render :json => {success: false, message: "Invalid password"}
end
end
This gives me the error: can't dup NilClass
on this line: #home = Home.find_for_authentication(params[:_id])
What is the problem?
The problem will be here:
Home.find_for_authentication(params[:_id])
I've never used database_authenticatable before (will research it, thanks!), so I checked the Devise docs for you
The method they recommend:
User.find(1).valid_password?('password123') # returns true/false
--
Object?
The method you've used has a doc:
Find first record based on conditions given (ie by the sign in form).
This method is always called during an authentication process but it
may be wrapped as well. For instance, database authenticatable
provides a find_for_database_authentication that wraps a call to
this method. This allows you to customize both database
authenticatable or the whole authenticate stack by customize
find_for_authentication.
Overwrite to add customized conditions, create a join, or maybe use a
namedscope to filter records while authenticating
The actual code looks like this:
def self.find_for_authentication(tainted_conditions)
find_first_by_auth_conditions(tainted_conditions)
end
Looking at this code, it seems to me passing a single param is not going to cut it. You'll either need an object (hence User.find([id])), or you'll need to send a series of params to the method
I then found this:
class User
def self.authenticate(username, password)
user = User.find_for_authentication(:username => username)
user.valid_password?(password) ? user : nil
end
end
I would recommend doing this:
#home = Home.find_for_authentication(id: params[:_id])
...
I'm attempting to display a users password along in his confirmation page sent by the Devise mailer. The confirmation page is the default
Welcome test0#test.com!
You can confirm your account email through the link below:
Confirm my account
However, I wish to have
Welcome test0#test.com!
Your password is currently DASADSADS
You can confirm your account email through the link below:
Confirm my account
How do I access the user object in the view? Do I need to override the mailer controller with a custom one? If so, how do I tell what the methods of the current mailer do (tried looking at documentation but can't find any clues)?
I noticed that #email and #resource are used in the view. Can I use any of these to access the current password in its unhashed form?
Note that I am sending this email manually with user.find(1).send_confirmation_instructions
Although this can be done, I would caution very strongly against doing so. Hashed passwords are specifically used so that the password cannot be recreated easily. Passing the original password back to the user will cause it to be sent back in plain text which sort of defeats the whole purpose. Also, shouldn't the user already know their password (they did type it in twice after all)?!?
To do this, you would need to capture the original (unhashed) password in the registration create action and send the email at that point (passing along the password). You can do this by overriding the sign_up method - you can do this in an initializer:
class Devise::RegistrationsController < DeviseController
def sign_up(resource_name, resource)
sign_in(resource_name, resource)
resource.unhashed_password = resource_params[:password]
resource.send_confirmation_instructions
end
end
Alternatively, you can derive a new controller from Devise::RegistrationsController and put this override code there (the recommended approach - but then again, this whole operation isn't really recommended). You'll need to add the unhashed_password accessor for this to work:
class User < ActiveRecord::Base
attr_accessor :unhashed_password
end
And then you can update your confirmation view (at app/views/devise/mailer/confirmation_instructions.html.erb) to contain this:
<p>Your password is currently <%= #resource.unhashed_password %></p>
Devise save password in encrypted form: You can decrypt it using,
Generate new migration:
$ rails g migration AddLegacyPasswordToUser legacy_password:boolean
invoke active_record
create db/migrate/20120508083355_add_legacy_password_to_users.rb
$ rake db:migrate
Using legacy_password method in following code you can decrypt your password:
class User < ActiveRecord::Base
...
def valid_password?(password)
if self.legacy_password?
# Use Devise's secure_compare to avoid timing attacks
if Devise.secure_compare(self.encrypted_password, User.legacy_password(password))
self.password = password
self.password_confirmation = password
self.legacy_password = false
self.save!
else
return false
end
end
super(password)
end
# Put your legacy password hashing method here
def self.legacy_password(password)
return Digest::MD5.hexdigest("#{password}-salty-herring");
end
end
You can just use request.request_parameters[:user][:password] to get the plain text password on the create or update action.
In a Rails 3.2 app I'm using Devise on a User model, with omniauth-twitter. A User has many Authentications.
Login is via email + password, or via Twitter.
I'm trying to modify the email_required method so that email is not required if a User is signing up with Twitter (Twitter does not provide email in its email response). The User will then be taken to a settings screen where he will need to set his email.
I'm having trouble working out what my condition should be here. I'm thinking something like
def email_required?
if user.sign_in_count == 1 && self.authentications.first.provider == "twitter"
false
else
super
end
end
but (1) the Authentication record is built but not created when this validation is passed, so not available; and (2) I want email to be required immediately after the User is created (i.e., the signin count may still be 1)
Another option may be to check if Twitter params are in the session, something like (pseudocode)
def email_required?
if session["devise.twitter_data"]
false
else
super
end
end
but accessing the session from a model doesn't seem right.
Would appreciate any suggestions or thoughts, I have a feeling I'm overlooking something obvious!