9.3 sign in success.
1. we will first finish the create action:
def create
user = User.authenticate(params[:session][:email],
params[:session][:password])
if user.nil?
flash.now[:error] = "Invalid email/password combination."
@title = "Sign in"
render 'new'
else
sign_in user
redirect_to user
end
end
for the below, we will write sign_in method!!!
2. start from TDD again!!!
describe SessionsController do
.
.
.
describe "POST 'create'" do
.
.
.
describe "with valid email and password" do
before(:each) do
@user = Factory(:user)
@attr = { :email => @user.email, :password => @user.password }
end
it "should sign the user in" do
post :create, :session => @attr
# Fill in with tests for a signed-in user.
end
it "should redirect to the user show page" do
post :create, :session => @attr
response.should redirect_to(user_path(@user))
end
end
end
end
3. in this section, we will need some methods that are need to in both controller and view,
for view, we can define the method in SessionHelpr, (the method in all helpers are viewable for all views.)
to make controller see this method, we just need to include this module into the controller.
class ApplicationController < ActionController::Base
protect_from_forgery
include SessionsHelper
end
4. session and cookies:
Because HTTP is a stateless protocol, web applications requiring user signin must implement a way to track each user’s progress from page to page. One technique for maintaining the user signin status is to use a traditional Rails session (via the special session function) to store a remember token equal to the user’s id:
session[:remember_token] = user.id
This session object makes the user id available from page to page by storing it in a cookie that expires upon browser close. On each page, the application can simply call
User.find_by_id(session[:remember_token])
to retrieve the user. Because of the way Rails handles sessions, this process is secure; if a malicious user tries to spoof the user id, Rails will detect a mismatch based on a special session id generated for each session.
but if we want a permanant token, so that it still work after browser close, then just include user id in the session[:remember_token] is not secure enough, hacker can use user.id to do bad, so we can include user.salt
[user.id, user.salt]
also, a permanant token has another security hole, hacker can get it by inspecting the user browser cookie.
the solution is when user change his password, we change the cookie.
4. now we are ready to implement the sign_in function:
a. we will put place a remember_token as a cookie on the user's browser.
b. app will use this token to find the user record from database when user move from page to page.
c. session will have a current_user.
module SessionHelper
def sign_in(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
self.current_user = user
end
end
this part of code revealed the cookies utility supplied by rails.
we can use cookies as if it were a hash.
each element in the cookie is itself a hash of two elements, a value and an optional expired data.
for example:
cookies[:remember_token] = {:value => user.id, :expires => 20.years.from_now.utc}
then we can retrieve the user in this way:
User.find_by_id(cookies[:remember_token])
of course, cookies is not a real hash, since assigning to cookies actually saves a piece of text on the browser, but the beauty of rails is let you forget about the detail, and concentrate on writing the app.
since just using user.id is not secure, before rails 3, rails use a secure token associated with the user model.
since this is so commen, rails 3 now implements it for us using:
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
using permanent, cause rails to set the expiration to 20.years.from_now.
and signed makes the cookie secure, so that the user's id is never exposed in the browser.
5. since ruby class is open, rails add many handy method to ruby classes:
1. year.from_now
10.weeks.ago
1. kilobyte ===> 1024
5.megabytes ====> 5242880
These two are useful when validate the file size of uploads, like images.
6. current user.
now we will look at how to get and set session's current user.
since this method is shared by the controller, so the self is the controller object.
the purpose of this line is to create current user, accessible in both controllers and views.
so that we can use <%= current_user.name %> directly in views, and
ok TDD first:
it "should sign the user in" do
post :create, :session => @attr
controller.current_user.should == @user
controller.should be_signed_in
end
note,
controller.should be_signed_in
is equivalent to
controller.signed_in?.should be_true
now, we can implement
so we need to define a function, which is an assignment.
def current_user=(user)
@current_user = user
end
and very natrual, we need to define a get method too:
def current_user
@current_user
end
if we did this, it is actually equivalent of using:
attr_accessor :current_user
but this is not what we want, because @current_user will disappear for next request which will generate a new controller object.
to fix this, we need to get the user from the token in the cookie in the get method.
def current_user
@current_user ||= user_from_remember_token
end
private
def user_from_remember_token
User.authenticate_with_salt(*remember_token)
end
def remember_token
cookies.signed[:remember_token] || [nil, nil]
end
end
a. in this part of code, there is one totally new thing:
*remember_token
the "*" operator, allows us to use a two element array as an argument to a method expectiong two vars.
so
def foo(bar, baz)
bar + baz
end
foo(*[1,2])
====> 3
the reason of using "*" is that
authenticate_with_salt in general should accept two arguments.
(id, user_salt)
b. another thing is:
cookies.signed[:remember_token] || [nil, nil]
c. the next step is to define the
authenticate_with_salt method in user.rb
def self.authenticate_with_salt(id, cookie_salt)
user = find_by_id(id)
(user && user.salt == cookie_salt) ? user : nil
end
(user && user.salt == cookie_salt) ? user : nil
this is very traditional rails code, you really should get used to it!!!
d. we still need to define the signed_in method.
def signed_in?
!current_user.nil?
end