In Michael Hart's book this code is used to implement authentication:
module SessionsHelper
def sign_in(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt] #permanent
# -alternatively-
# cookies.signed[:remember_token]={
# :value => [user.id, user.salt],
# expires => some_time.from_now
# }
current_user = user
end
def current_user=(user)
#current_user = user
end
def current_user
return #current_user ||= user_from_remember_token
end
private
def user_from_remember_token
#passes array of length two as a parameter -- first slot contains ID,
#second contains SALT for encryption
User.authenticate_with_salt(*remember_token)
end
def remember_token
#ensures return of a double array in the event that
#cookies.signed[:remember_token] is nil.
cookies.signed[:remember_token] || [nil,nil]
end
end
It does it's job very well, I can either log in for an infinite amount of time, or for a limited period of time as I wish.
But it has a downside, cookies are stored on the client and they dont go away even if the browser is closed
Now I was wondering, since rails sessions get destroyed after a browser is closed, how would I combine them and the cookies presented here to implement authentication with the following characteristics:
-- if a user logs in,
the data should be stored in a session
so that after a user closes his browser they get logged of
-- if a user logs in, with a 'remember me' checkbox selected
their data should be stored in a cookie with a long expiration date
What would be a take on this that remains secure and simple?
I googled on the web and found nothing recent enough (Rails 3) that
could guide me in the right direction.
I was thinking of creating 2 separate modules for sessions and cookies and fire their respective sign_in methods in the controller whether a remember_me param was present or not, but that would seem a lot of duplication.
PS I am not looking into any authentication gems to provide this functionality, id prefer to implement it on my own.
Thanks
Related
This question already has answers here:
Session not destroyed when closing browser - RailsTutorial.org
(2 answers)
Closed 5 years ago.
It's my understanding that the default behavior of Rails, when storing a session e.g. session[:id] = 1, is that it will save the id to a cookie, which will expire when the user closes the browser window. However, in my app, when I close (exit out) the browser and restart it, the browser still 'remembers' me as being logged in. Here is my controller code:
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
log_in user
redirect_to user
else
flash.now[:notice] = "Invald email / password combination"
render 'new'
end
end
def destroy
end
end
and my helper file:
module SessionsHelper
def log_in(user)
session[:user_id] = user.id
end
def current_user
#current_user ||= User.find_by(id: session[:user_id])
end
def logged_in?
!current_user.nil?
end
def user_name
#current_user.first_name ? (#current_user.first_name + " ") : nil
end
end
I have nothing in the application controller nor did I ever mess with the initializers or config files regarding the session. What could be causing my session to persist and not expire the cookie?
Withing config/initializers/session_store.rb file add
Rails.application.config.session_store :cookie_store, key: '_app_session', expire_after: nil
Instead of
Rails.application.config.session_store :cookie_store, key: '_app_session'
Please have a look at here for more info.
The solution is to simply turn off cookies or reset cookies, which results in a new session being created with each visit. The solution looked like this:
On client side, write a beforeunload event handler.
From that handler make an AJAX request to logout URL, in your case it
might be a special view that deletes the cookie (see Rails docs on
cookies).
The AJAX request has to be synchronous to make sure beforeunload
event handler waits for its completion.
refer this https://codedecoder.wordpress.com/2013/12/04/clear-session-or-cookie-browser-close-ruby-jquery/ it might also help
I have a very simple rails app that I rolled my own authentication for, based on the rails tutorial book by Michael Hartl (http://www.railstutorial.org/book/modeling_users). The app is a content management system for an equally simple iOS app. I am aware that devise is very popular, but I really do not think it is necessary for this project. I want to be able to link my iOS app to my rails app, but everywhere I look the only advice I can find is how to do it with devise. All I want to do is have the user be presented with a login screen so they can establish a session and then I can handle all of the permission logic on the rails end of things. Here are a few things to give you an idea of my current authentication scheme:
My session controller:
class SessionsController < ApplicationController
def new
end
##
#Use the email in the nested hash to find the right user
#Check to make sure that the user authenticates with the given password
##
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
sign_in user
redirect_back_or root_url
else
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
current_user.update_attribute(:remember_token,User.digest(User.new_remember_token))
cookies.delete(:remember_token)
self.current_user = nil
session.delete(:return_to) #not sure if this should really be here or if better way to fix bug
redirect_and_alert(root_url, "User Successfully Logged Out!",:success)
end
end
Sessions helper:
module SessionsHelper
##
#set the remember token for the user
#make the cookie reflect that token
#update the users remember token column
#set the user being passed in as the current user
##
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.digest(remember_token))
self.current_user = user
end
#set the current user
def current_user=(user)
#current_user = user
end
#Helper current user method
def current_user
remember_token = User.digest(cookies[:remember_token])
#current_user ||= User.find_by(remember_token: remember_token)
end
#Is the requesting user the current user
def current_user?(user)
user == current_user
end
#Is the user signed in?
def signed_in?
!current_user.nil?
end
#Store user request info for friendly forwarding
def redirect_back_or(default)
redirect_to(session[:return_to] || default)
session.delete(:return_to)
end
#Store user request info for friendly forwarding
def store_location
session[:return_to] = request.url if request.get?
end
#Authorization
def signed_in_user
store_location
redirect_to signin_url, notice: "Please sign in." unless signed_in?
end
def super_user
redirect_and_alert(root_url,
"You are not allowed to do that. Contact the admin for this account.",
:error) unless (current_user.role.id == 1)
end
def super_user_or_admin
redirect_and_alert(root_url,
"You are not allowed to do that. Contact the admin for this account.",
:error) unless (current_user.role.id == 1 || current_user.role.id == 2)
end
def is_super_user
current_user.role.id == 1
end
def is_admin
current_user.role.id == 2
end
def is_regular_user
current_user.role.id == 3
end
end
You could go for a token-based system rather than a session-based system.
Create an attribute called authentication token for every user. This token can be generated and assigned during sign-up. The token itself can be generated using simple techniques such as SecureRandom.hex(n), where n is the length of the random hex number generated.
After sign-in/sign-up from the app, send the authentication token in the response from the server. You can then have the iOS app send the token along with every subsequent request to the server.
Have the server check the token every time a controller is hit with a request. This can be achieved using the before_filter. So a sample controller could look like this:
before_filter :authenticate_user
def authenticate_user
# assuming the parameter sent from the app is called auth_token
auth_token = params[:auth_token]
user = User.find_by_authentication_token(auth_token)
if user.nil?
# what to do if user does not exist
else
# what to do if user exists
end
end
Typically for mobile applications tokens are used instead of sessions. The token is essentially just a string that your Rails app will give each mobile device to verify it has access.
Step 1 is to have your mobile app authenticate against the Rails app (this can be through a custom email/password login or through a 3rd party like Facebook).
Have your Rails app send a token to the mobile application
Store this token in UserDefaults on iOS (help link: http://www.learnswiftonline.com/objective-c-to-swift-guides/nsuserdefaults-swift/)
Whenever your app makes a request (or websocket connection, etc.) to the Rails app, send the token (usually as a header).
Whenever the iOS app opens, check for the token in UserDefaults. If it is there, then you skip having the user authenticate again.
If you want to get really fancy, the Rails app should only accept a token once and issue new-tokens on each request. So your iOS app would send a token on each request and then grab a new one with each response. This is a more secure practice so someone does not grab a token and mess with your Rails app.
This diagram covers the high-level process (it uses FB login for initial authentication, but you can plug yours/an alternate system in instead): http://www.eggie5.com/57-ios-rails-oauth-flow.
In my Rails app I have a sessions_helper like this:
def sign_in(user)
token = User.generate_token
if params[:remember_me]
cookies.permanent[:remember_token] = token
else
cookies[:remember_token] = token
end
self.current_user = user
end
def sign_out
cookies.delete(:remember_token)
self.current_user = nil
end
Now, for security reasons, it would be nice to automatically sign out a user if s/he has been inactive for, say, 30 minutes.
What is the best way to achieve this in Rails?
I was thinking about using a prepend_before_filter in my ApplicationController that will extend the lifetime of the user's cookie by another 30 minutes whenever the user does something within the application.
But is this approach recommended from a security point-of-view?
Thanks for any input.
I use a similar method, but I'm using sessions not cookies. I have an 'authenticate' action in my application controller which is triggered by a before_filter for each controller, this action pokes the 'session[:accesses]' to add 1 which in turn updated the 'updated_at' column in the database. I then have a cron job that removes any sessions from the database that haven't been updated in 20 minutes.
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.
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.