Creating rememberable module.

This commit is contained in:
Carlos A. da Silva
2009-10-20 00:31:33 -02:00
parent fc08a7c5ac
commit 28a7f78fba
16 changed files with 319 additions and 37 deletions

View File

@@ -7,6 +7,10 @@
<p><%= f.label :password %></p>
<p><%= f.password_field :password %></p>
<% if devise_mapping.rememberable? -%>
<p><%= f.check_box :remember_me %> <%= f.label :remember_me %></p>
<% end -%>
<p><%= f.submit "Sign in" %></p>
<% end -%>

View File

@@ -1,12 +1,14 @@
begin
require 'warden'
rescue
gem 'hassox-warden'
require 'warden'
end
#begin
# require 'warden'
#rescue
# gem 'hassox-warden'
# require 'warden'
#end
require File.join(File.dirname(__FILE__), '..', 'warden', 'lib', 'warden')
module Devise
ALL = [:authenticable, :confirmable, :recoverable, :validatable].freeze
ALL = [:authenticable, :confirmable, :recoverable, :rememberable, :validatable].freeze
# Maps controller names to devise modules
CONTROLLERS = {
@@ -17,7 +19,6 @@ module Devise
end
require 'devise/warden'
require 'devise/mapping'
require 'devise/routes'
# Ensure to include Devise modules only after Rails initialization.

View File

@@ -40,6 +40,7 @@ module Devise
# Sign out based on scope.
def sign_out(scope, *args)
warden.user(scope) # Without loading user here, before_logout hook is not called
warden.raw_session.inspect # Without this inspect here. The session does not clear.
warden.logout(scope, *args)
end

View File

@@ -0,0 +1,27 @@
# After authenticate hook to verify if the user in the given scope asked to be
# remembered while he does not sign out. Generates a new remember token for
# that specific user and adds a cookie with this user info to sign in this user
# automatically without asking for credentials. Refer to rememberable strategy
# for more info.
Warden::Manager.after_authentication do |record, auth, options|
scope = options[:scope]
remember_me = auth.params[scope].try(:fetch, :remember_me, nil)
remember_me = remember_me == '1' || remember_me == 'true'
mapping = Devise.mappings[scope]
if remember_me && mapping.present? && mapping.rememberable?
record.remember_me!
auth.cookies['remember_token'] = record.class.serialize_into_cookie(record)
end
end
# Before logout hook to forget the user in the given scope, only if rememberable
# is activated for this scope. Also clear remember token to ensure the user
# won't be remembered again.
# TODO: verify warden to call before_logout when @users are not loaded yet.
Warden::Manager.before_logout do |record, auth, scope|
mapping = Devise.mappings[scope]
if mapping.present? && mapping.rememberable?
record.forget_me!
auth.cookies['remember_token'] = nil
end
end

View File

@@ -9,7 +9,7 @@ module Devise
# map.devise_for :users
# mapping = Devise.mappings[:user]
#
# mapping.name #=> :user
# mapping.name #=> :user
# # is the scope used in controllers and warden, given in the route as :singular.
#
# mapping.as #=> "users"
@@ -57,7 +57,7 @@ module Devise
# self.for.include?(:confirmable)
# end
#
CONTROLLERS.values.each do |m|
ALL.each do |m|
class_eval <<-METHOD, __FILE__, __LINE__
def #{m}?
self.for.include?(:#{m})

View File

@@ -0,0 +1,62 @@
require 'digest/sha1'
module Devise
module Models
# Rememberable Module
module Rememberable
def self.included(base)
base.class_eval do
extend ClassMethods
# Remember me option available in after_authentication hook.
attr_accessor :remember_me
attr_accessible :remember_me
end
end
# Generate a new remember token and save the record without validations.
def remember_me!
self.remember_token = friendly_token
save(false)
end
# Removes the remember token only if it exists, and save the record
# without validations.
def forget_me!
if remember_token?
self.remember_token = nil
save(false)
end
end
# Checks whether the incoming token matches or not with the record token.
def valid_remember_token?(token)
remember_token.present? && remember_token == token
end
module ClassMethods
# Attempts to remember the user through it's id and remember_token.
# Returns the user if one is found and the token is valid, otherwise nil.
# Attributes must contain :id and :remember_token
def remember_me!(attributes={})
rememberable = find_by_id(attributes[:id])
rememberable if rememberable.try(:valid_remember_token?, attributes[:remember_token])
end
# Create the cookie key using the record id and remember_token
def serialize_into_cookie(record)
"#{record.id}::#{record.remember_token}"
end
# Recreate the user based on the stored cookie
def serialize_from_cookie(cookie)
record_id, remember_token = cookie.split('::')
remember_me!(:id => record_id, :remember_token => remember_token)
end
end
end
end
end

View File

@@ -16,28 +16,30 @@ module Devise
end
end
# Find the attributes for the current mapping.
def attributes
@attributes ||= request.params[scope]
end
private
# Check for the right keys.
def valid_attributes?
attributes && attributes[:email].present? && attributes[:password].present?
end
# Find the attributes for the current mapping.
def attributes
@attributes ||= params[scope]
end
# Stores requested uri to redirect the user after signing in. We cannot use
# scoped session provided by warden here, since the user is not authenticated
# yet, but we still need to store the uri based on scope, so different scopes
# would never use the same uri to redirect.
def store_location
session[:"#{mapping.name}.return_to"] = request.request_uri if request.get?
end
# Check for the right keys.
def valid_attributes?
attributes && attributes[:email].present? && attributes[:password].present?
end
# Create path to sign in the resource
def sign_in_path
"/#{mapping.as}/#{mapping.path_names[:sign_in]}"
end
# Stores requested uri to redirect the user after signing in. We cannot use
# scoped session provided by warden here, since the user is not authenticated
# yet, but we still need to store the uri based on scope, so different scopes
# would never use the same uri to redirect.
def store_location
session[:"#{mapping.name}.return_to"] = request.request_uri if request.get?
end
# Create path to sign in the resource
def sign_in_path
"/#{mapping.as}/#{mapping.path_names[:sign_in]}"
end
end
end
end

View File

@@ -0,0 +1,33 @@
module Devise
module Strategies
# Remember the user through the remember token. This strategy is responsible
# to verify whether there is a cookie with the remember token, and to
# recreate the user from this cookie if it exists. Must be called *before*
# authenticable.
class Rememberable < Devise::Strategies::Base
# A valid strategy for rememberable needs a remember token in the cookies.
def valid?
super && remember_me_cookie.present?
end
# To authenticate a user we deserialize the cookie and attempt finding
# the record in the database. If the attempt fails, we pass to another
# strategy handle the authentication.
def authenticate!
if resource = mapping.to.serialize_from_cookie(remember_me_cookie)
success!(resource)
else
pass
end
end
private
# Accessor for remember cookie
def remember_me_cookie
cookies['remember_token']
end
end
end
end

View File

@@ -19,6 +19,11 @@ module Warden::Mixins::Common
raw_session.inspect # why do I have to inspect it to get it to clear?
raw_session.clear
end
# Proxy to request cookies
def cookies
request.cookies
end
end
# Session Serialization in. This block determines how the user will be stored
@@ -41,9 +46,13 @@ end
# Adds Warden Manager to Rails middleware stack, configuring default devise
# strategy and also the controller who will manage not authenticated users.
Rails.configuration.middleware.use Warden::Manager do |manager|
manager.default_strategies :authenticable
manager.default_strategies :rememberable, :authenticable
manager.failure_app = SessionsController
end
# Setup devise strategies for Warden
Warden::Strategies.add(:rememberable, Devise::Strategies::Rememberable)
Warden::Strategies.add(:authenticable, Devise::Strategies::Authenticable)
# Require rememberable hooks
require 'devise/hooks/rememberable'

View File

@@ -48,6 +48,7 @@ class ControllerAuthenticableTest < ActionController::TestCase
end
test 'proxy logout to warden' do
@mock_warden.expects(:user).with(:user).returns(true)
@mock_warden.expects(:logout).with(:user).returns(true)
@controller.sign_out(:user)
end

View File

@@ -0,0 +1,56 @@
require 'test/test_helper'
class RememberMeTest < ActionController::IntegrationTest
def create_user_and_remember(add_to_token='')
user = create_user
user.remember_me!
cookies['remember_token'] = User.serialize_into_cookie(user) + add_to_token
user
end
test 'do not remember the user if he has not checked remember me option' do
user = sign_in_as_user
assert_nil user.reload.remember_token
end
test 'generate remember token after sign in' do
user = sign_in_as_user :remember_me => true
assert_not_nil user.reload.remember_token
end
test 'remember the user before sign in' do
user = create_user_and_remember
get users_path
assert_response :success
assert warden.authenticated?(:user)
assert warden.user(:user) == user
end
test 'do not remember with invalid token' do
user = create_user_and_remember('add')
get users_path
assert_response :success
assert_not warden.authenticated?(:user)
end
test 'forget the user before sign out' do
user = create_user_and_remember
get users_path
assert warden.authenticated?(:user)
get destroy_user_session_path
assert_not warden.authenticated?(:user)
assert_nil user.reload.remember_token
end
test 'do not remember the user anymore after forget' do
user = create_user_and_remember
get users_path
assert warden.authenticated?(:user)
get destroy_user_session_path
get users_path
assert_not warden.authenticated?(:user)
end
end

View File

@@ -60,10 +60,12 @@ class MapTest < ActiveSupport::TestCase
assert mapping.authenticable?
assert mapping.confirmable?
assert mapping.recoverable?
assert mapping.rememberable?
mapping = Devise.mappings[:admin]
assert mapping.authenticable?
assert_not mapping.confirmable?
assert_not mapping.recoverable?
assert_not mapping.rememberable?
end
end

View File

@@ -0,0 +1,76 @@
require 'test/test_helper'
class RememberableTest < ActiveSupport::TestCase
test 'should respond to remember_me attribute' do
user = new_user
assert user.respond_to?(:remember_me)
end
test 'should have remember_me accessible' do
assert field_accessible?(:remember_me)
end
test 'remember_me should generate a new token and save the record without validating' do
user = create_user
user.expects(:valid?).never
token = user.remember_token
user.remember_me!
assert_not_equal token, user.remember_token
assert_not user.changed?
end
test 'forget_me should clear remember token and save the record without validating' do
user = create_user
user.remember_me!
assert_not_nil user.remember_token
user.expects(:valid?).never
user.forget_me!
assert_nil user.remember_token
assert_not user.changed?
end
test 'forget should do nothing if no remember token exists' do
user = create_user
user.expects(:save).never
user.forget_me!
end
test 'valid remember token' do
user = create_user
assert_not user.valid_remember_token?(user.remember_token)
user.remember_me!
assert user.valid_remember_token?(user.remember_token)
user.forget_me!
assert_not user.valid_remember_token?(user.remember_token)
end
test 'find a user by its id and remember it if the token is valid' do
user = create_user
user.remember_me!
remembered_user = User.remember_me!(:id => user.id, :remember_token => user.remember_token)
assert_not_nil remembered_user
assert_equal remembered_user, user
end
test 'remember me should return nil if no user is found' do
assert_nil User.remember_me!(:id => 0)
end
test 'remember me return nil if is a valid user with invalid token' do
user = create_user
assert_nil User.remember_me!(:id => user.id, :remember_token => 'invalid_token')
end
test 'serialize into cookie' do
user = create_user
user.remember_me!
assert_equal "#{user.id}::#{user.remember_token}", User.serialize_into_cookie(user)
end
test 'serialize from cookie' do
user = create_user
user.remember_me!
assert_equal user, User.serialize_from_cookie("#{user.id}::#{user.remember_token}")
end
end

View File

@@ -1,3 +1,3 @@
class Admin < ActiveRecord::Base
devise :all, :except => [:recoverable, :confirmable]
devise :all, :except => [:recoverable, :confirmable, :rememberable]
end

View File

@@ -24,21 +24,24 @@ class ActionController::IntegrationTest
end
def sign_in_as_user(options={}, &block)
create_user(options)
user = create_user(options)
visit new_user_session_path unless options[:visit] == false
fill_in 'email', :with => 'user@test.com'
fill_in 'password', :with => '123456'
check 'remember me' if options[:remember_me] == true
yield if block_given?
click_button 'Sign In'
user
end
def sign_in_as_admin(options={}, &block)
create_admin(options)
admin = create_admin(options)
visit new_admin_session_path unless options[:visit] == false
fill_in 'email', :with => 'admin@test.com'
fill_in 'password', :with => '123456'
yield if block_given?
click_button 'Sign In'
admin
end
# Fix assert_redirect_to in integration sessions because they don't take into

View File

@@ -19,10 +19,15 @@ ActiveRecord::Schema.define(:version => 1) do
t.string :email, :null => false
t.string :encrypted_password, :null => false
t.string :password_salt, :null => false
t.string :confirmation_token
t.datetime :confirmation_sent_at
t.datetime :confirmed_at
t.string :reset_password_token
if table == :users
t.string :confirmation_token
t.datetime :confirmation_sent_at
t.datetime :confirmed_at
t.string :reset_password_token
t.string :remember_token
end
t.timestamps
end
end
end