diff --git a/generators/devise_install/templates/devise.rb b/generators/devise_install/templates/devise.rb index 07c5d613..a8c035e5 100644 --- a/generators/devise_install/templates/devise.rb +++ b/generators/devise_install/templates/devise.rb @@ -34,6 +34,9 @@ Devise.setup do |config| # The time the user will be remembered without asking for credentials again. # config.remember_for = 2.weeks + # The time interval to timeout the user session without activity. + # config.timeout = 10.minutes + # Configure the e-mail address which will be shown in DeviseMailer. # config.mailer_sender = "foo.bar@yourapp.com" diff --git a/lib/devise.rb b/lib/devise.rb index 27288df8..6b01b25d 100644 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -1,5 +1,5 @@ module Devise - ALL = [:authenticatable, :confirmable, :recoverable, :rememberable, :validatable].freeze + ALL = [:authenticatable, :confirmable, :recoverable, :rememberable, :timeoutable, :validatable].freeze # Maps controller names to devise modules CONTROLLERS = { @@ -45,6 +45,10 @@ module Devise mattr_accessor :confirm_within @@confirm_within = 0.days + # Time interval to timeout the user session without activity. + mattr_accessor :timeout + @@timeout = 10.minutes + # Used to define the password encryption algorithm. mattr_accessor :encryptor @@encryptor = :sha1 @@ -141,5 +145,4 @@ Warden::Manager.default_scope = nil require 'devise/strategies/base' require 'devise/serializers/base' - require 'devise/rails' diff --git a/lib/devise/hooks/confirmable.rb b/lib/devise/hooks/confirmable.rb index a6b577c9..71c897f1 100644 --- a/lib/devise/hooks/confirmable.rb +++ b/lib/devise/hooks/confirmable.rb @@ -6,7 +6,6 @@ Warden::Manager.after_set_user do |record, warden, options| if record && record.respond_to?(:active?) && !record.active? scope = options[:scope] warden.logout(scope) - if warden.winning_strategy # If winning strategy was set, this is being called after authenticate and # there is no need to force a redirect. diff --git a/lib/devise/hooks/timeoutable.rb b/lib/devise/hooks/timeoutable.rb new file mode 100644 index 00000000..3bcb552b --- /dev/null +++ b/lib/devise/hooks/timeoutable.rb @@ -0,0 +1,17 @@ +Warden::Manager.after_set_user do |record, warden, options| + if record.present? + scope = options[:scope] + # Current record may have already be logged out by another hook. + # For instance, Devise confirmable hook may have logged the record out. + # TODO: move this verify to warden: he should stop the hooks if the record + # is logged out by any of them. + if warden.authenticated?(scope) + last_request_at = warden.session(scope)['last_request_at'] + if last_request_at && last_request_at <= 10.minutes.ago.utc + warden.logout(scope) + throw :warden, :scope => scope, :message => :timeout + end + warden.session(scope)['last_request_at'] = Time.now.utc + end + end +end diff --git a/lib/devise/models/timeoutable.rb b/lib/devise/models/timeoutable.rb new file mode 100644 index 00000000..f18e1d07 --- /dev/null +++ b/lib/devise/models/timeoutable.rb @@ -0,0 +1,21 @@ +require 'devise/hooks/timeoutable' + +module Devise + module Models + + # Timeoutable + module Timeoutable + + def self.included(base) + base.class_eval do + extend ClassMethods + end + end + + module ClassMethods + end + + Devise::Models.config(self, :timeout) + end + end +end diff --git a/test/failure_app_test.rb b/test/failure_app_test.rb index 4f9a4e4c..7cbe259b 100644 --- a/test/failure_app_test.rb +++ b/test/failure_app_test.rb @@ -1,5 +1,5 @@ require 'test/test_helper' -require 'ostruct' +require 'ostruct' class FailureTest < ActiveSupport::TestCase @@ -22,6 +22,18 @@ class FailureTest < ActiveSupport::TestCase assert_equal '/users/sign_in?test=true', location end + test 'uses the given message' do + warden = OpenStruct.new(:message => 'Hello world') + location = call_failure('warden' => warden).second['Location'] + assert_equal '/users/sign_in?message=Hello+world', location + end + + test 'setup default url' do + Devise::FailureApp.default_url = 'test/sign_in' + location = call_failure('warden.options' => { :scope => nil }).second['Location'] + assert_equal '/test/sign_in?unauthenticated=true', location + end + test 'set content type to default text/plain' do assert_equal 'text/plain', call_failure.second['Content-Type'] end diff --git a/test/integration/confirmable_test.rb b/test/integration/confirmable_test.rb index 88b66b09..90bdb367 100644 --- a/test/integration/confirmable_test.rb +++ b/test/integration/confirmable_test.rb @@ -58,9 +58,9 @@ class ConfirmationTest < ActionController::IntegrationTest assert warden.authenticated?(:user) end - test 'not confirmed user and setup to block without confirmation should not be able to sign in' do + test 'not confirmed user with setup to block without confirmation should not be able to sign in' do Devise.confirm_within = 0 - user = sign_in_as_user(:confirm => false) + sign_in_as_user(:confirm => false) assert_contain 'You have to confirm your account before continuing' assert_not warden.authenticated?(:user) @@ -68,7 +68,7 @@ class ConfirmationTest < ActionController::IntegrationTest test 'not confirmed user but configured with some days to confirm should be able to sign in' do Devise.confirm_within = 1 - user = sign_in_as_user(:confirm => false) + sign_in_as_user(:confirm => false) assert_response :success assert warden.authenticated?(:user) diff --git a/test/integration/timeoutable_test.rb b/test/integration/timeoutable_test.rb new file mode 100644 index 00000000..4cce312a --- /dev/null +++ b/test/integration/timeoutable_test.rb @@ -0,0 +1,44 @@ +require 'test/test_helper' + +class SessionTimeoutTest < ActionController::IntegrationTest + + def last_request_at + @controller.user_session['last_request_at'] + end + + test 'set last request at in user session after each request' do + sign_in_as_user + old_last_request = last_request_at + assert_not_nil last_request_at + get users_path + assert_not_nil last_request_at + assert_not_equal old_last_request, last_request_at + end + + test 'time out user session after default limit time' do + sign_in_as_user + assert_response :success + assert warden.authenticated?(:user) + + # Setup last_request_at to timeout + get new_user_path + assert_not_nil last_request_at + + get users_path + assert_redirected_to new_user_session_path(:timeout => true) + assert_not warden.authenticated?(:user) + end + + test 'not time out user session before default limit time' do + user = sign_in_as_user + + # Setup last_request_at to timeout + get edit_user_path(user) + assert_not_nil last_request_at + + get users_path + assert_response :success + assert warden.authenticated?(:user) + end + +end diff --git a/test/models/timeoutable_test.rb b/test/models/timeoutable_test.rb new file mode 100644 index 00000000..7d1da8b1 --- /dev/null +++ b/test/models/timeoutable_test.rb @@ -0,0 +1,5 @@ +require 'test/test_helper' + +class TimeoutableTest < ActiveSupport::TestCase + +end diff --git a/test/models_test.rb b/test/models_test.rb index 4eefbc36..aa77e97f 100644 --- a/test/models_test.rb +++ b/test/models_test.rb @@ -16,6 +16,10 @@ class Rememberable < User devise :authenticatable, :rememberable end +class Timeoutable < User + devise :timeoutable +end + class Validatable < User devise :authenticatable, :validatable end @@ -32,7 +36,8 @@ class Configurable < User devise :all, :stretches => 15, :pepper => 'abcdef', :confirm_within => 5.days, - :remember_for => 7.days + :remember_for => 7.days, + :timeout => 15.minutes end class ActiveRecordTest < ActiveSupport::TestCase @@ -54,33 +59,38 @@ class ActiveRecordTest < ActiveSupport::TestCase end test 'include by default authenticatable only' do - assert_include_modules Authenticable, :authenticatable - assert_not_include_modules Authenticable, :confirmable, :recoverable, :rememberable, :validatable + assert_include_modules Authenticatable, :authenticatable + assert_not_include_modules Authenticatable, :confirmable, :recoverable, :rememberable, :timeoutable, :validatable end test 'add confirmable module only' do assert_include_modules Confirmable, :authenticatable, :confirmable - assert_not_include_modules Confirmable, :recoverable, :rememberable, :validatable + assert_not_include_modules Confirmable, :recoverable, :rememberable, :timeoutable, :validatable end test 'add recoverable module only' do assert_include_modules Recoverable, :authenticatable, :recoverable - assert_not_include_modules Recoverable, :confirmable, :rememberable, :validatable + assert_not_include_modules Recoverable, :confirmable, :rememberable, :timeoutable, :validatable end test 'add rememberable module only' do assert_include_modules Rememberable, :authenticatable, :rememberable - assert_not_include_modules Rememberable, :confirmable, :recoverable, :validatable + assert_not_include_modules Rememberable, :confirmable, :recoverable, :timeoutable, :validatable + end + + test 'add timeoutable module only' do + assert_include_modules Timeoutable, :authenticatable, :timeoutable + assert_not_include_modules Timeoutable, :confirmable, :recoverable, :rememberable, :validatable end test 'add validatable module only' do assert_include_modules Validatable, :authenticatable, :validatable - assert_not_include_modules Validatable, :confirmable, :recoverable, :rememberable + assert_not_include_modules Validatable, :confirmable, :recoverable, :timeoutable, :rememberable end test 'add all modules' do assert_include_modules Devisable, - :authenticatable, :confirmable, :recoverable, :rememberable, :validatable + :authenticatable, :confirmable, :recoverable, :rememberable, :timeoutable, :validatable end test 'configure modules with except option' do @@ -104,6 +114,10 @@ class ActiveRecordTest < ActiveSupport::TestCase assert_equal 7.days, Configurable.remember_for end + test 'set a default value for timeout' do + assert_equal 15.minutes, Configurable.new.timeout + end + test 'set null fields on migrations' do Admin.create! end diff --git a/test/rails_app/app/controllers/users_controller.rb b/test/rails_app/app/controllers/users_controller.rb index e9cc204f..1d4617e0 100644 --- a/test/rails_app/app/controllers/users_controller.rb +++ b/test/rails_app/app/controllers/users_controller.rb @@ -4,4 +4,14 @@ class UsersController < ApplicationController def index user_session[:cart] = "Cart" end + + def new + user_session['last_request_at'] = 11.minutes.ago.utc + render :text => 'New user!' + end + + def edit + user_session['last_request_at'] = 9.minutes.ago.utc + render :text => 'Edit user!' + end end diff --git a/test/rails_app/config/routes.rb b/test/rails_app/config/routes.rb index faf1feeb..0fda3a51 100644 --- a/test/rails_app/config/routes.rb +++ b/test/rails_app/config/routes.rb @@ -8,7 +8,7 @@ ActionController::Routing::Routes.draw do |map| :path_prefix => '/:locale', :requirements => { :extra => 'value' } - map.resources :users, :only => :index + map.resources :users, :only => [:index, :new, :edit] map.resources :admins, :only => :index map.root :controller => :home diff --git a/test/test_helper.rb b/test/test_helper.rb index f82f8711..1e311d1e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -33,6 +33,7 @@ end Webrat.configure do |config| config.mode = :rails + config.open_error_files = false end class ActiveSupport::TestCase