#274 Remember Me & Reset Password
Jul 11, 2011 | 12 minutes | Authentication
It is easy to create authentication from scratch, but how do we extend it with more features? Here I add a "remember me" check box and a "forgotten password" link.
- Download:
- source code
- mp4
- m4v
- webm
- ogv
Since rails detects and rejects cookies that have been tampered with, could we just use the user id rather than the auth_token, and put it in a signed cookie? Or, since sessions seem to actually be signed cookies (no?), is there a way in rails to just directly extend the expiration of the session cookie (if there's nothing else we keep in the session that we want to expire sooner)?
Great point. You can store the user id in a signed cookie instead of generating a custom token. However for permanent cookies I prefer to keep it separate so it gives you more control over when to expire the session. For example you can reset the
auth_token
every time someone resets the password, or maybe every time someone logs in to automatically log-out previous ones. There's a lot more flexibility here.It is also possible to change when the session cookie expires, but I think it's better to stick with a separate cookie.
Is there then a reason why you didn't use
cookies.permanent.signed
for theauth_token
instead ofcookies.permanent
? It seems you can chain them and I can't find any drawbacks from using this.Signing cookie adds overhead to the size (http://yuiblog.com/blog/2007/03/01/performance-research-part-3/). So that depends on the security you need.
For better security you want to save also a expiry time inside the cookie and sign it. Then you can check e.g. that the cookie isn't more than year old so that info in old computers cannot be abused. But in most cases the way described in this screencast is enough.
Also, about Devise, it's an engine, so changing views or controllers is as simple as putting them directly in your app. With rails engines, what's in your app takes precedence over what's in the engine. If you have no view for X, the it takes it from the engine. If you do, it uses yours. I like the idea of building things from scratch (this is a great episode) and minimizing plugin bloat, but it's worth mentioning.
You're right, it is easy to customize Devise views. My point being that if you're going to customize this anyway, I feel it's a good portion of the work involved in adding these features from scratch. I also find I often need to customize more than this in Devise, such as overriding certain controller actions, and then things get messy.
Ryan, I agree with you.
I used to use Devise. But then I realised I spend way more time trying to customise it than just implementing it from scratch.
This is especially true when you need to do something that Devise doesn't support "out of the box".
Once again, nice screencast!
However, I think you have too much logic inside te password_resets_controller. Especially the 2.hours part. Now for small apps this might not be a big problem, but otherwise, I would create something like a PasswordReset class in the model directory to handle this logic for me.
And I agree with @introspectif that Devise is childishly easy to customize. So much so that I would never write my own authentication. There are too many security pitfalls to remember.
I agree that should probably go somewhere else, maybe in the User model or a separate config. I would hesitate to create a PasswordReset class just for this. True that it might fit the RESTful paradigm when there is a resource backing it, but I don't think the end result would be clearer or easier to read.
Good point about security. I personally feel Devise gets messy when needing to customize the behavior, however this may be just a personal opinion. I would love to see something lower level that can handle all of the security measures without taking over controllers. Something along the lines of Authlogic but which followed MVC a little better.
What's with Authlogic anyhow? I always felt it was the better level of abstraction compared to Devise.
I do like it that Authlogic does not try to take over views, but I think there are better solutions out there. It always bothered me how much it knows about the current request/session, and it felt a little over-complicated.
The authentication gem sorcery claims to be a "stripped-down, bare-bones authentication library" that lets you add common auth functionality from the library to your own controllers.
I haven't used it yet, but I plan to give it a shot on my next project.
https://github.com/NoamB/sorcery
Sorcery has been on my list to check out, but I haven't yet. Thanks for the reminder!
I also agree with @Iain and @introspectif that there are too many security loopholes to remember to close each time to build from scratch.
Also I would suggest adding an index on the email column of the user model since every time someone logs in you are performing an
user = User.find_by_email(params[:email])
Are there any specific security loopholes you see that would be difficult to remember? The most common security issue is hashing the password which is now handled nicely in Rails 3.1 with
has_secure_password
.Good point! The
auth_token
should be indexed as well.Ryan,
One problem with your method is that, while the authentication tokens cannot be guessed they can be duplicated and used on another system. It's close to session fixation. I think that the user record should probably store their log-in OS/Browser checksum to validate any returning users.
At least then it would be harder to steal a session.
I find users often want to log-in through multiple devices at the same time. Also if one can steal the cookie it would not be difficult to spoof their user agent.
I think it depends on the app, if you are making a banking site then security is much more critical and then you probably shouldn't listen to me. :)
Awesome episode as usual.
You're right, I prefer using custom logins and authentication over Devise or other methods. I just feel "out of control" when using a plug for something as secure and important as my login.
Keep up the great work!
Nice work. Again for those who didn't understand, this screencast should be consider as an introduction to authentication. The purpose of it is not to define all security hole and patch them.
The Devise source code is very good at this. The engine cover a lot of advance features. I suggest everyone to have a look at it and learn from it.
Secure an application is not an easy task to accomplish. I suggest everyone to create a checklist and verify there projects against it.
For anyone who is still using ruby 1.8.7 for rails 3.0.5
SecureRandom.urlsafe_base64 will generate a method not found error. I used SecureRandom.hex as a substitute and it worked. Just thought I'd share in case this helps anyone...
Nothing wrong with hex, but if you want base64, you could do this:
Thanks guys! That helped a lot! :)
Thanks for the hint -- exactly what I needed for my "legacy" app.
I like episodes about gems much more: just a little piece of code, and great result. This time i didn't see that. Instead, we got 5 times more code to remember - that's hard for me. :)
Still, there's no need to show everything in things-from-scratch, it's complicated, boring, and will never be the same for other case.
In my opinion, for modern small website we should forget about registering user emails - only omniauth - it's fast, it's handy, but i don't know a simple way to get it up and running on rails yet. For example: devise + omniauth still requires me to know user email. I don't really need it. I'd love to come to website, press "Add comment" button, choose authentication provider, and that's it (just like on railscasts, btw)
Anyways, i'll come back here, to this Episode, whenever i will need authentication from scratch.
Well I feel bad because I have been providing feedback that the screencasts have focused too much on gems. I like to see things from scratch, it helps become a better coder. Omniauth is a perfect example, sure you can just plug it into devise, but what if you need to use external services separate from login - most people would be stuck because they've relied so heavily on gems to do this kind of work. All I can say is we have a difference of opinion, i just wanted mine to be heard because I like a variety of screencasts, sometimes about specific code, and sometimes about gems.
I too prefer Omniauth for authentication, but not everyone wants to use external services for auth. Even when using Omniauth it's good to provide a fallback for username/password if users don't want to link the account to twitter/facebook.
Thanks for the feedback regarding gems vs. from scratch. I try to cover both because I think they both have their place. There are many cases where gems don't quite fit and require so much customization that the dependency is not worth it. Authentication is often that way for me.
Hi, Ryan. Just an observation.
At 5:45 you say something about not letting bad guys know if an email is already registered.
I've heard many people arguing that and many sites implementing it. My experience so far:
Why is it useless? Because on every site I've tried, you can just try to register with a given email address and it gives an error saying "email already registered" if it is.
@Javi, the idea is that authentication requires two parts - a
username
andpassword
. However, there are tools that allow you to brute force attacks on a login form.If the flash/error message changes depending on whether the username is valid, it will take far less time for a hacker to determine a valid username. Once a valid username is found, it becomes a simple attack (unless you implement some kind of failsafe in your code for multiple incorrect password attempts)
Mathematically, imagine it takes a computer 10 hours to brute-force a field of 64 characters (it's not, but I want easy math). If error messages don't reveal that a given username exists, the computer is required to compute 10x10 = 100 hours (for each username generated, it needs to produce passwords as well). However, if the hacker can determine a valid username first (10 hours), and then only brute-force the password (another 10 hours), it only takes 20 hours to hack the site.
It's not "useless", but it's not full-proof either.
and, back to parent post, imagine - i'm a hacker, and tryng to register existing email, it shows an error, so now i know valid email to attack.
Very good point. If you're giving an error that the email address is registered then there is no point in making the password reset message generic. In that case you might as well go with a more user friendly approach and state that the email address was not found.
I mentioned in the screencast that you may want to change that experience depending on the app.
Ryan,
Nice Podcast as always! You might want to consider doing a podcast on Two factor Authentication since security has been in the media lately and on our minds. https://moocode.com/posts/2-two-factor-authentication-with-rails is a nice tutorial but I think you could make it into a great podcast! Just a though thou. ;0)
Checkout https://github.com/rejeep/authlogic-password-reset-tutorial for reset with Authlogic!
Great episode. And I too want to put my vote down for more "from scratch" episodes.
I love the gem community and all the Github repos out there, and while I sometimes use these screencasts to find new "gems" amongst the gems, I mainly use it to learn and follow along while I write the same code, in my own style.
Thanks again!
Thanks for the feedback. I will try to do both because I think both have their place. I too try to reduce the number of gems I use in a project because having many dependencies has bitten me in the past.
Nice to see you active in the comments section as well. Wish you dropped by a little later to allow me to add this question to my reply, but here goes:
In the episode at 8:13 you talk about the URL the user uses to reset his/her password and that it isn't the most elegant RESTful route, but "it will work for us here".
I love (almost) all the screencasts you publish, and I watch every one of them at the start of the week (go self employed!) but I've noticed you've taken these "quick and dirty routes" a couple of more times in the past, and every time I hear you say that I get a nasty feeling, that I am following along writing some great code, and suddenly we make a shortcut and you leave me (us?) high and dry, wondering what I should do to make it actually be the best RESTful way to handle this.
On internal code (code that is never shown to the outer world) I don't mind quick'n'dirty techniques from time to time, if it helps me move on so I can come back to it later, but crucial parts (database layout) and code that is shown to the outside world (routing), I like to make it as good as possible.
Now this isn't criticism, instead I hope you take this as positive feedback. And perhaps you could elaborate on ways to improve the route at 8:13 for this screencast.
Thank again, and obviously we all have to try things ourself, so I'll definitely be trying to improve that piece of code.
That's a good point. When I mentioned the edit action not being truly RESTful I am not hiding a better solution, but more pointing out I am intentionally not following REST here. I considered a couple alternatives when preparing this episode.
What I consider the "proper" way to do this would be to have two controllers/resources. One being PasswordResetRequests with new/create actions, and one being "UserPasswords" with edit/update actions. But two controllers seem overboard for this one feature.
Alternatively we could handle the editing of the password in the Users controller, after all it is the User model we are working on, but if we add a way to edit the user profile then there is a conflict here. Suddenly the edit/update actions would need to have several "if" conditions determining if it is editing the profile or resetting the password. Not only that but it is mixing the password reset feature into other parts of the app. This would quickly get messy.
REST is actually more about the URLs than it is controllers, so we could keep everything in the same controller but add custom URLs. However, this would require 4 complex lines in the routes file instead of the simple
resources :password_resets
. I do not like that solution either.So, we're left with leaving everything in a PasswordResets controller which follows the REST pattern but it is not truly RESTful because it is working on two separate resources. Still I consider it to be the best solution for this problem. Maybe there are ways the Rails router itself can be improved to handle these situations.
Hey Ryan, thank you for the explanation. That makes a lot of sense. Still though, after reading your comment and seeing the episode I personally would prefer to go the UsersController way, having a special "reset_password" action.
I haven't tested this yet so I don't know if this is correct, but I guess this is all that is needed to get it working in the routes, which isn't all too complex and requires only three extra lines (two if you already have the :users resources):
routes.rb
or as an alternative:
UsersController
Actually the UsersController should be like this, the above is the update action, which should stay the same:
I can confirm that this method works. Thanks for the example.
Fantastic episode, Ryan. I am an experienced developer but a newbie to Rails. I enjoyed this "from scratch" episode immensely.
Do you any episodes where you go without scaffolding? Any where you plug in the different parts like controller, model(is this always necessary), view, custom validation, helpers by hand and as late as possible?
I see many blog posts about how scaffolding is not the recommended way but haven't found much guidance on how to do this from scratch.
Also, I am also very interested in BDD (behaviour driven development) and am hoping you can give some guidance on how to do that.
Thanks for your contribution
-Venu
I didn't use scaffolding in this episode, however I did use several generators (controller, mailer, migration). I think most Rails developers use these generators.
That said, I do scaffolding in my everyday development through Nifty Generators. I encourage everyone to create their own scaffold generators to improve their workflow.
I think scaffolding just gets a bad rap because the built-in scaffold generator is near useless in everyday Rails development.
I do hope to incorporate more of my development workflow into these screencasts. Maybe do some TDD at the beginning. We'll see.
Hey Ryan. If we're in the topic of authentication - is there any chance You could make an episode about creating oAuth provider?
There's no good tutorial out there about this.
Great episode btw! :)
One more great article. But I prefer using authlogic or device to achieve those results as those gems have a lot of features explained here and much more. And they're covered with tests what is important!
I am new also to rails. Is there a way to generate a unique token with a key (like the user id), which would ensure its uniqueness, which would prevent you from having to check if it already exist in the user table.
Thanks a lot Ryan,
Your approach does not allow a user to remember me from multiple machines. So I guess it worth to mention that. Also it worth to mention that probably storing the tokens in a Rails.cache is more scalable and durability can be achieved if the cache is backed by a durable cache (like membase)
Good point (re: remember me from multiple machines).
has anyone encountered this error going through this tut!!
undefined method `urlsafe_base64' for SecureRandom:Module
app/models/user.rb:39:in
generate_token'
send_password_reset'app/models/user.rb:31:in
app/controllers/password_resets_controller.rb:7:in `create'
because I have and i've no idea how to solve this..
Perhaps your problem is related to Matt-Bishop's comment?
Or use Gabe Kopley's solution:
Thanks a million Jean! Hit the bulls eye!! Spot on!!
I've just run into 'undefined method `urlsaf_base64' for SecureRandom:Module' on ruby 1.9.2p290
it would help if 'safe' were a word i could spell
Thanks
Ryan make a wonderful new Screencast! I instantly implemented this within my project.
For a rake-task (to reset auth_token)Ryan recommandedin the video I personally wrote:
Where does this section of code should be placed.
Does this code adds auth_token to previously existed users ?? Curiousity!
It's placed in
lib/tasks/rebuild_token_auth.rake
then you can run it with
I've also had to check it out!
Hey Ryan!
I'm new to Rails and I really like Your screencasts.
However, I think I've found kind of security flaw in this one.
In password_resets_controller.rb you do:
It allows the user to change any attribute he wants using prepared form/request.
I realize this is just an example, and in production you probably wouldn't do it, but still, it's not good example IMHO.
Correct me if I'm wrong.
i have the same concern, since it is quite possible to brute force many ids in password_resets/id/edit and then set the users password AND email to your own pw & email.
If you could not assign an email via the password_resets_controller, then it would be much harder to figure out which email was assigned to the updated password.
You could use UrlStore https://github.com/grosser/url_store and just encode user id and expiration time inside the url, no new columns needed. Only drawback is that the token gets longer.
Bye bye Authlogic/Devise, hello Ryan's "authentication from scratch" + CanCan. Thank you.
@Iain
That's only true for simple, small applications.
And that's true for Devise customizations in more complex scenarios ;)
For some reason
cookies.delete[:auth_token]
does not work in Rails 3.1.I had to use
cookies.delete :auth_token
to get it to destroy the session.It should be
cookies.delete(:auth_token)
, notcookies.delete[:auth_token]
.Great screencast! I realy love your "*from scratch" screencasts but i encountered some problems:
I had to use
save!(validate: false)
in the send_password_reset method after i added some validation on my user model. Why does the validation for the presence of a password and password_confirmation fire on the reset password form? And is there any way to validate only for a valid email withou using javascript?9 months bump.....
I also have this problem
have you tried using
validates_presence_of :password, on: :create
Would you not want validation when a user updates his details as well?
Hello, i retrieve an error by submit "reset the Password":
Have anybody a Idea? Thanks!
I keep getting
in my template I use:
in my application.rb it is defines
happens when a user is trying to reset the password.
Did you ever figure this out? I'm getting the same error.
I get same error. I even tried running the query from the console and I get nil.
I try find_by_email or find_by_first_name and it works. Only when running find_by_auth_token it returns nil.
I'm getting this too, I'm stuck!! Have compared my code with every file in the source code I can think of... Is it really working for everyone else following this screencast??
I'm running Rails 3.1.0, Ruby 1.9.2p290, sqlite3-ruby 1.3.3, Rake 0.8.7...
I ended up fixing this by rolling back my migrations and then migrating again. Anyone know why this would be necessary??
you didnt need to roll back you probably were trying to use/login with a user-account that was already in your database. After doing the tutorial, create a new user so the auth_token column is populated and it will fix your error. @fritzsche created a rake tasks that will fix all your current users in your database incase you dont want to do a rake db:migrate:reset.
in short you cannot login with a old user after doing this.
I ran into the same issue, I posted a solution at http://stackoverflow.com/a/15865006/1814446
I think it's important to mention that when you set @current_user with cookie instead of session you lose the efficiency of rails CSRF protection.
It's happen because when rails protect_from_forgery method identify CSRF attack it's reset only the session and not the user's cookies.
To fix it you need to override handle_unverified_request()
at MODULE ActionController::RequestForgeryProtection::ClassMethods
and define it to delete auth_token cookie
Hey...Anyone else having problems with validations?
Ny solutions?
for anybody having the same problem with testing, when switching from session to cookies. You need to use request.cookies to assign the cookie.
and in your normal specs where you use test_sign_in
in specs where you dont use test_sign_in but rather post :create, :email => "user@example.com", :password => "somepassword"
its really confusing me
@ryan is there any reason why in the
PasswordResetsController
you perform the 2 hour check in the update action and not in the edit action? It seems counter-intuitive to have the user post a new password, and then inform them that the password reset token has expired. Is there any reason why you wouldn't do it like this?And thanks for showing us this, I've now ditched Devise and feel cleansed.
Having a check in the edit action makes it more convenient.
However you should NOT remove the check from the update action, as attackers can now try outdated activation codes forever with forged PUT requests.
though i forgot, rails takes care of the forms validity with the authenticity token, however its probably still safer to have a time check in the update action.
I love this solution but as a total rails noob, I don't know how to test for it, it would be great to have some explanation or, even better, an episode on it.
Regarding validations...
Well, if you're like me, you have password validation and also encrypt the password before saving the user model.
That's a big monkey wrench for Ryan's excellent solution above.
To get it all to work, I created a PasswordReset model