Rails Tutorial: Short-Circuit Evaluation in section 8.4.4 - ruby-on-rails

I'm going in circles with a bit of code in section 8.4.4 ("Two Subtle Bugs") in the 3rd edition of Michael Hartl's Rails tutorial. (Link to this section of the text: https://www.railstutorial.org/book/log_in_log_out#sec-two_subtle_bugs[1])
Specifically I'm confused about the following text/code:
"The second subtlety is that a user could be logged in (and
remembered) in multiple browsers, such as Chrome and Firefox, which
causes a problem if the user logs out in one browser but not the
other. For example, suppose that the user logs out in Firefox, thereby
setting the remember digest to nil (via user.forget in Listing 8.38).
This would still work in Firefox, because the log_out method in
Listing 8.39 deletes the user’s id, so the user variable would be nil
in the current_user method:
def current_user
if (user_id = session[:user_id])
#current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
#current_user = user
end
end
end
As a result, the expression
user && user.authenticated?(cookies[:remember_token])
returns false due to short-circuit evaluation."
For this question, let's stick with Firefox and not worry about the second browser bug. Hartl seems to be saying the following:
The log_out method sets the remember digest to nil in the database.
The log_out method deletes the user_id stored in both the session
and cookie.
A subsequent call to the current_user method from within the same browser would not raise an error because "the user variable would be nil in the current_user method." This would cause short-circuiting of the expression user && user.authenticated?(cookies[:remember_token]).
My questions is how this could ever happen. If the log_out method works as stated, shouldn't the line elsif (user_id = cookies.signed[:user_id]) be false on subsequent calls? The elsif block wouldn't run and the user variable would never be set. In fact both conditionals in the current_user method would be false and the method would return nil. There would be no short-circuiting based on the user variable.
Can the short-circuit evaluation that he describes take place?

You are correct that the short-circuit does not occur since
elsif (user_id = cookies.signed[:user_id])
does not run and the user variable would never be set IN FIREFOX., since the cookie is delted in Sessions Helper:
# Forgets a persistent session.
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
# Logs out the current user.
def log_out
forget(current_user)
session.delete(:user_id)
#current_user = nil
end
However, that doesn't change Hartl's main point that the user was also logged in via Chrome, which is where the error occurs.

Hartl has recently edited that section of the book (as of early March 2015), and now the current way he describes it doesn't seem to have this error.

Related

Session Helper Methods in Michael Hartl Tutorial Chapter 8

I'm going through Michael Hartl tutorial on Ruby on Rails and I'm having trouble understanding some logic. Note that the logic works, it's just not resonating with me on what's actually happening.
In this chapter we're logging in users and creating a session. Here the helper methods:
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
# Returns the current logged-in user (if any).
def current_user
#current_user ||= User.find_by(id: session[:user_id])
end
# Returns true if the user is logged in, false otherwise.
def logged_in?
!current_user.nil?
end
end
Depending on whether the user's logged on or not we change the navigation with this conditional:
<% if logged_in? %>
do something....
<% else %>
do something else...
<% end %>
As I understand it, the code checks to see if the user is logged in by calling the logged_in? method. The logged_in? method calls the current_user method to see if it's nil. If it is nil it returns false, if it's not nil it returns true. As a test I tried to change the logged_in? method to the following:
def logged_in?
!#current_user.nil?
end
When I run this method for some reason, after I log in with credentials that are authenticated, #current_user returns nil. Why is this? Note this works if I change it back to the original logged_in? method where I'm just calling the method current_user.
This is not a direct answer to your question since you have that figured it out while I was trying to answer. But I want to clarify on some points.
In rails, in fact most of the web application, we track a user's log in state in server's session. the log_in method in your code does that.
Then when the new request comes in to a controller that requires authentication, we check the session if there is a stored user. If it exists then the request is authenticated, else it's unauthenticated. So, logged_in? method's actual responsibility is to check the session.
However, it is quite common that we want to access the authenticated user's attributes in the controller and/or views. So we set an #current_user variable on the controller so that you can access the User object of authenticated user. Again, using an instance variable directly is not a good practice. So we wrapped it in the current_user method.
Then you might ask, why don't we store the whole user object in session? Because it is bad to store much in session(see here). So, we just store the id and use it to get the user from db.
Here is where the ||= part comes in. ||= caches the result of db. Otherwise, we would be hitting db every time we call current_user method.
Hope this clarifies a bit on what's actually happening.
As I was formulating my question I figured this out. In the later case
def logged_in?
!#current_user.nil?
end
#current_user isn't set yet because the current_user method was never called. To test, I changed the method to the following:
def logged_in?
current_user
!#current_user.nil?
end
where the method was called first and then #current_user was evaluated. It worked without issue. The original method works because current_user returns #current_user to the logged_in? method as either a user object or nil (#current_user is set as the last line in the method -- it's the only line, so it's retuned implicitly to the logged_in? method).

Why is the variable nil even if I don't set it to nil?

I don't understand why current_user is nil after deleting user_id from session. When the current_user function is called wouldn't it return the old current_user value again?
#Returns the current logged-in user (if any).
def current_user
#current_user ||= User.find_by(id: session[:user_id])
end
# Logs out the current user.
def log_out
session.delete(:user_id)
# #current_user = nil
end
I assume these methods are defined in a controller. Controller instance variables only have a lifespan of a single HTTP request. Session values last longer, but you've deleted the session value that matters here.
When the next request comes in, #current_user needs to be set again, and since session[:user_id] is nil, you are essentially calling User.find_by(id: nil), which will return nil.

Signin Sessions Helpers in Rails

If you watch over any of Ryan Bates Authentication related Railscasts you'll see a recurring theme when creating sigin/signout functionality and I wanted to understand that a little bit more clearly.
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
For example usually in a session controller the create action will contain an assignment to the sessions hash such as session[:user_id] = user.id given that the variable user is set to an Active Record Object.
The above helper method is then used throughout the views to find the current signed in user.
However when signing out the destroy action contains only the line session[:user_id] = nil
My question is wouldn't #current_user also be needed to set to nil since it would be set to the previous User that was signed in?
Typically after setting session[:user_id] = nil your controller will return so #current_user still being active doesn't matter. You have to remember that #current_user only exists for that request, the next request that comes through is a new instance of that controller class.
You are right that if you did something like this:
def destroy
session[:user_id] = nil
logger.debug current_user.inspect # Current user is still set for this request
redirect_to admin_url, notice => "You've successfully logged out."
end
You would see the user information in the log file, but normally you are doing a redirect right after clearing the session[:user_id] so that controller instance is done.

Rails - Why use self.current_user = user in sign_in method

I have finished the Ruby on Rails Tutorial by Michael Hartl. I know some basic ideas about instance variable, getters and setters.
The sign_in method is here
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
Now I'm stuck at this line
self.current_user = user
I found this related question, but I still don't get it.
After sign_in, the user will be redirected to another page, so #current_user will be nil. Rails can only get current_user from cookie or session, then set #current_user, so that it doesn't need to check cookie or session again in current request.
In sign_out method
def sign_out
self.current_user = nil
cookies.delete(:remember_token)
end
For the same reason, why do we need self.current_user = nil since the user would be redirected to root_url?
Here's the code for getter and setter
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
You are right that the #current_user is not set after the redirection.
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
This statement helps avoid repeated calls to the database, but is only useful if current_user variable is used more than once for a single user request. Consequently, setting the current user is only helpful during a single call.
Similarly, setting the current user to nil and removing the token from cookies during sign_out ensures that subsequent processing will take the signing out into account. Otherwise, there is a risk of other methods referring current user variable and thinking that the user is still logged in.
You have a full explanation on the next section of the book
Current User
Basically when you do self.current_user= you invoque the method 'def current_user= ()' this is the setter, you will be probably not only assigning the #current_user variable here but also keeping some reference in the cookies or session for future reference. In the same way you will probably be creating an accessor that will look like
def current_user
#current_user ||= get_user_from_cookies
end
In order to have accesible the current user. I think you just went to fast and the book is trying to go step by step for users not familiarised with web dev
I believe you're right in saying that for the code you've written so far it doesn't make much difference.
However it doesn't make sense for your sign_in/sign_out methods to know the ins and outs of how users travel through you application. It would be very brittle (and not its business) if it assumed that the only thing your application did after login was to redirect the user to the root page.
You could be doing all sorts of things, from collecting audit data (record every time someone logs in for example) to redirecting them to a different page depending on the users preferences or some other attribute of the user.

Rails Tutorial — 9.3.3 Current_User

So I'm following the Rails Tutorial, and I've gotten to the portion where we want to sign a user in with a sign_in SessionHelper.
Question 1:
module SessionsHelper
def sign_in(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
current_user = user
end
def current_user=(user) #set current_user
#current_user = user
end
def current_user #get current_user
#current_user
end
What I'm having difficulty with is the part that reads:
The problem is that it utterly fails to solve our problem: with the code the user's signin status would be forgotten: as soon as the user went to another page.
I don't understand how this is true? I read on and understand the added code makes sure #current_user is never nil. But I'm not seeing how current_user would revert to nil if we just established it in 5th line.
Question 2:
The updated code reads as such:
module SessionsHelper
def sign_in(user) #in helper because used in view & controller
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
current_user = user
end
def current_user=(user) #set current_user
#current_user = user
end
def current_user #get current_user
#current_user ||= user_from_remember_token #<-- short-circuit evaluation
end
private
def user_from_remember_token
User.authenticate_with_salt(*remember_token) #*=use [] instead of 2 vars
end
def remember_token
cookies.signed[:remember_token] || [nil, nil]
end
end
In the remember_token helper, why does it use cookies.signed[] instead of cookies.permanent.signed[] & why doesn't it use ||= operator we just learned about?
Question 3:
Why do we need to authenticate_with_salt? If I authenticate & sign_in can see the id & salt attributes from the user who was passed to it, why do we need to double_check it? What kind of situation would trigger a mixup?
Remember that instance variables like #current_user are only set for the duration of the request. The controller and view handler instances are created specifically for rendering once and once only.
It is often easy to presume that because you've set a variable somewhere that it will continue to work at some point in the future, but this is not the case. To preserve something between requests you need to store it somewhere, and the most convenient place is the session facility.
What's missing in this example is something along the lines of:
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
Generally it's a good idea to use the write accessor to map out the functionality of the sign_in method you've given as an example:
def current_user=(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
#current_user = user
end
It's odd that there is a specific "sign in" method when the act of assigning the current user should be the same thing by implication.
From a matter of style, though, it might be more meaningful to call these methods session_user as opposed to current_user for those situations when one user is viewing another. "Current" can mean "user I am currently viewing" or "user I am currently logged in as" depending on your perspective, which causes confusion. "Session" is more specific.
Update:
In response to your addendum, the reason for using cookies to read and cookies.permanent to assign is much the same as using flash.now to assign, and flash to read. The .permanent and .now parts are intended to be used when exercising the assignment operator.

Resources