I am working through Hartl's Ruby on Rails Tutorial (3rd Ed.) and just completed Chapter 8. One of the goals of the chapter is to have the application sign users out by deleting the session’s user id and remove the permanent cookie from the browser. However, I've logged out of the app and then typed http://localhost:3000/users/1 in the browser and have been taken back to the profile for user 1. Is this a security issue?
The log in screen:
Logged in:
Logged out and directed back to the home page:
Now, here is where I'm confused. If I type in users/1 in the browser's address bar, I seem to be taken back to user #1's page, although the upper right corner doesn't show me as logged in.
Does anybody think that this is a security issue?
Here is some of the code related to the app. Am I missing something?
users_controller.rb
class UsersController < ApplicationController
def show
#user = User.find(params[:id])
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
log_in #user
flash[:success] = "Welcome to the Sample App!"
redirect_to #user
else
render 'new'
end
end
def edit
#user = User.find(params[:id])
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
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
params[:session][:remember_me] == '1' ? remember(#user) : forget(#user)
redirect_to #user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end
sessions_helper.rb
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
# Remembers a user in a persistent session.
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
# Returns the user corresponding to the remember token cookie.
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
# 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
# Logs out the current user.
def log_out
forget(current_user)
session.delete(:user_id)
#current_user = nil
end
end
user.rb
class User < ActiveRecord::Base
attr_accessor :remember_token
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
# Returns the hash digest of the given string.
class << self
def digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
# Returns a random token.
def new_token
SecureRandom.urlsafe_base64
end
end
# Remembers a user in the database for use in persistent sessions.
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
# Returns true if the given token matches the digest.
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
# Forgets a user.
def forget
update_attribute(:remember_digest, nil)
end
end
Thanks.
No. At this point there's no security issue. You're issuing a GET request for /users/1 and Rails delivers that. There is nothing preventing that page from being shown.
As sevenseacat explained in a comment above, there are no access restrictions there.
Related
I have been reading about Good Manners of Rails, and there are many articles like this one:
http://codefol.io/posts/Where-Do-I-Put-My-Code/
and seems like helpers should only be used for views. I don't understand well why, but if that would be the case and this is my sessions_helper.rb file:
module SessionsHelper
def log_in(user)
session[:user_id] = user.id
end
def logged_in?
!current_user.nil?
end
def log_off
forget(current_user)
session.delete(:user_id)
#current_user = nil
end
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&.authenticated?(cookies[:remember_token])
log_in user
#current_user = user
end
end
end
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_digest
end
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
end
and this is my model user.rb
class User < ApplicationRecord
has_many :post
attr_accessor :remember_token, :activation_token
before_save { self.email = email.downcase }
before_create :create_activation_digest
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i.freeze
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
# Returns the hash digest of the given string.
def self.digest(string)
Digest::SHA1.hexdigest(string.to_s)
end
# Returns a random token.
def self.new_token
SecureRandom.urlsafe_base64
end
# Remembers a user in the database for use in persistent sessions.
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
# Returns true if the given token matches the digest.
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
# Forgets a user.
def forget
update_attribute(:remember_digest, nil)
end
# Creates and assigns the activation token and digest.
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
what should I do to refactor it in a good way to be correct?
sorry If I am requesting too much help, but it is just that I am learning Rails and I don't understand this part very well.
That article is really just states the authors opinion. AbstractController::Helpers actually has helper method thats used to specifically include helpers into the controller so its hardly an officially endorsed opinion.
It can be debated endlessly exactly where exactly this peice of code belongs but since you're going to be using it in both the view and controller I would say just put it in app/helpers and be done with it. It makes a lot more sense than placing it somewhere else and then manually including it in the view context.
However this code should probally be split into two modules as only part of it makes sense to use from the view.
module SessionsHelper
def logged_in?
!current_user.nil?
end
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&.authenticated?(cookies[:remember_token])
log_in user
#current_user = user
end
end
end
end
# app/controllers/concerns/authenticable.rb
module Authenticable
def log_in(user)
session[:user_id] = user.id
end
def log_off
forget(current_user)
session.delete(:user_id)
#current_user = nil
end
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_digest
end
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
end
To satisfy my supervisor (or code reviewer), I had to copy/paste all session helper code on every single controller file, since copying it in application_controller was not enough.
The specific code below from the book using Rails 4
I believed in rails 5 required strong parameters that may cause error in Rails 5.
Please anyone could share me. How to apply permit and require for the following code)
From model user.rb
def remember
remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
error (in rails 5)
NoMethodError in SessionsController#create
undefined method `update_attribute' for #<Class
Here what I did.
def remember
remember_token = User.new_token
update_attribute(:remember_digest, User.digest(params:[remember_token]))
end
error:
NoMethodError in SessionsController#create
undefined method `update_attribute' for #<Class:0x007fd23b30a790> Did you mean? _default_attributes
Tobe clear: those method called from sessionshelper.erb
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
# Remembers a user in a persistent session.
def remember(user)
User.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
# Returns the user corresponding to the remember token cookie.
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
# 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
# Logs out the current user.
def log_out
forget(current_user)
session.delete(:user_id)
#current_user = nil
end
end
user.rb
class User < ApplicationRecord
attr_accessor :remember_token, :activation_token, :reset_token, :remember_digest
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
class << self
# Returns the hash digest of the given string.
def digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
def remember
remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
# Returns a random token.
def new_token
SecureRandom.urlsafe_base64
end
end
# Returns true if the given token matches the digest.
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
# Forgets a user.
def forget
update_attribute(:remember_digest, nil)
end
def activate
update_attribute(:activated, true)
update_attribute(:activated_at, Time.zone.now)
end
end
sessionscontroller.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
remember user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out
redirect_to url_for(:controller => :sessions, :action => :new)
end
end
The issue is in the helper, the line
You are calling the remember method with the User class, so it is showing undefined method for class. Instead call with user object
User.remember
should be,
user.remember
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
You should be calling update_attribute on the user
try
current_user.update_attribute(a: b)
in your SessionsController#create
update_attributes is an instance method not a class method, so first thing you need to call it on an instance of User class.
like:
#user.update_attributes(:remember_digest, User.digest(params:[remember_token]))
Try this once.
i have a similar implementation,maybe this might help you
in user.rb
def create_confimation_token
generate_token(:confirmation_token)
update_attribute(:expiration,Time.zone.now + 2.days)
save!
end
def generate_token(column)
begin
self[column] = SecureRandom.urlsafe_base64
end
end
u should call update_attributes for a user object.
helper.rb
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
model.rb
def remember
remember_token = new_token
self.update_attributes(:remember_digest, digest(remember_token))
end
It worked Now
What happened are the
class << self in user.rb
cause following method not working.
def remember
remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
User.remember in SessionsHelper should be
user.remember
Thanks everyone
one my my tests receiving the following error in Chapter 8 of Hartl's Rails Tutorial.
> 1) Error:
> UsersLoginTest#test_login_with_valid_information_followed_by_logout:
> NoMethodError: undefined method `forget' for #<Class:0x000000079be3b0>
> app/helpers/sessions_helper.rb:30:in `forget'
> app/helpers/sessions_helper.rb:37:in `log_out'
> app/controllers/sessions_controller.rb:18:in `destroy'
> test/integration/users_login_test.rb:40:in `block in <class:UsersLoginTest>'
I have tried copying and pasting code exactly as it is in the tutorial, but it doesn't seem to be solving the issue.
Here is my Sessions Helper
module SessionsHelper
def log_in(user)
session[:user_id] = user.id
end
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
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
def logged_in?
!current_user.nil?
end
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
end
And my sessions controller is below
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
remember user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end
Any suggestions would be very much appreciated!
Here we can see the importance of really looking closely at the error message. It says there is a "No Method Error". OK. So we're doing something with the Users and there's no method. A user is a modeled thing, so let's look at app/models/user.rb:
class User < ActiveRecord::Base
attr_accessor :remember_token
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
# Returns the hash digest of the given string.
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
# Returns a random token.
def User.new_token
SecureRandom.urlsafe_base64
end
# Remembers a user in the database for use in persistent sessions.
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
# Returns true if the given token matches the digest.
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
# Forgets a user.
def forget
update_attribute(:remember_digest, nil)
end
end
we see that at the very end, there is a method being created called "forget"
def forget
#something
end
That opens up the method to calls from other controllers and helpers, which you've already created in the Sessions section.
I'm following Michael Hartl's book: "Ruby on Rails Tutorial 3". I have reached section 8.4.1 where he says:
newly logged in users are correctly remembered, as you can verify by
logging in, closing the browser, and checking that you’re still logged
in when you restart the sample application and revisit the sample
application.
when I go through these steps I'm not logged in i.e: I can log in but when I restart the browser and revisit the app I'm not logged in as it is supposed to be.(I'm using localhost:3000)
If you want, you can even inspect the browser cookies to see the
result directly
This one, however, works. I can verify that the cookie is saved successfully.
I have gone through this chapter several times, making sure that I'm following the exact same steps but still no luck.
Also the test is not red, I can logout at any time. I really don't know what to do. I have been trying several things but no luck. Excuse me if this is a simple question as I'm new to rails.
Edit: you can view the chapter here: Rails tutorial 3
Edit 2:
By adding some more tests, I can find that the problem is that current_user is returning nil (note: the problem is in the cookies not in the sessions):
session_helper.rb
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
# Remembers a user in a persistent session.
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
def forget user
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
# Returns the user corresponding to the remember token cookie.
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
# 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
forget current_user
session.delete(:user_id)
#current_user = nil
end
end
models/user.rb
class User < ActiveRecord::Base
attr_accessor :remember_token
before_save { self.email.downcase! }
validates :name, presence: true, length: {maximum: 50}
Valid_email_regex = /\A[\w+\-.]+#[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true, length: {maximum: 255},
format: {with: Valid_email_regex},
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: {minimum: 6}
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
def User.new_token
SecureRandom.urlsafe_base64
end
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(:remember_token))
end
def forget
update_attribute(:remember_digest, nil)
end
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
end
session_controller
# logging in a user by email and logging them out
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
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_to user
else
flash.now[:danger] = "Invalid email/password combination"
render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end
Any help is appreciated.
1) The ONLY difference I can see with my own code, is in the models/user.rb
in before_save I think you either do
{ self.email.downcase }
or
{ email.downcase! }
With this, I could not reproduce your problem though. And I don't see how it could affect the log in persistence.
2) The code you posted has implemented the 'remember me'-checkbox from 8.5
So, when you signed up, did you check the checkbox?
I'm working my way through Michael Hartl's Rails Tutorial and I am currently on chapter 8, in section 8.55. I am trying to trouble shoot a failure that I'm receiving via that test, which I can't figure out where to solve.
FAIL["test_current_user_returns_right_user_when_session_is_nil", SessionsHelperTest, 0.05582] test_current_user_returns_right_user_when_session_is_nil#SessionsHelperTest (0.06s)
--- expected
+++ actual
## -1 +1 ##
-#<User id: 584273342, name: "Kyle Example", email: "kyle#example.com", created_at: "2014-12-27 20:09:35", updated_at: "2014-12-27 20:09:35", password_digest: "$2a$04$yK7dSRppfGCqCbrXXZ34meDB2jEulxy9BDVNH32qLDz...", remember_digest: "$2a$04$GT65nJ.fE90LasXpaT1HruCChf.hl.4fvMrtx2iV48V...">
+nil
test/helpers/sessions_helper_test.rb:11:in `block in <class:SessionsHelperTest>'
My test code is using the following code.
require 'test_helper'
class SessionsHelperTest < ActionView::TestCase
def setup
#user = users(:kyle)
remember(#user)
end
test "current_user returns right user when session is nil" do
assert_equal #user, current_user
assert is_logged_in?
end
test "current_user returns nil when remember digest is wrong" do
#user.update_attribute(:remember_digest, User.digest(User.new_token))
assert_nil current_user
end
end
SessionsHelper module
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
#Remembers a user in a persistent session.
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
#Returns the user corresponding to the remember token cookie.
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
#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
def log_out
session.delete(:user_id)
#current_user = nil
end
# 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
end
User model
class User < ActiveRecord::Base
attr_accessor :remember_token
before_save { email.downcase! }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
# Returns the hash digest of the given string.
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
#Returns a random token.
def User.new_token
SecureRandom.urlsafe_base64
end
#Remembers a user in the database for use in persistent sessions
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
#Returns true if the given token matches the digest.
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember.digest).is_password?(remember_token)
end
# Forgets a user.
def forget
update_attribute(:remember_digest, nil)
end
end
Any ideas on what could have caused this?
You have two
def current_user
methods in module SessionsHelper
remove this one(which is old one and doesn't check for cookies presence):
#Returns the current logged-in user (if any).
def current_user
#current_user ||= User.find_by(id: session[:user_id])
end
it is being used in your test currently, because it is defined below updated one.