For some mind-boggling reason, only one specific student keeps getting logged out of the rails app that I've created for my classroom. She can successfully log in to the first page, which shows correct information for her account. When she tries to click to the second page, she is returned to the home page with a "Please Log In" flash message. I've replicated this problem from several different computers. A hundred other students logged in on the same day; no other students experienced log in problems.
Following the path from the flash message leads me to the create method from my sessions controller. I've suspected for long time that I haven't properly handled the way that my app logs in teachers and students through two different models. So I would love to hear your insight on this model, even if you can't help me solve the title problem.
def create
user = User.find_by(email: params[:session][:email].downcase)
if user == nil
user = Student.find_by(username: params[:session][:email].downcase)
end
if user && user.authenticate(params[:session][:password])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
flash.now[:danger] = 'Invalid email/username/password combination'
render 'new'
end
end
Or maybe my current_user method from sessions_helper.rb isn't written properly.
def dryFindUser(user_id)
if session[:user_type] == "student"
Student.find_by(id: user_id)
else
User.find_by(id: user_id)
end
end
def current_user
if (user_id = session[:user_id])
#current_user ||= dryFindUser(user_id)
elsif (user_id = cookies.signed[:user_id])
user = dryFindUser(user_id)
if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
#current_user = user
end
end
end
# Returns true if the user is logged in, false otherwise.
def logged_in?
!current_user.nil?
end
# Forgets a persistent session.
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
I'm still pretty stumped why this is only happening to one single student, when all other students work fine.
EDIT. David mentioned that he didn't know what the forget method did. I added that to the session_helper above.
LuckyRuby mentioned that I didn't present enough code. Here's another section that I thought might be applicable:
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
# Confirms a logged-in user.
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
end
I don't think there's enough code presented to truly debug the issue. If you're not deadset on rolling your own auth code, I recommend going with something like devise and saving yourself the headache.
Related
I'm going through Michael Hartl's The Ruby on Rails Tutorial, Chapter 8.3 Logging Out Sessions and I don't understand how removing the session[:user_id] can remove the #current_user as well:
here is the SessionController:
class SessionsController < ApplicationController
def new
end
def create
user =User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in(user)
redirect_to user
else
#flash.now will only flash once - if a new request or view is rendered,the flash will go away now
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out
redirect_to root_path
end
end
and here is the SessionsHelper for the login and logout helpers:
module SessionsHelper
def log_in(user)
session[:user_id] = user.id
end
def current_user
#find the user if #user is not defined yet, or else, just keep the current user
#that way we dont have to do a database search every time current_user is called
#current_user ||= User.find_by(id: session[:user_id])
end
def logged_in?
!current_user.nil?
end
def log_out
session.delete(:user_id)
end
end
The way I understand, once #current_user is defined once logged in, shouldn't the variable still last even though the session[:user_id] has been removed since it is being set to itself?
#current_user ||= User.find_by(id: session[:user_id])
There was no action that I am aware of that removed the #current_user variable. But when I tested it during the debugger, I can see that once someone logs out, #current_user becomes nil.
Can someone explain the mechanics to me?
The session persists between requests. But the instance variable #current_user only persists for the length of one request. When the destroy action redirects to the root_path that is the start of a new request which will load the root page.
You may want to try this out so see that clearing the user_id out of the session doesn't clear out the instance variable:
def destroy
# Test code to initialize #current_user
current_user
Rails.logger.debug("#current_user before: #{#current_user.inspect}")
log_out
# Test code to check #current_user after updating session
Rails.logger.debug("#current_user after: #{#current_user.inspect}")
redirect_to root_path
end
And then check what ends up in log/development.log. #current_user will still be present after log_out but it will go away at the end of the request.
I'm learning Rails and I'm trying to restrict access to pages if a user hasn't logged in and to only allow them to view the login and sign up pages.
Currently, my code creates a session when a user logs in and clears it when the user logs out. I've got a Sessions helper so that I can check whether a user is logged in but I'm unsure how to redirect the user throughout the app if he/she's not logged in.
UPDATE:
As I posted the question, I managed to get something to work with a before_filter. Should I use a before_action or before_filter?
Do I need to copy the same method in all my controllers where I want to restrict access?
CODE:
/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
end
/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out
redirect_to root_url
end
end
/helpers/sessions_helper.rb
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
# Logs out the current user.
def log_out
session.delete(:user_id)
#current_user = nil
end
end
You can use a before_action. The rails guide has a nice section with an example on that:
class ApplicationController < ActionController::Base
before_action :require_login
private
def require_login
unless logged_in?
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url # halts request cycle
end
end
end
So on my local host, the sign in persists and the user is signed in. However, on Heroku, after signing in, it doesn't even recognize that the user is still signed in.
In the heroku logs, it starts
-A get request for log in
-SessionsController renders a new session
-A user is committed
-A POST request to start sessions occurs
-redirect occurs
-user sign in apparently isn't saved
-This seems like an issue with the cookie?
This is my Sessions Controller
class SessionsController < ApplicationController
def new
end
def create
user = User.authenticate(params[:email], params[:password])
if user
session[:user_id] = user.id
sign_in user
redirect_to root_url, :notice => "Logged in!"
else
flash.now.alert = "Invalid email or password"
render "new"
end
end
def destroy
sign_out
redirect_to posts_url, :notice => "Logged out!"
end
end
This is my SessionsHelper
module SessionsHelper
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
def signed_in?
!current_user.nil?
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
def current_user?(user)
user == current_user
end
def is_admin?
signed_in? ? current_user.admin : false
end
def sign_out
current_user.update_attribute(:remember_token,
User.encrypt(User.new_remember_token))
cookies.delete(:remember_token)
self.current_user = nil
end
end
In my user model I do save a remember_token before saving
before_save :create_remember_token
def create_remember_token self.remember_token = User.encrypt(User.new_remember_token)
Any ideas would be much appreciated!
Following your code step by step:
A User is created with no remember token.
The sign_in method is being called. Nil is saved to cookies as User has no remember token yet.
Your current_user method sets #current_user to nil because User.find_by(nil) returns nil, and doesn't raise an exception as User.find(nil) would.
While I'm not 100% sure this is the reason why your code is breaking (as I can't see what callbacks you've written), it certainly makes sense. If in your local environment you create a new user from scratch I assume that it'll break as well. The only place I see you defining a remember token for the user is in the sign_out method - if you were signed in while implementing the feature, signed out to test it, and signed back in, the code would seemingly work.
This issue can be fixed by using a callback to set the remember_token of the User on create via callback, or in the sign_in method.
As a side note, if you're following Hartl's implementation of sessions, I'd definitely revisit that as you made a fairly large error in how you handle the remember tokens. The encrypted token should be stored on your database, and the unencrypted one should be stored on the cookie. Then when using find_by, you should be encrypting that cookie to find the user in the database. As it currently stands you're saving the naked remember token in both the db and cookie, which poses security issues.
I am trying to redirect to the page from where I clicked login, but after logining in it doesn't redierect to previous page but stays on login page (although the user is already logged in).
Here is my code:
session_helper.rb
module SessionsHelper
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.encrypt(remember_token))
self.current_user = user
end
def redirect_back_or(default)
redirect_to(session[:return_to] || default)
session.delete(:return_to)
end
def store_location
session[:return_to] = request.fullpath
end
end
sessions_controller.rb
class SessionsController < ApplicationController
include SessionsHelper
def new
end
def create
user = User.find_by_username(params[:session][:username])
if user && user.authenticate(params[:session][:password])
cookies.permanent[:remember_token] = user.remember_token
#redirect_to root_url,:notice => "Logged in!"
redirect_back_or user
else
flash[:error] = 'Invalid email/password combination' # Not quite right!
render 'new'
end
end
def destroy
cookies.delete(:remember_token)
#session[:user_id] = nil
redirect_to root_url, :notice => "Logged out!"
end
end
I also tried to write in create function in sessions_controller.rb
redirect_to request.referer
but it doesn't work.
Am I missing something?
Thanks for your help!
The problem happens at store_location.
Though you havn't said in question, I guess you probably put this method in before_filter. So, no matter GET or POST or other request, the request hit this filter at first and store location.
Now, in this case, actually the user has two requests. One is to #new by GET, and the other is to #create by POST. In the later, his last request to #new was recorded as the going back location. So you'll see you always go back to #new :)
The solution is to filter the location to be stored.
def store_location
disable_pattern = /\A\/user*/
session[:return_to] = request.fullpath unless request.fullpath ~= disable_pattern
end
This pattern could solve current problem but not exclusive. In practice you may see even JS/JSON requests has been recorded, so you may need to add more restrictions according to the specific case. For example, only apply before_filter on #show or #index, use white list, etc.
I think request.referer may not have worked because of a typo in the method. It should be request.referrer.
I'm following the tutorial on http://ruby.railstutorial.org
Specifically, chapter 9 (9.2.3)
http://ruby.railstutorial.org/chapters/updating-showing-and-deleting-users#top
I've managed to get the part when a user will get prompted to login when accessing a restricted page then be redirected back to the restricted page after successfully logging in.
I'm trying to get it so that after one redirects to the protected page, the next login attempt will direct back to the main user profile page, however, session.delete(:return_to) doesn't appear to be working and the user is repeatedly directed back to the originally saved protected page. Here's my code:
My session Controller:
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by_email(params[:session][:email])
if user && user.authenticate(params[:session][:password])
sign_in user
redirect_back_or user
# Sign the user in and redirect to the user's show page.
else
# Create an error message and re-render the signin form.
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
...
end
My session helper:
module SessionsHelper
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
def signed_in?
!current_user.nil?
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
def current_user?(user)
user == current_user
end
def sign_out
self.current_user = nil
cookies.delete(:remember_token)
end
def redirect_back_or(default)
redirect_to(session[:return_to] || default)
session.delete(:return_to)
end
def store_location
session[:return_to] = request.url
end
end
Any help you can give would be brilliant! It seems like session.delete() simply isn't working.
The following block solved it. Nothing else needs to change.
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in." #unless signed_in?
end
end
When I did the tutorial, my code had the first lines of the SessionsController#create method as just:
user = User.find_by_email(params[:email])
if user && user.authenticate(params[:password])
But, I can see that the corresponding code in the book has changed to:
user = User.find_by_email(params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
I attempted to use that new code in my sample_app, but most of my tests ended up failing. So, for you, I guess test adding the downcase method to your params[:session][:email] call first, and if that doesn't work, try substituting the lines out for the session-less code above and see if it works.
Update
After looking at your code, as far as I can tell, these are your problems:
You're calling session.delete(:return_to) in SessionsController#create for some reason. This line can be removed:
app/controllers/sessions_controller.rb
def create
user = User.find_by_email(params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# session.delete(:return_to)
sign_in user
# ...
#...
end
Both lines of code in your UsersController#signed_in_user method need to be put in the unless block, not just the call to redirect_to:
app/controllers/users_controller.rb
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in." #unless signed_in?
end
end
If you make these changes and run your tests, you'll still have a Nokogiri::XML::XPath::SyntaxError: on your call to
spec/requests/authentication_pages_spec.rb
it { should have_exact_title('title', text: full_title('')) }`
but I'm assuming this is a custom matcher you're planning to work on. If not and it's a mistake, remove it and all your tests will pass.