I'm following Iteration 13 of Agile Web development, for User login.
I create a migration to add 2 columns to my User model : hashed_password and salt.
Creation of Users works
login fails, due to this error undefined method 'hashed_password' in method 'authenticate'
The problem is that :
In rails console, I can fetch User.first.hashed_password, what seems OK :-)
I outputted the User that I fecth, and it is NOT nil
I tried to output user.hashed_password as I did in the rails console, but that throws always the same error :
NoMethodError (undefined method hashed_password' for #<ActiveRecord::Relation:0x00000003e6c3c0>):
app/models/user.rb:21:inauthenticate'
app/controllers/sessions_controller.rb:6:in `create'
This is my User model :
require 'digest/sha2'
class User < ActiveRecord::Base
has_and_belongs_to_many :products
has_many :created_products, :class_name => "Product", :foreign_key => :product_id
default_scope :order => "username ASC"
# Attributs pour le login (Livre)
validates :username, :presence => true, :uniqueness => true
validates :password, :confirmation => true
attr_accessor :password_confirmation
attr_reader :password
validate :password_must_be_present
def User.authenticate(name, password)
logger.debug "---------- Beginning of Authenticate"
if user = User.where(:username => name)
logger.debug "utilisateur = #{user.inspect}" # THIS IS OK AND NOT NIL
logger.debug "utilisateur hashed PW = #{user.hashed_password}" # ERROR
if user.hashed_password == encrypt_password(password, user.salt)
return user
end
end
end
def User.encrypt_password(password, salt)
Digest::SHA2.hexdigest(password + "wibble" + salt)
end
def password=(password)
#password = password
if (password.present?)
generate_salt
self.hashed_password = self.class.encrypt_password(password, salt)
end
end
private
def password_must_be_present
errors.add(:password, "Mot de passe manquant") unless hashed_password.present?
end
def generate_salt
self.salt = self.object_id.to_s + rand.to_s
end
end
User.where(:username => name) is returning an ActiveRecord::Relation object (hence the error message you are seeing). Try changing your if statement to:
if user = User.where(:username => name).first
That will set take the first matching user, which will be an instance of User and so will have a hashed_password field. If no user matches, you'll get nil.
put this in your model
private
def self.hash_password(password)
Digest::SHA1.hexdigest(password)
end
Related
I am trying to store a password after hashing it but it shows up as NULL in the database.I generated a scaffold for users using password string and name string, and then altered the mysql table to store hashed password instead using this :
ALTER TABLE users CHANGE password hashed_password CHAR(40) NULL;
my model:
class User < ActiveRecord::Base
attr_accessor :password
attr_accessible :name, :password
validates :name, :uniqueness => true
validates :password, :length => { :in => 6..20 }
def before_create
self.hashed_password = User.hash_password(self.password)
end
def after_create
#password = nil
end
private
def self.hash_password(password)
Digest::SHA1.hexdigest(password)
end
end
I am using Rails 3.2.13.
I think you should use
before_create :hash_the_password
after_create :nil_the_password
def hash_the_password
self.hashed_password = User.hash_password(self.password)
end
def nil_the_password
#password = nil
end
and NOT
#Wrong?
def before_create
...
end
so the callbacks can be the problem.
I'm folowwing Charles Max Wood tutorial on the twitter clone , flitter.
I'm having and error undefined method friendships when I launch rake db:seed .I'am trying to add friend via the rake db:seed task , The method add_friend is define in the User model. But i need help to define the method friendships so that the task can work .Thank you a lot for your help .
Here is the db/seeds.rb file
require 'faker'
require 'populator'
User.destroy_all
10.times do
user = User.new
user.username = Faker::Internet.user_name
user.email = Faker::Internet.email
user.password = "test"
user.password_confirmation = "test"
user.save
end
User.all.each do |user|
Flit.populate(5..10) do |flit|
flit.user_id = user.id
flit.message = Faker::Lorem.sentence
end
3.times do
User.add_friend(User.all[rand(User.count)])
end
end
and there is the user file.
class User < ActiveRecord::Base
# new columns need to be added here to be writable through mass assignment
attr_accessible :username, :email, :password, :password_confirmation
attr_accessor :password
before_save :prepare_password
validates_presence_of :username
validates_uniqueness_of :username, :email, :allow_blank => true
validates_format_of :username, :with => /^[-\w\._#]+$/i, :allow_blank => true, :message => "should only contain letters, numbers, or .-_#"
validates_format_of :email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
validates_presence_of :password, :on => :create
validates_confirmation_of :password
validates_length_of :password, :minimum => 4, :allow_blank => true
has_many :flits, :dependent => :destroy
has_many :friendships
has_many :friends, :through => :friendships
def self.add_friend(friend)
friendship = friendships.build(:friend_id => friend.id)
if !friendship.save
logger.debug "User '#{friend.email}' already exists in the user's friendship list."
end
end
# login can be either username or email address
def self.authenticate(login, pass)
user = find_by_username(login) || find_by_email(login)
return user if user && user.password_hash == user.encrypt_password(pass)
end
def encrypt_password(pass)
BCrypt::Engine.hash_secret(pass, password_salt)
end
private
def prepare_password
unless password.blank?
self.password_salt = BCrypt::Engine.generate_salt
self.password_hash = encrypt_password(password)
end
end
end
friendship.rb
class Friendship < ActiveRecord::Base
attr_accessible :friend_id, :user_id
belongs_to :user
belongs_to :friend, :class_name => 'User'
validates_uniqueness_of :friend_id, :scope => :user_id
validates_presence_of :user_id, :friend_id
end
I think what you want to be doing is calling add_friend on the instance user, and not on the class User:
3.times do
user.add_friend(User.all[rand(User.count)])
end
Also your add_friend method should be an instance method, not a class method, so you don't need the self:
def add_friend(friend)
friendship = friendships.build(:friend_id => friend.id)
if !friendship.save
logger.debug "User '#{friend.email}' already exists in the user's friendship list."
end
end
You should define this method as a class method not instance method:
def self.add_friend(friend)
friendship = friendships.build(:friend_id => friend.id)
if !friendship.save
logger.debug "User '#{friend.email}' already exists in the user's friendship list."
end
end
My User model has an attribute called "points" and when I try to update it in another model controller (decrementing points), the attribute will not save (even after adding it to attr_accessible).
The method in my Venture Controller code:
def upvote
#venture = Venture.find(params[:id])
if current_user.points < UPVOTE_AMOUNT
flash[:error] = "Not enough points!"
else
flash[:success] = "Vote submitted!"
current_user.vote_for(#venture)
decremented = current_user.points - UPVOTE_AMOUNT
current_user.points = decremented
current_user.save
redirect_to :back
end
I have even tried using the update_attributes method, but to no avail.
I added a quick little test with flash to see if it was saving:
if current_user.save
flash[:success] = "Yay"
else
flash[:error] = "No"
end
and the error was returned.
current_user comes from my Sessions helper:
def current_user
#current_user ||= user_from_remember_token
end
Thanks ahead of time.
My User model:
class User < ActiveRecord::Base
attr_accessor :password, :points
attr_accessible :name, :email, :password, :password_confirmation, :points
STARTING_POINTS = 50
acts_as_voter
has_karma :ventures
has_many :ventures, :dependent => :destroy
email_regex = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :name, :presence => true,
:length => { :maximum => 50 }
validates :email, :presence => true,
:format => { :with => email_regex },
:uniqueness => { :case_sensitive => false }
validates :password, :presence => true,
:confirmation => true,
:length => { :within => 6..40 }
before_save :encrypt_password
after_initialize :initialize_points
def has_password?(submitted_password)
password_digest == encrypt(submitted_password)
end
def self.authenticate(email, submitted_password)
user = find_by_email(email)
return nil if user.nil?
return user if user.has_password?(submitted_password)
end
def self.authenticate_with_salt(id, cookie_salt)
user = find_by_id(id)
(user && user.salt == cookie_salt) ? user : nil
end
private
def initialize_points
self.points = STARTING_POINTS
end
def encrypt_password
self.salt = make_salt if new_record?
self.password_digest = encrypt(password)
end
def encrypt(string)
secure_hash("#{salt}--#{string}")
end
def make_salt
secure_hash("#{Time.now.utc}--#{password}")
end
def secure_hash(string)
Digest::SHA2.hexdigest(string)
end
end
This is what I get after printing <%= debug current_user %>
--- !ruby/object:User
attributes:
id: 1
name: Test User
email: a#s.com
created_at: 2011-08-27 21:03:01.391918
updated_at: 2011-08-27 21:03:01.418370
password_digest: 40d5ed415df384adaa5182a5fe59964625f9e65a688bb3cc9e30b4eef2a0614b
salt: ac7a332f5d63bc6ad0f61ceacb66bc154e1cad1164fcaed6189d8cea2b55ffe4
admin: t
points: 50
longitude_user:
latitude_user:
attributes_cache: {}
changed_attributes: {}
destroyed: false
errors: !omap []
marked_for_destruction: false
new_record: false
points: 50
previously_changed: {}
readonly: false
You are requiring the user's password to be present any time the user is saved. When the upvote is submitting, the password is not present, therefor validation is not passing.
This would suggest some kind of validation failed. An easy way to circumvent this, is to use update_attribute. This will update a single attribute and save without running the validations.
So instead write
current_user.update_attribute :points, current_user.points - UPVOTE_AMOUNT
This should work.
This does not solve the problem why saving an existing user could fail, so you still need to check your validations and before_save actions.
Hope this helps.
Ha. Indeed. The update_attribute does skip validations, but not the before_save.
So, if the before_save is the problem, you only want to trigger if the password has changed, so you could do something like
def encrypt_password
self.salt = make_salt if new_record?
self.password_digest = encrypt(password) if self.password_changed?
end
But this would only work if password is an actual attribute of your model, which seems unlikely. Why would you store the hash (for safety reasons) and the password in cleartext. So ... I guess you only have a password_digest field, and then it should become something like:
def encrypt_password
self.salt = make_salt if new_record?
self.password_digest = encrypt(password) if password.present?
end
Only if a password was given, try to recreate the digest.
I'm very new to rails and I'm trying to accomplish the following authentication issue:
User makes a comment or grants "absolution" (similar to comment) and he gets some coins for it. Coins is the virtual currency in my app and is also a column in the users table.
Because of your kind help, I was already capable to update the coins value after writing a comment or grant absolution. However, when I write a comment and log out after that, my login name or password gets changed(?)...I can't login anymore with this account.
This is how my User model looks like:
require 'digest'
class User < ActiveRecord::Base
attr_accessor :password
attr_accessible :name, :email, :password, :password_confirmation, :twitter_url, :homepage_url, :coins
has_many :comments, :dependent => :destroy
has_many :absolutions, :dependent => :destroy
has_many :ratings
has_many :rated_sins, :through => :ratings, :source => :sins
email_regex = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
homepage_regex = /(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix
validates :name, :presence => true,
:length => { :maximum => 50 }
validates :email, :presence => true,
:format => { :with => email_regex },
:uniqueness => { :case_sensitive => false }
validates :twitter_url, :format => { :with => homepage_regex }
validates :homepage_url, :format => { :with => homepage_regex }
validates :password, :presence => true,
:confirmation => true,
:length => { :within => 6..40 }
before_save :encrypt_password
def has_password?(submitted_password)
encrypted_password == encrypt(submitted_password)
end
def self.authenticate(email, submitted_password)
user = find_by_email(email)
return nil if user.nil?
return user if user.has_password?(submitted_password)
end
class << self
def authenticate(email, submitted_password)
user = find_by_email(email)
(user && user.has_password?(submitted_password)) ? user : nil
end
def authenticate_with_salt(id, cookie_salt)
user = find_by_id(id)
(user && user.salt == cookie_salt) ? user : nil
end
end
private
def encrypt_password
self.salt = make_salt if new_record?
self.encrypted_password = encrypt(password)
end
def encrypt(string)
secure_hash("#{salt}--#{string}")
end
def make_salt
secure_hash("#{Time.now.utc}--#{password}")
end
def secure_hash(string)
Digest::SHA2.hexdigest(string)
end
end
And this is my comments controller:
class CommentsController < ApplicationController
before_filter :authenticate, :only => [:create, :destroy]
def new
#comment = Comment.new
end
def create
#sin = Sin.find(params[:sin_id])
#comment = current_user.comments.build(params[:comment])
#comment.sin_id = #sin.id
if #comment.save
flash[:success] = "Comment created! Earned 20 coins."
coins_new = current_user.coins.to_i + 20
current_user.update_attribute(:coins, coins_new)
redirect_to sin_path(#sin)
else
flash[:error] = "Comment should have 1 - 1000 chars."
redirect_to sin_path(#sin)
end
end
def destroy
end
private
def authenticate
deny_access unless signed_in?
end
end
I assume, that it has something to do with the before_save encrypt_password method, but its only a guess. I really appreciate your help and suggestions!
Edit:
It gets warmer...It has something to do with the following line in the Comments Controller:
current_user.update_attribute(:coins, coins_new)
When he updates the :coins column, something seems to go wrong. If you need further info, just drop a comment. Thanks for your help!
Your problem is that you're encrypting the already encrypted password in your "encrypt_password" method.
So, when the user is a new_record?, you're taking the password (say it's "cat"), hashing it, and storing it in the database.
So, what gets stored is a cryptographic hash of "cat", which we'll say is "dog"
Then, the next time you're saving the user record, you're taking the hashed password ("dog") in line #2 of your "encrypt_password" method, and hashing it again, which we'll say generates "kangaroo".
Next time you log in, your app is hashing the password you enter into the login form "cat", hashing it to "dog", and comparing it to the hashed version in the database, "kangaroo". Oh, but "dog" doesn't match "kangaroo", so the login fails.
So change:
def encrypt_password
self.salt = make_salt if new_record?
self.encrypted_password = encrypt(password)
end
To either:
def encrypt_password
self.salt = make_salt if new_record?
self.password = decrypt(password) # decrypt it first to the plain text
self.encrypted_password = encrypt(password) # then re-encrypt the plain text with the salt
end
Or:
def encrypt_password
self.salt = make_salt if new_record?
if (password_has_changed?) # somehow you'll have to figure this out
self.encrypted_password = encrypt(password)
end
This code is from the agile web development with rails book.. I don't understand this part of the code...
User is a model which has name,hashed_password,salt as its fields. But in the code they are mentioning about password and password confirmation, while there are no such fields in the model. Model has only hashed_password. I am sure mistake is with me. Please clear this for me :)
User Model has name,hashed_password,salt. All the fields are strings
require 'digest/sha1'
class User < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name
attr_accessor :password_confirmation
validates_confirmation_of :password
validate :password_non_blank
def self.authenticate(name, password)
user = self.find_by_name(name)
if user
expected_password = encrypted_password(password, user.salt)
if user.hashed_password != expected_password
user = nil
end
end
user
end
def password
#password
end
def password=(pwd)
#password = pwd
return if pwd.blank?
create_new_salt
self.hashed_password = User.encrypted_password(self.password, self.salt)
end
private
def password_non_blank
errors.add(:password,"Missing password")if hashed_password.blank?
end
def create_new_salt
self.salt = self.object_id.to_s + rand.to_s
end
def self.encrypted_password(password, salt)
string_to_hash = password + "wibble" + salt
Digest::SHA1.hexdigest(string_to_hash)
end
end
attr_accessor is used to create getter/setter methods for an instance variable. For example:
attr_accessor :foo
# is equivalent to
def foo
#foo
end
def foo=(value)
#foo = value
end
In the code you pasted, def password and def password= are defined manually. However, I'm a bit confused by the use of:
attr_accessor :password_confirmation
validates_confirmation_of :foo automatically creates a foo_confirmation accessor, so this should be:
attr_accessor :password
validates_confirmation_of :password
Add a simple before_save callback to encrypt the password and you're all done.
require 'digest/sha1'
class User < ActiveRecord::Base
# attrs
attr_accessor :password
# class methods
class << self
def encrypt(password, salt)
Digest::SHA1.hexdigest("--#{salt}--#{password}--");
end
end
# validation
validates_presence_of :name
validates_confirmation_of :password
# callbacks
before_save :encrypt_password
protected
def encrypt_password
return if password.blank?
if new_record?
self.salt = Digest::SHA1.hexdigest("--#{Time.now}--#{name}--")
end
self.encrypted_password = User.encrypt(password, salt)
end
end
Since you don't want to store the password in the database in plaintext, you create a virtual attribute called password. You do this when you write:
def password=(pwd)
#password = pwd
return if pwd.blank?
create_new_salt
self.hashed_password = User.encrypted_password(self.password, self.salt)
end
That way, when you call password="wibble" it is actually encrypting "wibble" and storing the encrypted value in the database instead.
In the example, password_confirmation property is added to the model using the attr_accessor helper which sets up a getter and mutator for you in one line of code:
attr_accessor :password_confirmation
That one line is the same as if you had written this:
def password_confirmation
#password_confirmation
end
def password_confirmation=(pwd_conf)
#password_confirmation = pwd_conf
end
password's accessors are defined explicitly in the model:
def password
#password
end
def password=(pwd)
#password = pwd
return if pwd.blank?
create_new_salt
self.hashed_password = User.encrypted_password(self.password, self.salt)
end
These virtual attributes are defined in the model code, because they don't exist in the db table the model gets the rest of it's attributes from, because you don't want them stored in the db.
I was having the same question.
I found the item of validates_confirmation_of at Rails API website useful: http://ar.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#M000081.