In the Rails Tutorial Chapter 8 it is tested that the 'remember me' checkbox is working.
In a real contest, if a user checks the 'remember me' checkbox in the login page, after login the create action of the sessions controller uses the remember(user) helper, which creates a remember_token for the user and update the remember_digest attribute in the user model (via the remember method in user.rb), then sets cookies[:user_id] = user.id and cookies[:remember_token] = user.remember_token.
The book says that in the test "Ideally, we would check that the cookie’s value is equal to the user’s remember token, but as currently designed there’s no way for the test to get access to it: the user variable in the controller has a remember token attribute, but (because remember_token is virtual) the #user variable in the test doesn’t".
The test is defined below:
def setup
#user = users(:michael)
end
test "login with remembering" do
log_in_as(#user, remember_me: '1')
assert_not_nil cookies['remember_token']
end
First of all, considering how cookies['remember_token'] is defined (cookies[:remember_token] = user.remember_token), if it is true that we cannot access the remember token attribute, I wonder how can we check that cookies['remember_token'] is not nil.
Although the fixtures do not define any remember token for user michael, the log_in_as test helper method is defined to post to the login_path correct values for params[:session], so I am wondering: aren't these values taken by the create action of the sessions controller? If this is the case, then the create action should do the same job as described above for the real contest: the remember(user) helper would create a remember_token for the user and we could check if cookies['remember_token'] = user.remember_token.
I do not understand why we should not be able to access user.remember_token.
#user is created through a fixture. The issue is, #remember_token is a virtual attribute, which means it does not map to a database column. It gets set only when the #remember function is called on a User instance, and when that instance dies, it dies with it (though a digest of it is saved in the database, and an encrypted version of it is saved in the user's cookies).
What you are doing there using the #log_in_as is that, first you create a User, and then in the #log_in_as you take that user's email address and password, and send it to the controller. The controller finds that user from the database using the email that you provided, and goes ahead and calls the #remember function on that instance.
As you can see, the #remember function has never been called on your #user instance, so it never received the remember_token. But through that controller action, a cookie is set and a digest is saved in the database. So what you test there is to check if the cookie is set or not.
If you want to be more meticulous, I guess another thing you can do is to check if the digest of the cookie is the same as the digest in the database.
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
I'm trying to test my User model's class method #registered_but_not_logged_in(email), which grabs the first user that matches the email that has a confirmed_at entry but has never logged in (which I'm counting with sign_in_count). I'm using rspec with Factorygirl, plus shoulda-matchers v2.8.
Here's the ruby:
def self.registered_but_not_logged_in email
self.with_email( email ).confirmed.never_signed_in.first
end
I've tested this in the Rails console and I know it works as expected so it's not a logic problem on that end, so I'm thinking I'm doing something wrong in my test:
describe User do
# create #user
describe ".registered_but_not_logged_in" do
it "returns a user that matches the provided email who is confirmed, but who has not yet signed in" do
#user.confirmed_at = 2.days.ago
#user.email = "fisterroboto5893#mailinator.com"
result = described_class.registered_but_not_logged_in("fisterroboto5893#mailinator.com")
expect(result).to be_instance_of(User)
end
end
In this example, result is nil. I know that this is a case of #user existing outside the database while the method is actively checking the DB, but I don't know how to handle this while using rspec/factorygirl. Any help is definitely appreciated!
So I'm not 100% sure why what I'm doing is working, but here's the solution that I stumbled across with the help of #nort and one of my coworkers:
it "returns a user that matches the provided email who is confirmed, but who has not yet signed in" do
#user.confirmed_at = 2.days.ago
#user.email = "fisterroboto5893#mailinator.com"
#user.sign_in_count = 0
#user.save!
expect(User.registered_but_not_logged("fisterroboto5893#mailinator.com")).to be_instance_of(User)
end
What I believe is happening is the save! is saving #user to the test database, which is otherwise completely unpopulated as I develop against a different DB. The issue of course being that we can't test data that doesn't exist.
As a bonus, note that expect().to... is the preferred convention for rpsec. Also, described_class I believe would totally work fine, but am preferring explicitness right now since I'm still learning this stuff.
I have User model and use Devise gem on it. In my application i have an admin user who can register other users (for example:clients). Then these users can log in by themselves with randomly generated password. (But no one can register by themselves, only admin can register users.)
When I am creating user from other user logged in, i.e admin user creates another user , i want to generate some random password, encrypt it, and save to database.
How to encrypt passwords, so that it will work with Devise authorization. I guess I have to use the same method as Devise?
I want something like that:
EDIT:
def create
#user = User.new(user_params)
# set #user.password to some random encrypted password
#user.save
end
So every created user will get some random password.
The reason i am asking this, is that i think that if my encryption/decription will not match what devise uses users will not be able to log in with their passwords, since when they log in their input is encrypted via devise's encryption.
If you are using Devise and you have :database_authenticable enabled, you don't need what you describe at all.
Devise encrypts automatically when it saves to the database and doesn't decrypt when it reads it back, however when you store it in the password field, it can be plain text, Devise will take care of it for you only when writing (so it will stay plain text until you save).
So in your controller to create new users you can just do the following:
def create
# I assume your form will pass a `params[:password]` in plain text too
#user = User.new(user_params)
#user.password_confirmation = params[:password]
#user.save
end
This should be enough for your purpose, don't need to match devise encryption
Update 1:
To generate a random password in addition, you can do something like:
require 'securerandom'
def create
# I assume your form will pass a `params[:password]` in plain text too
#password = SecureRandom.hex(16)
#user = User.new(user_params.to_h.merge(password: #password, password_confirmation: #password))
#user.save
# Remember to display `#password` in some way to the user
end
You should be having :database_authenticatable as one of the devise modules in your User model.
From Devise, it says
Database Authenticatable: encrypts and stores a password in the
database to validate the authenticity of a user while signing in. The
authentication can be done both through POST requests or HTTP Basic
Authentication.
I am new to ruby. I want to know when/where the Current user set. I know cookie will be generated for each URL request. And where the session details are stored? And where the current user set(in which file). Any one please explain briefly.
Hope you have a users table in your Rails application, so devise will automatically load all columns of users table in current_user.
It all depends on how you implement it. If you're using a library like Devise it has its own implementation, but usually such things are stored in encrypted Rails session store and on every request 'session' controller verifies visitor's cookie and only after that current_user is set to the User object from the session.
i prefer it in applicaton_controller..so that i can check where user_signed_in on every request and check the session ..if it exits then its ok else redirect_to login page..
for example in application_controller.rb
before_filter :check_current_user
def check_current_user
if current_user
#check if current user exists in our session
session[:current_user_id] = User.find(session[:current_user_id]).id
else
#if not ,then create new and set it to the session and return the current_user as u
session[:current_user_id] = User.create(:username => "guest", :email => "guest_# {Time.now.to_i}#{rand(100)}#example.com")
u.save!(:validate => false)
session[:current_user_id] = u.id
u
end
end
the above code is not perfect though..but i just wanted to show how current_user can be implemented to check current_user on every request using session and sets it in the session if there is no current_user as guest...
I've put together a basic application with user authentication using bcrypt-ruby and has_secure_password. The result is essentially a barebones version of the application from the Rails Tutorial. In other words, I have a RESTful user model as well as sign-in and sign-out functionality.
As part of the tests for editing a user's information, I've written a test for changing a password. Whereas changing the password works just fine in the browser, my test below is not passing.
subject { page }
describe "successful password change"
let(:new_password) { "foobaz" }
before do
fill_in "Password", with: new_password
fill_in "Password Confirmation", with: new_password
click_button "Save changes"
end
specify { user.reload.password.should == new_password }
end
Clearly, I'm misunderstanding some basic detail here.
In short:
1) Why exactly is the code above not working? The change-password functionality works in the browser. Meanwhile, rspec continues to reload the old password in the last line above. And then the test fails.
2) What is the better way to test the password change?
Edit:
With the initial password set to foobar, the error message is:
Failure/Error: specify { user.reload.password.should == new_password }
expected: "foobaz"
got: "foobar" (using ==)
Basically, it looks like the before block is not actually saving the new password.
For reference, the related controller action is as follows:
def update
#user = User.find(params[:id])
if #user.update_attributes(params[:user])
flash[:success] = "Profile Updated"
sign_in #user
redirect_to root_path
else
render 'edit'
end
end
For Devise users, use #valid_password? instead:
expect(user.valid_password?('correct_password')).to be(true)
Credit: Ryan Bigg
One not so satisfying solution here is to write a test using the #authenticate method provided by bcrypt-ruby.
specify { user.reload.authenticate(new_password).should be_true }
Granted this isn't a proper integration test, but it will get us to green.
Your answer (using authenticate) is the right approach; you should be satisfied with it. You want to compare the hashed versions of the passwords not the #password (via attr_accessor) in the model. Remember that you're saving a hash and not the actual password.
Your user in your test is an copy of that user in memory. When you run the tests the update method loads a different copy of that user in memory and updates its password hash which is saved to the db. Your copy is unchanged; which is why you thought to reload to get the updated data from the database.
The password field isn't stored in the db, it's stored as a hash instead, so the new hash gets reloaded from the db, but you were comparing the ephemeral state of #password in your user instance instead of the the encrypted_password.