mirror of
https://github.com/heartcombo/devise.git
synced 2022-11-09 12:18:31 -05:00
Merge branch 'session_timeout'
This commit is contained in:
commit
7933d203e3
15 changed files with 225 additions and 37 deletions
|
@ -34,6 +34,10 @@ Devise.setup do |config|
|
|||
# The time the user will be remembered without asking for credentials again.
|
||||
# config.remember_for = 2.weeks
|
||||
|
||||
# The time you want to timeout the user session without activity. After this
|
||||
# time the user will be asked for credentials again.
|
||||
# config.timeout = 10.minutes
|
||||
|
||||
# Configure the e-mail address which will be shown in DeviseMailer.
|
||||
# config.mailer_sender = "foo.bar@yourapp.com"
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
@ -14,7 +14,7 @@ module Devise
|
|||
|
||||
# Maps the messages types that are used in flash message. This array is not
|
||||
# frozen, so you can add messages from your own strategies.
|
||||
FLASH_MESSAGES = [ :unauthenticated, :unconfirmed, :invalid ]
|
||||
FLASH_MESSAGES = [ :unauthenticated, :unconfirmed, :invalid, :timeout ]
|
||||
|
||||
# Declare encryptors length which are used in migrations.
|
||||
ENCRYPTORS_LENGTH = {
|
||||
|
@ -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 = 30.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'
|
||||
|
|
|
@ -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.
|
||||
|
|
19
lib/devise/hooks/timeoutable.rb
Normal file
19
lib/devise/hooks/timeoutable.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Each time a record is set we check whether it's session has already timed out
|
||||
# or not, based on last request time. If so, the record is logged out and
|
||||
# redirected to the sign in page. Also, each time the request comes and the
|
||||
# record is set, we set the last request time inside it's scoped session to
|
||||
# verify timeout in the following request.
|
||||
Warden::Manager.after_set_user do |record, warden, options|
|
||||
if record && record.respond_to?(:timeout?)
|
||||
scope = options[:scope]
|
||||
# Record may have already been logged out by another hook (ie confirmable).
|
||||
if warden.authenticated?(scope)
|
||||
last_request_at = warden.session(scope)['last_request_at']
|
||||
if record.timeout?(last_request_at)
|
||||
warden.logout(scope)
|
||||
throw :warden, :scope => scope, :message => :timeout
|
||||
end
|
||||
warden.session(scope)['last_request_at'] = Time.now.utc
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,6 +6,7 @@ en:
|
|||
unauthenticated: 'You need to sign in or sign up before continuing.'
|
||||
unconfirmed: 'You have to confirm your account before continuing.'
|
||||
invalid: 'Invalid email or password.'
|
||||
timeout: 'Your session expired, please sign in again to continue.'
|
||||
passwords:
|
||||
send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.'
|
||||
updated: 'Your password was changed successfully. You are now signed in.'
|
||||
|
|
30
lib/devise/models/timeoutable.rb
Normal file
30
lib/devise/models/timeoutable.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
require 'devise/hooks/timeoutable'
|
||||
|
||||
module Devise
|
||||
module Models
|
||||
|
||||
# Timeoutable takes care of veryfing whether a user session has already
|
||||
# expired or not. When a session expires after the configured time, the user
|
||||
# will be asked for credentials again, it means, he/she will be redirected
|
||||
# to the sign in page.
|
||||
#
|
||||
# Configuration:
|
||||
#
|
||||
# timeout: the time you want to timeout the user session without activity.
|
||||
module Timeoutable
|
||||
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
# Checks whether the user session has expired based on configured time.
|
||||
def timeout?(last_access)
|
||||
last_access && last_access <= self.class.timeout.ago.utc
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
Devise::Models.config(self, :timeout)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -102,17 +102,14 @@ class AuthenticationTest < ActionController::IntegrationTest
|
|||
end
|
||||
|
||||
test 'error message is configurable by resource name' do
|
||||
begin
|
||||
I18n.backend.store_translations(:en, :devise => { :sessions =>
|
||||
{ :admin => { :invalid => "Invalid credentials" } } })
|
||||
|
||||
store_translations :en, :devise => {
|
||||
:sessions => { :admin => { :invalid => "Invalid credentials" } }
|
||||
} do
|
||||
sign_in_as_admin do
|
||||
fill_in 'password', :with => 'abcdef'
|
||||
end
|
||||
|
||||
assert_contain 'Invalid credentials'
|
||||
ensure
|
||||
I18n.reload!
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -58,32 +58,30 @@ 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
|
||||
Devise.confirm_within = 0
|
||||
user = sign_in_as_user(:confirm => false)
|
||||
test 'not confirmed user with setup to block without confirmation should not be able to sign in' do
|
||||
swap Devise, :confirm_within => 0.days do
|
||||
sign_in_as_user(:confirm => false)
|
||||
|
||||
assert_contain 'You have to confirm your account before continuing'
|
||||
assert_not warden.authenticated?(:user)
|
||||
assert_contain 'You have to confirm your account before continuing'
|
||||
assert_not warden.authenticated?(:user)
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
swap Devise, :confirm_within => 1.day do
|
||||
sign_in_as_user(:confirm => false)
|
||||
|
||||
assert_response :success
|
||||
assert warden.authenticated?(:user)
|
||||
assert_response :success
|
||||
assert warden.authenticated?(:user)
|
||||
end
|
||||
end
|
||||
|
||||
test 'error message is configurable by resource name' do
|
||||
begin
|
||||
I18n.backend.store_translations(:en, :devise => { :sessions =>
|
||||
{ :admin => { :unconfirmed => "Not confirmed user" } } })
|
||||
|
||||
store_translations :en, :devise => {
|
||||
:sessions => { :admin => { :unconfirmed => "Not confirmed user" } }
|
||||
} do
|
||||
get new_admin_session_path(:unconfirmed => true)
|
||||
|
||||
assert_contain 'Not confirmed user'
|
||||
ensure
|
||||
I18n.reload!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
73
test/integration/timeoutable_test.rb
Normal file
73
test/integration/timeoutable_test.rb
Normal file
|
@ -0,0 +1,73 @@
|
|||
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 '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
|
||||
|
||||
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 'user configured timeout limit' do
|
||||
swap Devise, :timeout => 8.minutes do
|
||||
user = sign_in_as_user
|
||||
|
||||
# Setup last_request_at to timeout
|
||||
get edit_user_path(user)
|
||||
assert_not_nil last_request_at
|
||||
assert_response :success
|
||||
assert warden.authenticated?(:user)
|
||||
|
||||
get users_path
|
||||
assert_redirected_to new_user_session_path(:timeout => true)
|
||||
assert_not warden.authenticated?(:user)
|
||||
end
|
||||
end
|
||||
|
||||
test 'error message with i18n' do
|
||||
store_translations :en, :devise => {
|
||||
:sessions => { :user => { :timeout => 'Session expired!' } }
|
||||
} do
|
||||
sign_in_as_user
|
||||
# Setup last_request_at to timeout
|
||||
get new_user_path
|
||||
get users_path
|
||||
follow_redirect!
|
||||
assert_contain 'Session expired!'
|
||||
end
|
||||
end
|
||||
|
||||
end
|
27
test/models/timeoutable_test.rb
Normal file
27
test/models/timeoutable_test.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
require 'test/test_helper'
|
||||
|
||||
class TimeoutableTest < ActiveSupport::TestCase
|
||||
|
||||
test 'should be expired' do
|
||||
assert new_user.timeout?(11.minutes.ago)
|
||||
end
|
||||
|
||||
test 'should not be expired' do
|
||||
assert_not new_user.timeout?(9.minutes.ago)
|
||||
end
|
||||
|
||||
test 'should not be expired when params is nil' do
|
||||
assert_not new_user.timeout?(nil)
|
||||
end
|
||||
|
||||
test 'fallback to Devise config option' do
|
||||
swap Devise, :timeout => 1.minute do
|
||||
user = new_user
|
||||
assert user.timeout?(2.minutes.ago)
|
||||
assert_not user.timeout?(30.seconds.ago)
|
||||
Devise.timeout = 5.minutes
|
||||
assert_not user.timeout?(2.minutes.ago)
|
||||
assert user.timeout?(6.minutes.ago)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
require 'test/test_helper'
|
||||
|
||||
class Authenticable < User
|
||||
class Authenticatable < User
|
||||
devise :authenticatable
|
||||
end
|
||||
|
||||
|
@ -16,6 +16,10 @@ class Rememberable < User
|
|||
devise :authenticatable, :rememberable
|
||||
end
|
||||
|
||||
class Timeoutable < User
|
||||
devise :authenticatable, :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.timeout
|
||||
end
|
||||
|
||||
test 'set null fields on migrations' do
|
||||
Admin.create!
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ end
|
|||
|
||||
Webrat.configure do |config|
|
||||
config.mode = :rails
|
||||
config.open_error_files = false
|
||||
end
|
||||
|
||||
class ActiveSupport::TestCase
|
||||
|
|
Loading…
Reference in a new issue