Demo: In Task F, Iteration F1 (near File 54 marker), in the Rails book by D. Thomas. He suggests handling password hashing by using the AR callbacks. <snip> attr_accessor :password <snip> def before_create self.hashed_password = User.hash_password(self.password) end Is this simply to illustrate callbacks? Would it not be easier to create a virtual accessor: def password=(plain) hashed_password = User.hash_password(plain) end I don''t like the idea of that transient plain-text field and extra callback methods. I''m just wondering if I''m missing something about the whole form->action->AR->save cycle where this alternate technique would be worse than the callback method. I can answer my own question here in that it would be hard to enforce password policies without a transient plain-text version to validate against; however, the question remains: what is the best practice? Thanks
Forrest Thiessen
2005-Nov-16 05:34 UTC
Re: Agile Dev w/Rails, pp.128-129 Hashing Passwords
Edward Frederick <epfrederick@...> writes:> Is this simply to illustrate callbacks? Would it not be easier to > create a virtual accessor: > ... > I''m just wondering if I''m missing something about the whole > form->action->AR->save cycle where this alternate technique would be > worse than the callback method. > > I can answer my own question here in that it would be hard to enforce > password policies without a transient plain-text version to validate > against; however, the question remains: what is the best practice?I tried several ways of accomplishing the same goal; after some trial-and-error, I settled on the "virtual" approach you describe as being the most simple to understand (and so theoretically the most maintainable). Other than complexity, I don''t see that one method has any benefit over the other. As you suggest in your comment about enforcing password policies, you have to add a little bit more code to make the virtual attribute approach work with form validation. Here''s what I did (inside my model): def password=(password) @password = password salt = random_string(10) hash = Digest::SHA1.hexdigest(salt + password) self.password_salt = salt self.password_hash = hash end def password return @password if @password return ''password'' if not self.password_hash.blank? end def password_is?(password) return Digest::SHA1.hexdigest(self.password_salt + password) == self.password_hash end Basically, my password= is the same as yours, with the addition of doing the salt stuff and setting an instance variable to the plain-text password. The instance variable is never saved, so it is indeed "transient", as you suggest, but it''s available for the password method to access for purposes of form validation. Since we don''t save the plain-text password we have to come up with *something* to return when the password method is called and the @password instance variable is no longer available--so I return the string "password": it will pass my field validations, so it serves nicely. You could return the password hash, but it''s longer than the length limit I have on the password field in my validations. Set up this way I can put all my password rules in the model as field validations, and everything automagically works just the way you would want it to. The third method is what I use to actually check whether the password a user types is correct or not; I decided to encapsulate it in the model so none of the rest of my code needs to know anything about salts or any other implementation details. --Forrest