devise_session_expirable 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +14 -5
- data/VERSION +1 -1
- data/devise_session_expirable.gemspec +2 -2
- data/lib/devise_session_expirable.rb +4 -9
- data/lib/devise_session_expirable/delegator.rb +13 -0
- data/lib/devise_session_expirable/devise_extensions.rb +38 -0
- data/lib/devise_session_expirable/failure_app.rb +21 -0
- data/lib/devise_session_expirable/hook.rb +0 -41
- data/lib/devise_session_expirable/model.rb +0 -5
- data/lib/devise_session_expirable/warden_extensions.rb +46 -0
- data/test/integration/session_expirable_test.rb +20 -22
- metadata +9 -2
data/README.rdoc
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
All good things come to an end: At least that's how it's supposed to work.
|
|
4
4
|
|
|
5
|
-
SessionExpirable is an enhanced version of Devise's +timeoutable+ module
|
|
5
|
+
SessionExpirable is an enhanced version of Devise[https://github.com/plataformatec/devise]'s +timeoutable+ module
|
|
6
6
|
that ensures that no session is allowed to last forever.
|
|
7
7
|
|
|
8
8
|
Like Devise +timeoutable+, SessionExpirable adds a timestamp to sessions
|
|
@@ -20,10 +20,19 @@ never be considered as having timed out.
|
|
|
20
20
|
In order to prevent the abuse of non-expiring sessions, SessionExpirable
|
|
21
21
|
treats sessions without timestamps as having already expired.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
Another difference is that in the case of a timeout, +timeoutable+
|
|
24
|
+
prevents other authentication strategies from being tried, although
|
|
25
|
+
it has custom logic to allow Devise +rememberable+ to work in spite of a
|
|
26
|
+
timed-out session.
|
|
27
|
+
|
|
28
|
+
SessionExpirable does not allow a timed-out session to interfere with any
|
|
29
|
+
authentication strategies that are configured. One consequence is that
|
|
30
|
+
SessionExpirable does not support invalidation of authentication tokens
|
|
31
|
+
from the Devise +token_authenticatable+ module when a request with a
|
|
32
|
+
valid authentication token is accompanied by an expired session.
|
|
33
|
+
|
|
34
|
+
One final difference is that SessionExpirable does not invoke the Devise
|
|
35
|
+
FailureApp for requests that do not require authentication.
|
|
27
36
|
|
|
28
37
|
== Configuration
|
|
29
38
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.2.0
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
Gem::Specification.new do |s|
|
|
7
7
|
s.name = "devise_session_expirable"
|
|
8
|
-
s.version = "0.
|
|
8
|
+
s.version = "0.2.0"
|
|
9
9
|
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
11
11
|
s.authors = ["Riley Lynch"]
|
|
12
|
-
s.date = "2013-
|
|
12
|
+
s.date = "2013-03-11"
|
|
13
13
|
s.description = "devise_session_expirable is an enhanced version of devise's timeoutable module that ensures that no session is allowed to last forever."
|
|
14
14
|
s.email = "oss+expirable@teleological.net"
|
|
15
15
|
s.extra_rdoc_files = [
|
|
@@ -3,16 +3,11 @@ require 'active_support/core_ext/module/attribute_accessors'
|
|
|
3
3
|
require 'active_support/concern'
|
|
4
4
|
require 'devise'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
mattr_accessor :default_last_request_at
|
|
9
|
-
@default_last_request_at = nil
|
|
10
|
-
|
|
11
|
-
add_module :session_expirable,
|
|
12
|
-
:model => 'devise_session_expirable/model'
|
|
13
|
-
|
|
14
|
-
end
|
|
6
|
+
require 'devise_session_expirable/warden_extensions'
|
|
7
|
+
require 'devise_session_expirable/devise_extensions'
|
|
15
8
|
|
|
16
9
|
module DeviseSessionExpirable #:nodoc:
|
|
10
|
+
autoload :Delegator, 'devise_session_expirable/delegator'
|
|
11
|
+
autoload :FailureApp, 'devise_session_expirable/failure_app'
|
|
17
12
|
end
|
|
18
13
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
class DeviseSessionExpirable::Delegator < Devise::Delegator
|
|
3
|
+
|
|
4
|
+
def failure_app(env)
|
|
5
|
+
app = env["warden.options"] &&
|
|
6
|
+
(scope = env["warden.options"][:scope]) &&
|
|
7
|
+
Devise.mappings[scope.to_sym].failure_app
|
|
8
|
+
|
|
9
|
+
app || DeviseSessionExpirable::FailureApp
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
end
|
|
13
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
module Devise #:nodoc:
|
|
3
|
+
|
|
4
|
+
mattr_accessor :default_last_request_at
|
|
5
|
+
@default_last_request_at = nil
|
|
6
|
+
|
|
7
|
+
add_module :session_expirable,
|
|
8
|
+
:model => 'devise_session_expirable/model'
|
|
9
|
+
|
|
10
|
+
def self.configure_warden! #:nodoc:
|
|
11
|
+
warden_config.failure_app = DeviseSessionExpirable::Delegator.new
|
|
12
|
+
warden_config.default_scope = Devise.default_scope
|
|
13
|
+
warden_config.intercept_401 = false
|
|
14
|
+
|
|
15
|
+
Devise.mappings.each_value do |mapping|
|
|
16
|
+
warden_config.scope_defaults mapping.name, :strategies => mapping.strategies
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@@warden_config_block.try :call, Devise.warden_config
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Mapping #:nodoc:
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def default_failure_app(options)
|
|
28
|
+
@failure_app = options[:failure_app] || DeviseSessionExpirable::FailureApp
|
|
29
|
+
if @failure_app.is_a?(String)
|
|
30
|
+
ref = Devise.ref(@failure_app)
|
|
31
|
+
@failure_app = lambda { |env| ref.get.call(env) }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
class DeviseSessionExpirable::FailureApp < Devise::FailureApp
|
|
3
|
+
|
|
4
|
+
def redirect_url
|
|
5
|
+
flash[:timedout] = true if timeout?
|
|
6
|
+
scope_path
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
protected
|
|
10
|
+
|
|
11
|
+
def warden_message
|
|
12
|
+
@message ||=
|
|
13
|
+
warden.message || warden_options[:message] || (timeout? ? :timeout : nil)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def timeout?
|
|
17
|
+
!! env['devise.timeout']
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
|
|
@@ -1,40 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
# Each time the user record is fetched from a session, the record is
|
|
3
|
-
# consulted (via +#session_expired?+) to determine if the
|
|
4
|
-
# +last_request_at+ time in the session is valid, or if the session
|
|
5
|
-
# should be considered as having timed out. This validation is not
|
|
6
|
-
# performed if +devise.skip_timeout+ is set in the rack environment.
|
|
7
|
-
#
|
|
8
|
-
# If the session is deemed to have timed out, the record is logged out
|
|
9
|
-
# of the session and a +:timeout+ message is thrown, invoking the
|
|
10
|
-
# +FailureApp+.
|
|
11
|
-
#
|
|
12
|
-
# Unlike the Devise +timeoutable+ module, devise_session_expirable does
|
|
13
|
-
# not support invalidation of authentication tokens from the devise
|
|
14
|
-
# +token_authenticatable+ module when a request with a valid
|
|
15
|
-
# authentication token is accompanied by an expired session.
|
|
16
|
-
|
|
17
|
-
Warden::Manager.after_fetch do |record, warden, options|
|
|
18
|
-
scope = options[:scope]
|
|
19
|
-
env = warden.request.env
|
|
20
|
-
|
|
21
|
-
puts "after fetch"
|
|
22
|
-
|
|
23
|
-
if record &&
|
|
24
|
-
record.respond_to?(:session_expired?) &&
|
|
25
|
-
warden.authenticated?(scope) &&
|
|
26
|
-
options[:store] != false &&
|
|
27
|
-
!env['devise.skip_timeout']
|
|
28
|
-
|
|
29
|
-
last_request_at = warden.session(scope)['last_request_at']
|
|
30
|
-
if record.session_expired?(last_request_at)
|
|
31
|
-
puts "expired!"
|
|
32
|
-
warden.logout(scope)
|
|
33
|
-
throw :warden, :scope => scope, :message => :timeout
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
1
|
# Each time the user record is set, the +last_request_at+ time
|
|
39
2
|
# is updated in the scoped session. This update is not performed if
|
|
40
3
|
# devise.skip_trackable is set in the rack environment.
|
|
@@ -43,16 +6,12 @@ Warden::Manager.after_set_user do |record, warden, options|
|
|
|
43
6
|
scope = options[:scope]
|
|
44
7
|
env = warden.request.env
|
|
45
8
|
|
|
46
|
-
puts "after set_user"
|
|
47
|
-
|
|
48
9
|
if record &&
|
|
49
10
|
record.respond_to?(:session_expired?) &&
|
|
50
11
|
warden.authenticated?(scope) &&
|
|
51
12
|
options[:store] != false &&
|
|
52
13
|
!env['devise.skip_trackable']
|
|
53
14
|
|
|
54
|
-
puts "reset"
|
|
55
|
-
|
|
56
15
|
warden.session(scope)['last_request_at'] = Time.now.utc
|
|
57
16
|
end
|
|
58
17
|
end
|
|
@@ -34,11 +34,6 @@ module Devise #:nodoc:
|
|
|
34
34
|
# is set.
|
|
35
35
|
|
|
36
36
|
def session_expired?(last_access)
|
|
37
|
-
puts "last_access #{last_access}"
|
|
38
|
-
puts "timeout_in #{timeout_in}"
|
|
39
|
-
puts "deadline #{timeout_in.ago}"
|
|
40
|
-
puts !timeout_in.nil? && (!last_access || last_access <= timeout_in.ago)
|
|
41
|
-
|
|
42
37
|
last_access ||= default_last_request_at
|
|
43
38
|
return false if remember_exists_and_not_expired?
|
|
44
39
|
!timeout_in.nil? && (!last_access || last_access <= timeout_in.ago)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
|
|
2
|
+
# Each time the user record is fetched from a session, the record is
|
|
3
|
+
# consulted (via +#session_expired?+) to determine if the
|
|
4
|
+
# +last_request_at+ time in the session is valid, or if the session
|
|
5
|
+
# should be considered as having timed out. If the session is deemed
|
|
6
|
+
# to have timed out, the record is disregarded.
|
|
7
|
+
#
|
|
8
|
+
# Unlike the Devise +timeoutable+ module, devise_session_expirable does
|
|
9
|
+
# not support invalidation of authentication tokens from the devise
|
|
10
|
+
# +token_authenticatable+ module when a request with a valid
|
|
11
|
+
# authentication token is accompanied by an expired session.
|
|
12
|
+
|
|
13
|
+
class Warden::SessionSerializer
|
|
14
|
+
|
|
15
|
+
def fetch(scope)
|
|
16
|
+
key = session[key_for(scope)]
|
|
17
|
+
return nil unless key
|
|
18
|
+
|
|
19
|
+
method_name = "#{scope}_deserialize"
|
|
20
|
+
user = respond_to?(method_name) ? send(method_name, key) : deserialize(key)
|
|
21
|
+
user = nil unless valid_for_deserialization?(scope, user)
|
|
22
|
+
delete(scope) unless user
|
|
23
|
+
user
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def valid_for_deserialization?(scope, user)
|
|
29
|
+
! validate_session_expiration(scope, user)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def validate_session_expiration(scope, user)
|
|
33
|
+
is_expired = false
|
|
34
|
+
if user && user.respond_to?(:session_expired?)
|
|
35
|
+
last_request_at = session_for_scope(scope)['last_request_at']
|
|
36
|
+
is_expired = user.session_expired?(last_request_at)
|
|
37
|
+
end
|
|
38
|
+
env['devise.timeout'] = is_expired
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def session_for_scope(scope)
|
|
42
|
+
session["warden.user.#{scope}.session"] ||= {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
|
|
@@ -43,7 +43,7 @@ class SessionExpirableIntegrationTest < ActionDispatch::IntegrationTest
|
|
|
43
43
|
get clear_timeout_user_path(user)
|
|
44
44
|
|
|
45
45
|
get users_path
|
|
46
|
-
assert_redirected_to
|
|
46
|
+
assert_redirected_to new_user_session_path
|
|
47
47
|
assert_not warden.authenticated?(:user)
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -53,27 +53,39 @@ class SessionExpirableIntegrationTest < ActionDispatch::IntegrationTest
|
|
|
53
53
|
assert_not_nil last_request_at
|
|
54
54
|
|
|
55
55
|
get users_path
|
|
56
|
-
assert_redirected_to
|
|
56
|
+
assert_redirected_to new_user_session_path
|
|
57
57
|
assert_not warden.authenticated?(:user)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
test '
|
|
60
|
+
test 'sign out when timed out is ignored' do
|
|
61
61
|
user = sign_in_as_user
|
|
62
62
|
get expire_user_path(user)
|
|
63
63
|
|
|
64
64
|
get destroy_user_session_path
|
|
65
|
-
|
|
66
65
|
assert_response :redirect
|
|
67
66
|
assert_redirected_to root_path
|
|
67
|
+
assert_not warden.authenticated?(:user)
|
|
68
68
|
follow_redirect!
|
|
69
|
-
|
|
69
|
+
assert_not_contain 'Signed out successfully'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test 'expired session is not extended by sign in page' do
|
|
73
|
+
user = sign_in_as_user
|
|
74
|
+
get expire_user_path(user)
|
|
75
|
+
assert warden.authenticated?(:user)
|
|
76
|
+
|
|
77
|
+
get "/users/sign_in"
|
|
78
|
+
assert_response :success
|
|
79
|
+
assert_contain 'Sign in'
|
|
80
|
+
assert_not warden.authenticated?(:user)
|
|
70
81
|
end
|
|
71
82
|
|
|
72
83
|
test 'time out is not triggered on sign in' do
|
|
73
84
|
user = sign_in_as_user
|
|
74
85
|
get expire_user_path(user)
|
|
75
86
|
|
|
76
|
-
post "/users/sign_in",
|
|
87
|
+
post "/users/sign_in",
|
|
88
|
+
:user => { :email => user.email, :password => "12345678" }
|
|
77
89
|
|
|
78
90
|
assert_response :redirect
|
|
79
91
|
follow_redirect!
|
|
@@ -90,7 +102,7 @@ class SessionExpirableIntegrationTest < ActionDispatch::IntegrationTest
|
|
|
90
102
|
|
|
91
103
|
begin
|
|
92
104
|
get admins_path
|
|
93
|
-
assert_redirected_to
|
|
105
|
+
assert_redirected_to new_admin_session_path
|
|
94
106
|
assert_not warden.authenticated?(:admin)
|
|
95
107
|
ensure
|
|
96
108
|
Admin.send(:remove_method, :reset_authentication_token!)
|
|
@@ -108,25 +120,12 @@ class SessionExpirableIntegrationTest < ActionDispatch::IntegrationTest
|
|
|
108
120
|
|
|
109
121
|
get expire_user_path(user)
|
|
110
122
|
get users_path
|
|
111
|
-
assert_redirected_to
|
|
123
|
+
assert_redirected_to new_user_session_path
|
|
112
124
|
assert_not warden.authenticated?(:user)
|
|
113
125
|
end
|
|
114
126
|
end
|
|
115
127
|
|
|
116
128
|
test 'error message with i18n' do
|
|
117
|
-
store_translations :en, :devise => {
|
|
118
|
-
:failure => { :user => { :timeout => 'Session expired!' } }
|
|
119
|
-
} do
|
|
120
|
-
user = sign_in_as_user
|
|
121
|
-
|
|
122
|
-
get expire_user_path(user)
|
|
123
|
-
get root_path
|
|
124
|
-
follow_redirect!
|
|
125
|
-
assert_contain 'Session expired!'
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
test 'error message with i18n with double redirect' do
|
|
130
129
|
store_translations :en, :devise => {
|
|
131
130
|
:failure => { :user => { :timeout => 'Session expired!' } }
|
|
132
131
|
} do
|
|
@@ -135,7 +134,6 @@ class SessionExpirableIntegrationTest < ActionDispatch::IntegrationTest
|
|
|
135
134
|
get expire_user_path(user)
|
|
136
135
|
get users_path
|
|
137
136
|
follow_redirect!
|
|
138
|
-
follow_redirect!
|
|
139
137
|
assert_contain 'Session expired!'
|
|
140
138
|
end
|
|
141
139
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: devise_session_expirable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2013-
|
|
12
|
+
date: 2013-03-11 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activesupport
|
|
@@ -108,8 +108,12 @@ files:
|
|
|
108
108
|
- VERSION
|
|
109
109
|
- devise_session_expirable.gemspec
|
|
110
110
|
- lib/devise_session_expirable.rb
|
|
111
|
+
- lib/devise_session_expirable/delegator.rb
|
|
112
|
+
- lib/devise_session_expirable/devise_extensions.rb
|
|
113
|
+
- lib/devise_session_expirable/failure_app.rb
|
|
111
114
|
- lib/devise_session_expirable/hook.rb
|
|
112
115
|
- lib/devise_session_expirable/model.rb
|
|
116
|
+
- lib/devise_session_expirable/warden_extensions.rb
|
|
113
117
|
- test/integration/session_expirable_test.rb
|
|
114
118
|
- test/models/session_expirable_test.rb
|
|
115
119
|
- test/orm/active_record.rb
|
|
@@ -168,6 +172,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
168
172
|
- - ! '>='
|
|
169
173
|
- !ruby/object:Gem::Version
|
|
170
174
|
version: '0'
|
|
175
|
+
segments:
|
|
176
|
+
- 0
|
|
177
|
+
hash: 1721273659600446097
|
|
171
178
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
172
179
|
none: false
|
|
173
180
|
requirements:
|