#250 Authentication from Scratch
Jan 24, 2011 | 15 minutes | Authentication
Password authentication is not too complicated to make from scratch, it will also help to get a better understanding of how it works.
- Download:
- source code
- mp4
- m4v
- webm
- ogv
Thats what am talking about! I'm thinking of dropping Devise and any other auth engine from my projects and make my own auth system!
And CanCan will work normally as expected, right?
Thanks Ryan!
Congratulations on making #250!
Now keep it up for another 250. :)
@Amr, yes, CanCan will work with any authentication solution. It just relies on a current_user method, which even then you can customize.
@alex, thanks. I expect to.
Great cast! Really helpful to see what's going on under the hood.
Considering how Authlogic has gone the way of the dodo, I wish I had just done this in the first place with my apps. The fewer external dependencies you have, especially for something this important, the better off you are in the long run.
It'd be great to see a second part to this episode that covers a few less essential but common features like account verification via email, password resetting, and a "remember me" option.
Congrats on episode #250! Keep it up. :D
Quite nice, indeed! I also prefer doing authentication myself. I ended up using devise though, as it takes care of a lot of details, like validation and has a lot of useful modules - also for OmniAuth.
I'm agree with Jimmy.. Hope to see a second episode with the other features!!
Also congrats on #250.
I would really like to see your take on unit testing this piece of functionality.
An excellent 250th episode, congrats.
Got to say, while I've used devise without too many problems I do feel like you Jimmy, that something this important shouldn't be reliant on a third party library. Especially since authentication libraries seem to go in and out of favour all the time!
AFAIK, bcrypt hash has unique salt in it, which means that we don't need password_salt field.
Check out http://pastie.org/private/eqhgo4trb3owrsefupmxgg
Excellent timing! I just started working on an application that needed a simple/homemade authentication system. Keep up the great work! I probably wouldn't be a ruby on rails programmer without Railscasts!
Another great episode! Thanks Ryan, this makes much more sense for simple auth than requiring an engine.
I agree with other users, It'd be great to see a second part to this episode that covers account verification via email, password resetting, and a "remember me" option and others!
agree... would love to see a change password, remember me as well
I exactly do the same for my apps...this cast was an assurance that I am doing right.
I'm a rails noob so pardon me if this is obvious, but the one thing I see missing here is how you mark a page as needing to be authenticated.
@Ricky You can add a before_filter to your controller and have a method which may redirect a user if they're not logged in. That's how a lot of authentication solutions work.
Thanks for the great screencast (all of them)!
I agree with the others about having a
followup screencast on authentication.
In particular it would be nice to see
how to replace Authlogic's feature of
using "?user_credentials=xxxxx" for
API requests.
@Ryan BCrypt actually handles the salting for you automatically, so there is no need to store the sale separately.
Also, DHH is adding has_secure_password to rails which does all the password stuff for you using BCrypt. See:
https://github.com/rails/rails/commit/bd9dc4ff23ab1e185df6ccf35d6058c0a3d234ce
Hi Ryan,
I'm a bit off topic here, but where can I get the Textmate snippets you continuously use?
Thanks
As it happens, I was wrapping up simple username/password authentication into a gem when this RailsCast came out:
https://github.com/airblade/quo_vadis
Ryan, is there any need to flush passwords after save? I usually do something like this in my model:
after_save :flush_passwords
private
def flush_passwords
@password = @password_confirmation = nil
end
Needless or important?
Although there is some benefit in using bcrypt vs the traditional sha1 (actually the only one I can see is a much more cpu power required to generate each hash/password), I would still stick to sha1 when rolling out my own authentication solution. At least that's what I did in my simply_authenticate Rails plugin:
https://github.com/pjg/simply_authenticate/
(btw. there is some utf8 related problem with getting my username from github).
Great. This reassures me that my authentication layer is done correctly. Thanks for the tip with using "password.present?" I was just using "password" before.
Cheers
http://pastie.org/1501829
I added this to my application controller but it doesnt redirect if I'm not logged in
I would recommend using the interface bcrypt-ruby provides, instead of storing salts on your own. It stores the whole password + hash and host in a 60 character string. See the gist for rewritten encrypt and authenticate functions.
https://gist.github.com/799333
As an additional note, by using BCrypt::Password you also store the cost of the BCrypt hash in the string. This allows you to easily upgrade the cost as computing power advances. New users will get the newer cost, old users will keep the older cost (until they update their password). You could also give admins a higher cost hash, whilst keeping the same authenticate function.
Well, if you store all three data (eg. hash, salt, and cost), you can update users' password on the fly to increase cost of older encrypted passwords... at successful login:
class User
DEFAULT_COST = 15
def self.authenticate(email, password)
user = where(:email => email).first
if user
pw = BCrypt::Password.new(user.password_hash)
if pw == password
if pw.cost < DEFAULT_COST
user.password = password
user.save
end
user
else
nil
end
else
nil
end
end
...
end
@Piotr Kurek
you could do something like this in the application controller.
protected
def logged_in?
unless session[:user_id]
flash[:notice] = "You need to log in first."
redirect_to some_path
return false
else
return true
end
end
and then add a:
before_filter :logged_in?
to the controllers you want to protect.
PS: Nice screencast, congrats on 250th episode and +1 to a screencat on "account verification via email, password resetting, and a "remember me" option and others!"
+1
+1
Ryan,
thanks for the cast. I'm wondering why you used a flash message to alert the user of login failure, instead of using an error on the user model.
thanks,
Ian
Ryan,
I cannot view/download any video from you webside. media.railscasts.com is down! What happened?
Ryan
great idea to have a RC more on the metal this time, seamed like a very nice voyage in time :).
@Tim, I'd say paranoiac: your ivar is living during the request only! I admit though, that paranoiac is good when it comes to security ;). However, you also need to flush the params hash and maybe one or two other things. And probably the object in which you are flushing is going to die very, very soon after the save, and flushing before the save might not be a brilliant move ;).
Cheers
Robert
Hi Ryan,
I've never posted on your casts before... but I'm using them more and more now that I'm working with Rails on a daily basis...
So I think it's time to say thank you, and congrats on reaching #250 :)
Cheers
Olivier
Hello Ryan,
Please consider doing another episode using Devise's new omniauth integration support?
Thanks for all the hard work.
Bharat
Hey Ryan,
its all nice and nice BUT the harder parts about 0authentification are confirmation emails, password resets and generating (even non-html) mails. I hope you cover such a thing too then I can drop devise.
I fully agree... generators are better than engines for "unfinished" or to-by-heavily-modified parts of applications, still Devise is great ;)
in the asciicast, this line:
BCrypt::Engine.hash_secret(password, password_salt)
got changed to this:
BCrypt::Engine.hash_secret(password, password_hash)
pass it on to whomever does asciicasts i guess
@Chris password_hash can be used because the whole hash output from BCrypt also contains the salt and hash_secret is smart enough to detect that and just use the salt.
In fact, I would not even store the salt in the database any more. It's not needed and just increases the chance of bugs (duplicated storage of state is never a good idea).
@Pawe? what is your reasoning behind sticking to a non-standard and less secure solution? I can see that you might not want to change your existing old code, which is totally fine, but coming here to a tutorial about addition authentication from scratch to new applications, recommending an outdated and non-standard solution of the publicly accepted more secure standard solution seems not too intelligent.
Hi, is it really necessary leave the new action in session controller?
And btw, what means >> ||= ?
It is in the current_user method
@David Silva: Or equals. E.g. if `abc` is nil, then give it the value 1:
abc ||= 1
Hi, great episode.
Has anyone any idea about how to protect resources only for signed in users?
So thanks in advance!!!
Thanks a bunch for this one. I'd recommend dropping `resources :sessions` in favour of:
get 'log_in' => 'sessions#new'
post 'log_in' => 'sessions#create'
get 'log_out' => 'sessions#destroy'
This way, if the log in fails, the location doesn't change to `\sessions`.
+1
Due to the current lack of good, middle level books on Rails 3, learning Rails is very hard right now. I've scraped much information together from various sources. This cast had the added bonus of pulling everything I've learned together and giving a great beginning example of how it all works without a bunch of wasted words or code that just slow me down and confuse me. Thanks so much for this.
Can anyone help me understand why we created a sessions controller instead of putting the required actions in the users controller?
Great tutorial as always
You'll have *unencrypted* passwords in your logs! One more thing needs to be done, described here: http://asciicasts.com/episodes/224-controllers-in-rails-3
@50 - sorry, my mistake. It is set by default to hide this param.
ah yes, I also support the "Remember me" suggestion
Great episode!
Although, I too would love too see "Remember me" functionality.
Also, any reason you wouldn't use 'reset_session' instead of 'session[:user_id] = nil'?
Great episode!
I am also interested in the use reset_session - Should it be used just after login to change the session id. (The rails security guide mentions it as a way to prevent Session Fixation). Is there any issues\problem with using it?
Thank you!
Great episode, and I've studied it while creating Sorcery https://github.com/NoamB/sorcery
so thanks.
Thanks Ryan. Really brilliant and so very, very, very usable in every day life again as are all of your webcasts.
Sorcery is beautiful Noam. Thanks! Will use and fork asap.
Thank you so much for this!
Has anyone else had problems with bcrypt in windows?
Using rails 3.1 beta, and looking at my sqlite3 db, password hashes are not being saved.
Also is the bundle command mentioned just "bundle install"? or something more?
The bcrypt site says:
You
Doesn't work man. I followed your guide from A-Z.
uninitialized constant User::BCrypt
But i have included the gem in the gemfile and also ran the bundle command.
I'm getting the same problem.
gem 'bcrypt-ruby'
is in my gemfile and I've runbundle install
. Still no joy.I'm on OSX and running Ruby 1.9.2 (via Rbenv) and Rails 3.2.0.
Any ideas?
Ignore me. Got it sorted now. Added
gem 'bcrypt-ruby', :require => 'bcrypt'
to my gemfile and it works perfectly :)Thanks for that I was missing the require => 'bcrypt', this solved the problem for me too.
Thanks Ryan!
As usual, this is great!
I saw Maxsy's problems, too.
How can I fix it?
I can't get the password_validation to work properly. Whatever I enter in password_confirmation (even if it's not the same value as password), it'll save the user regardless :(
I'm seeing this behavior as well.
Maxsy and airyym,
Regarding the "uninitialized constant User::BCrypt" problem, you need to run "bundle install" in the terminal and then restart the rails server.
+1 for this. I've found it's sometimes tough remembering when to restart the server. Helped me out quite a bit.
As an aside, I feel it's absolutely important when creating any sorts of instructions for people that no steps be left out.
(Always assume the lowest level of experience.)
I have a few small simplifications that I'll share. I'm new to Rails (but not programming in general) so I apologize if anything is bad practice.
I forgot to add that with the above changes, you can remove the "password_salt" column.
Is there a particular obvious or elegant solution for automatically logging in a user upon registration? I do love to build from scratch as much as possible, but that is one thing which other gems do seem to provide that this does not.
@IanWhalen
automatically login after signup. Add the cookies line in the save condition.
I tried to add the cookie line because I would like to automatically log in after signup as well but I am getting the error:
NoMethodError in UsersController#create
undefined method `auth_token' for #< User:0x9a89d60 >
Am I missing something?
This tutorial is so freaking good. I think I just messed my pants a little.
Regarding my above modifications, I needed "if password.present?" in the encrypt_password expression, otherwise it will end up encrypting an empty string when you save the user. You could also do this in a before_create callback or similar so it doesn't have to run on every single save.
Also, the find_by_email and authenticate methods need to be separated out. I changed it to:
Which I don't really like since I find relying on short circuit evaluation to be iffy.
Ryan,
Thanks for this. I am running into some problems with my functional tests
1) Failure:
test_should_create_user(UsersControllerTest) [/test/functional/users_controller_test.rb:20]:
"User.count" didn't change by 1.
expected but was
.
2) Failure:
test_should_update_user(UsersControllerTest) [/test/functional/users_controller_test.rb:39]:
Expected response to be a <:success>, but was
I am also having a lot of trouble testing this (I am using rspec). I've been working all day on it, and if I figure it out I'll post it.
Okay, this is specifically for controller tests. Add this line to your authenticated tests:
I spent a long time messing with posting the sessions create action, stubs, etc, but finally realized it all just comes down to a cookie.
Thanks for this cast! Using it in my new app, but one question - how can I sign users in on signup instead of having them have to sign up and then sign in?
The hartl book talks about this, but he sets the authentication system up a bit differently and I'm having trouble translating it to this application!
This took me a while to figure out as well. You want to add the session line to your users controller and point it wherever you are taking your users once they log in.
Great tutorial! How do I validate an email from a specific domain before creating the new user? I tried :
validates_presence_of :email, :if => :in_co?
validates_uniqueness_of :email, :if => :in_co?
def in_co?
email == 'test@test.com'
end
but it didn't do anything.
Thanks in advance!
This is what I cam up with for the time being...
oops, wrong screen cast, I meant to put this in 3.1 Auth.
How secure is this authentication?
Is it safe enough to stick on the evil internet, on a Debian server using the Passenger mod for Apache?
What else do I need to consider?
Hi
Thanks for this really helped.
I would like to add Roles to the mix here. Thinking of only 3 different roles what would be the best solution?
Thanks again
Any suggestions on migrating an existing devise db to use this scratch-built solution?
This ended up being very simple. First of all, like other's have mentioned, it isn't required to save the salt. In fact, BCrypt now has much nicer helpers to create/check/save passwords. Here is a gist of the relevant lines from my User model. I use mongodb + mongoid and instead of renaming the encrypted_password field from devise (which is called the hash in the railcast) I just continue to use encrypted_password name (so no migration needed even for other db I think):
https://gist.github.com/1637700
Just tried working through this exercise - the User.save does NOT save the encrypted passwords to the table, although it does save all the other user info. No error messages are generated, but the save is obviously incomplete as it strays on the sign-in page!
Any ideas? I am running on Windows XP, using SQlite3 and Rails 3.2.1.
I did the follow steps and get this. I already put gem "bcrypt-ruby", :require => "bcrypt"
runned bundle install and restart my server.
Completed 500 Internal Server Error in 3ms
NameError (uninitialized constant User::Bcrypt):
app/models/user.rb:24:in
encrypt_password'
create'app/controllers/users_controller.rb:8:in
Rendered /home/hudson/.rvm/gems/ruby-1.9.3-p0/gems/actionpack-3.2.1/lib/action_dispatch/middleware/templates/rescues/_trace.erb (2.8ms)
Rendered /home/hudson/.rvm/gems/ruby-1.9.3-p0/gems/actionpack-3.2.1/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb (0.9ms)
Rendered /home/hudson/.rvm/gems/ruby-1.9.3-p0/gems/actionpack-3.2.1/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb within rescues/layout (5.3ms)
Hi, im getting "Can't mass-assign protected attributes" error.
I googled the problem so i added attr_accessible but it doesnt work anyways...Any ideas?
Thanks
Ursusurus,
Watch the REVISED version at http://railscasts.com/episodes/250-authentication-from-scratch-revised which was produced on 2/25/12. This version was produced 1/24/11 and is out of date.
Chip
I'm having issues deploying an app that is using this authentication method. It works fine on dev (mac) enviroment, however, it crashes on a Linux Ubuntu box.
I'm getting the following :
cache: [POST /users] invalidate, pass
Any idea ?
Thanks,
Ramos.
is password.present? necessary since validates_prescence_of :password, :on => :create already takes care of that?
As written, the
routes.rb
file has something like this:The
resources :sessions
line defines the full set of RESTful routes for SessionsController, but because of the first two lines, you don't need them.Am I correct?
I just began Rails 3 weeks back and for a beginner like me I cannot thank you enough for such detailed help on Authentication, with minor changes in Rails 2.3.8 I have implemented this successfully for my work, thank you.
Hi, thank you for this episode. I am using this authentication for a small Rails App and wanted to login from a iPhone App but i am not really sure which URL i have to call within my iOS App. It would be nice if someone could "push" me in the right direction.
Regards,
Sebastian
Did you ever figure this out? I'm also looking for a non-devise authentication solution for mobile apps.
rake sorcery:bootstrap is deprecated.
It is now rails generate sorcery:install
Sir what i have a question i hope you can help me
what if i have a signup page and it fails during the signing up how can i remain the URL of the page https://localhost:3000/signup and not https://localhost:3000/users
im using render action in the controller not redirect
i really need your help sir thank you
Hi guys, some of you can help me?
I have made all the video tutorial Rails Authentication Form Scratch Revised full.
But when i try to login with a email and one passowrd seems that my if statement has not evaluated the user.authenticate(params[:password]) because its never began login, in fact it is always evaluated the else statement of my if statement
this is my source code: SessionsController
def new
end
def create
user = User.find_by_email(params[:email])
if user && user.authenticate(params[:password_digest])
session[:user_id] = user.id
redirect_to root_path, :notice => "Bienvenido a Alertab"
else
flash.now.alert = "Tu correo o clave es invalida, intenta de nuevo"
render "new"
end
end
def destroy
session[:user_id] = nil
redirect_to root_path, :notice => "Session cerrada"
end
And here are my view source code:
Inicia Sección
<%=form_tag login_path do%>
<%= label_tag "Correo" %>
<%= email_field_tag :email, params[:email] %>
<%= label_tag "contraseña" %>
<%= password_field_tag :password %>
<%= submit_tag "Inicia Sección"%>
<%end%>
<%= link_to raw("← Regresar"), root_path %>
And here are console response:
Started POST "/login" for 127.0.0.1 at 2013-02-24 09:50:26 -0500
Processing by SessionsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"3xavsOw949fhLAIj/ypGq4eNpsQ2x3TSEx7/JS/898s=", "email"=>"my@gmail.com", "password"=>"[FILTERED]", "commit"=>"Inicia Sección"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."email" = 'my@gmail.com' LIMIT 1
Rendered sessions/new.html.erb within layouts/application (1.3ms)
Completed 200 OK in 13ms (Views: 11.7ms | ActiveRecord: 0.2ms)
Hi,
This sounds like a noob question, but I'm starting to learn Ruby. So I'm curious how this application is handling multiple user logging in/out. I figure that this revolves around using :user_id, but I'm confused regarding how :user_id is mapped to different user sessions (in particular :user_id is not defined anywhere before using, adding to my confusion). Hope someone can help to clear this up!
Thanks!
@Minh-Tue Every time you go to the server you get a session, and every session can have any arbitrary amount of objects stored on the server as session variables. Further, the server is smart enough to give every user their OWN session with a distinct ID. When you see things like 'session[:user_id] = user.id' it is just telling the server, essentially, "Create a new session variable named user_id and assign it the value of user.id." You can do this for anything you want, naming your variable anything you want -- for example, if your user table had a firstname column you could do session[:cool_name] = user.firstname and then use session[:cool_name] wherever you wanted to display the current user's firstname.
As far as multiple users go, the server writes a cookie to your browser with your unique session ID in it, which is how it keeps track of which session variable values to return to you so that you see your firstname, and not the value of session[:cool_name] for some other user.
I tried to change DB from sqlite3 to mysql2, but I got the next error:
ActiveRecord::RecordNotFound in UsersController#new
Couldn't find User with id=1
Rails.root: /home/epuente/proyectos/ruby/auth
Application Trace | Framework Trace | Full Trace
app/controllers/application_controller.rb:7:in
current_user'
_app_views_layouts_application_html_erb__11355537_84799900'app/views/layouts/application.html.erb:11:in
Is there any idea how to solve this?
Good One. Helped me a lot to get started with implementing authentication as I am a newbie to Ruby. Thanks a lot.
I was having this issue, and it could be due to the version of Rails I am using.
I get 'create new user' error: "ActiveModel::ForbiddenAttributesError when creating new user" and I looked it up on StackOverflow: http://stackoverflow.com/questions/17335329/activemodelforbiddenattributeserror-when-creating-new-user
That link solved my problem.
Also noticed (for Rails4):
layouts/application.html.erb, we should be using the instance variable instead for current_user, so it should be "<% if @current_user %>"
Actually - I am not sure, my 'current_user' is broken right now, the status is not stored, so I never see the "Log out" button, any ideas anyone? I can log-in and sign-up, just can't 'log-out' or get the current_user to work...
I replaced the salt with:
def self.authenticate(email, password)
user = find_by_email(email)
if user && BCrypt::Password.new(user.encrypted_password)==password
user
else
nil
end
end
def encrypt_password
if password.present?
self.encrypted_password = BCrypt::Password.create(password)
end
end
Nice tutorial!
What if I'd like to migrate my users to another authentication system such as 'Devise' later on?
Hi. I'm using this in Rails 4 and I have params like so:
def user_params
params.require(:user).permit(:email, :password_hash, :password_salt, :password)
end
Is it ok to put :password into params here? The code doesn't work (doesn't save) without it. Should be ok since "password" is not in database, but I'd like a confirmation.
In Rails 4.x, you can use has_secure_password to simplify the implementation. Checkout these articles, Authentication from Scratch : Register Feature and Login and Logout