devise_session_expirable 0.1.0 → 0.1.1

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.
Files changed (52) hide show
  1. data/README.rdoc +34 -6
  2. data/VERSION +1 -1
  3. data/devise_session_expirable.gemspec +106 -0
  4. data/lib/devise_session_expirable.rb +18 -0
  5. data/lib/devise_session_expirable/hook.rb +59 -0
  6. data/lib/devise_session_expirable/model.rb +77 -0
  7. data/test/integration/session_expirable_test.rb +152 -0
  8. data/test/models/session_expirable_test.rb +46 -0
  9. data/test/orm/active_record.rb +9 -0
  10. data/test/rails_app/Rakefile +10 -0
  11. data/test/rails_app/app/active_record/admin.rb +6 -0
  12. data/test/rails_app/app/active_record/shim.rb +2 -0
  13. data/test/rails_app/app/active_record/user.rb +6 -0
  14. data/test/rails_app/app/controllers/admins/sessions_controller.rb +6 -0
  15. data/test/rails_app/app/controllers/admins_controller.rb +11 -0
  16. data/test/rails_app/app/controllers/application_controller.rb +9 -0
  17. data/test/rails_app/app/controllers/home_controller.rb +4 -0
  18. data/test/rails_app/app/controllers/users_controller.rb +23 -0
  19. data/test/rails_app/app/helpers/application_helper.rb +3 -0
  20. data/test/rails_app/app/views/admins/index.html.erb +1 -0
  21. data/test/rails_app/app/views/admins/sessions/new.html.erb +2 -0
  22. data/test/rails_app/app/views/home/index.html.erb +1 -0
  23. data/test/rails_app/app/views/layouts/application.html.erb +24 -0
  24. data/test/rails_app/app/views/users/index.html.erb +1 -0
  25. data/test/rails_app/app/views/users/sessions/new.html.erb +1 -0
  26. data/test/rails_app/config.ru +4 -0
  27. data/test/rails_app/config/application.rb +41 -0
  28. data/test/rails_app/config/boot.rb +8 -0
  29. data/test/rails_app/config/database.yml +18 -0
  30. data/test/rails_app/config/environment.rb +5 -0
  31. data/test/rails_app/config/environments/development.rb +18 -0
  32. data/test/rails_app/config/environments/production.rb +33 -0
  33. data/test/rails_app/config/environments/test.rb +33 -0
  34. data/test/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  35. data/test/rails_app/config/initializers/devise.rb +171 -0
  36. data/test/rails_app/config/initializers/inflections.rb +2 -0
  37. data/test/rails_app/config/initializers/secret_token.rb +2 -0
  38. data/test/rails_app/config/routes.rb +26 -0
  39. data/test/rails_app/db/migrate/20100401102949_create_tables.rb +47 -0
  40. data/test/rails_app/lib/shared_admin.rb +25 -0
  41. data/test/rails_app/lib/shared_user.rb +22 -0
  42. data/test/rails_app/public/404.html +26 -0
  43. data/test/rails_app/public/422.html +26 -0
  44. data/test/rails_app/public/500.html +26 -0
  45. data/test/rails_app/public/favicon.ico +0 -0
  46. data/test/rails_app/script/rails +10 -0
  47. data/test/support/assertions.rb +18 -0
  48. data/test/support/helpers.rb +68 -0
  49. data/test/support/integration.rb +89 -0
  50. data/test/support/webrat/integrations/rails.rb +28 -0
  51. data/test/test_helper.rb +33 -0
  52. metadata +52 -3
@@ -25,13 +25,41 @@ does not support invalidation of authentication tokens from the
25
25
  devise +token_authenticatable+ module when a request with a valid
26
26
  authentication token is accompanied by an expired session.
27
27
 
28
- == Migrating to SessionExpirable
28
+ == Configuration
29
29
 
30
- For a less disruptive migration from sessions without timestamps, it is
31
- possible to set a +default_last_request_at+ value, which will be used in
32
- place of the timestamp for sessions which don't have one. After the
33
- +timeout_in+ interval passes, these legacy sessions will expire and
34
- the +default_last_request_at+ value can be unset.
30
+ Add +devise_session_expirable+ to your Gemfile:
31
+
32
+ gem 'devise_session_expirable'
33
+
34
+ Include +:session_expirable+ in your devise user model declaration:
35
+
36
+ class User < ActiveRecord::Base
37
+ devise :database_authenticatable, :session_expirable # ...
38
+ end
39
+
40
+ Then update the Devise initializer:
41
+
42
+ Devise.setup do |config|
43
+ # ...
44
+ config.timeout_in = 15.minutes
45
+ config.default_last_request_at = Time.parse('2013-02-16T00:00:00Z')
46
+ # ...
47
+ end
48
+
49
+ == Migrating from non-expiring sessions
50
+
51
+ The +default_last_request_at+ option is intended to enable a less
52
+ disruptive migration if sessions without timestamps have already been
53
+ issued. The configured value will be used in place of the timestamp
54
+ for sessions which don't have one.
55
+
56
+ If +default_last_request_at+ is configured, it should be set to a fixed
57
+ date/time, ideally matching the time of deployment. If set to a dynamic
58
+ time (e.g. Time.now), the lifetime of sessions without timestamps will
59
+ be extended every time Rails is initialized.
60
+
61
+ After the +timeout_in+ interval passes, any legacy sessions will have
62
+ expired and +default_last_request_at+ can be unset.
35
63
 
36
64
  == Alternatives
37
65
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
@@ -0,0 +1,106 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "devise_session_expirable"
8
+ s.version = "0.1.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Riley Lynch"]
12
+ s.date = "2013-02-16"
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
+ s.email = "oss+expirable@teleological.net"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ "Gemfile",
21
+ "Gemfile.lock",
22
+ "LICENSE.txt",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "devise_session_expirable.gemspec",
27
+ "lib/devise_session_expirable.rb",
28
+ "lib/devise_session_expirable/hook.rb",
29
+ "lib/devise_session_expirable/model.rb",
30
+ "test/integration/session_expirable_test.rb",
31
+ "test/models/session_expirable_test.rb",
32
+ "test/orm/active_record.rb",
33
+ "test/rails_app/Rakefile",
34
+ "test/rails_app/app/active_record/admin.rb",
35
+ "test/rails_app/app/active_record/shim.rb",
36
+ "test/rails_app/app/active_record/user.rb",
37
+ "test/rails_app/app/controllers/admins/sessions_controller.rb",
38
+ "test/rails_app/app/controllers/admins_controller.rb",
39
+ "test/rails_app/app/controllers/application_controller.rb",
40
+ "test/rails_app/app/controllers/home_controller.rb",
41
+ "test/rails_app/app/controllers/users_controller.rb",
42
+ "test/rails_app/app/helpers/application_helper.rb",
43
+ "test/rails_app/app/views/admins/index.html.erb",
44
+ "test/rails_app/app/views/admins/sessions/new.html.erb",
45
+ "test/rails_app/app/views/home/index.html.erb",
46
+ "test/rails_app/app/views/layouts/application.html.erb",
47
+ "test/rails_app/app/views/users/index.html.erb",
48
+ "test/rails_app/app/views/users/sessions/new.html.erb",
49
+ "test/rails_app/config.ru",
50
+ "test/rails_app/config/application.rb",
51
+ "test/rails_app/config/boot.rb",
52
+ "test/rails_app/config/database.yml",
53
+ "test/rails_app/config/environment.rb",
54
+ "test/rails_app/config/environments/development.rb",
55
+ "test/rails_app/config/environments/production.rb",
56
+ "test/rails_app/config/environments/test.rb",
57
+ "test/rails_app/config/initializers/backtrace_silencers.rb",
58
+ "test/rails_app/config/initializers/devise.rb",
59
+ "test/rails_app/config/initializers/inflections.rb",
60
+ "test/rails_app/config/initializers/secret_token.rb",
61
+ "test/rails_app/config/routes.rb",
62
+ "test/rails_app/db/migrate/20100401102949_create_tables.rb",
63
+ "test/rails_app/lib/shared_admin.rb",
64
+ "test/rails_app/lib/shared_user.rb",
65
+ "test/rails_app/public/404.html",
66
+ "test/rails_app/public/422.html",
67
+ "test/rails_app/public/500.html",
68
+ "test/rails_app/public/favicon.ico",
69
+ "test/rails_app/script/rails",
70
+ "test/support/assertions.rb",
71
+ "test/support/helpers.rb",
72
+ "test/support/integration.rb",
73
+ "test/support/webrat/integrations/rails.rb",
74
+ "test/test_helper.rb"
75
+ ]
76
+ s.homepage = "http://github.com/teleological/devise_session_expirable"
77
+ s.licenses = ["MIT"]
78
+ s.require_paths = ["lib"]
79
+ s.rubygems_version = "1.8.23"
80
+ s.summary = "Strict timeouts for devise-authenticated sessions"
81
+
82
+ if s.respond_to? :specification_version then
83
+ s.specification_version = 3
84
+
85
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
86
+ s.add_runtime_dependency(%q<activesupport>, ["~> 3.2.12"])
87
+ s.add_runtime_dependency(%q<devise>, [">= 2.2.3"])
88
+ s.add_development_dependency(%q<bundler>, [">= 1.2.4"])
89
+ s.add_development_dependency(%q<jeweler>, [">= 1.8.4"])
90
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
91
+ else
92
+ s.add_dependency(%q<activesupport>, ["~> 3.2.12"])
93
+ s.add_dependency(%q<devise>, [">= 2.2.3"])
94
+ s.add_dependency(%q<bundler>, [">= 1.2.4"])
95
+ s.add_dependency(%q<jeweler>, [">= 1.8.4"])
96
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
97
+ end
98
+ else
99
+ s.add_dependency(%q<activesupport>, ["~> 3.2.12"])
100
+ s.add_dependency(%q<devise>, [">= 2.2.3"])
101
+ s.add_dependency(%q<bundler>, [">= 1.2.4"])
102
+ s.add_dependency(%q<jeweler>, [">= 1.8.4"])
103
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
104
+ end
105
+ end
106
+
@@ -0,0 +1,18 @@
1
+
2
+ require 'active_support/core_ext/module/attribute_accessors'
3
+ require 'active_support/concern'
4
+ require 'devise'
5
+
6
+ module Devise #:nodoc:
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
15
+
16
+ module DeviseSessionExpirable #:nodoc:
17
+ end
18
+
@@ -0,0 +1,59 @@
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
+ # Each time the user record is set, the +last_request_at+ time
39
+ # is updated in the scoped session. This update is not performed if
40
+ # devise.skip_trackable is set in the rack environment.
41
+
42
+ Warden::Manager.after_set_user do |record, warden, options|
43
+ scope = options[:scope]
44
+ env = warden.request.env
45
+
46
+ puts "after set_user"
47
+
48
+ if record &&
49
+ record.respond_to?(:session_expired?) &&
50
+ warden.authenticated?(scope) &&
51
+ options[:store] != false &&
52
+ !env['devise.skip_trackable']
53
+
54
+ puts "reset"
55
+
56
+ warden.session(scope)['last_request_at'] = Time.now.utc
57
+ end
58
+ end
59
+
@@ -0,0 +1,77 @@
1
+
2
+ require 'devise_session_expirable/hook'
3
+
4
+ module Devise #:nodoc:
5
+ module Models #:nodoc:
6
+
7
+ # SessionExpirable verifies whether a user session has expired
8
+ # via the +#session_expired?+ method.
9
+ #
10
+ # == Options
11
+ #
12
+ # SessionExpirable adds the following options to devise_for:
13
+ #
14
+ # * +timeout_in+: lifetime in seconds of an inactive user session
15
+ # * +default_last_request_at+: age to assume for sessions with nil +last_request_at+
16
+ #
17
+ # == Examples
18
+ #
19
+ # user.session_expired?(30.minutes.ago)
20
+ #
21
+
22
+ module SessionExpirable
23
+
24
+ extend ActiveSupport::Concern
25
+
26
+ # Accepts the time a session was last used and compares it
27
+ # to the oldest valid +last_request_at+ date for a session.
28
+ # If nil or any other falsy value is passed and the
29
+ # +default_last_request_at+ option is configured, the configured
30
+ # value will be used for the comparison.
31
+ #
32
+ # Supports the Devise +rememberable+ module by deferring to
33
+ # +#remember_expired?+ if the +remember_created_at+ attribute
34
+ # is set.
35
+
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
+ last_access ||= default_last_request_at
43
+ return false if remember_exists_and_not_expired?
44
+ !timeout_in.nil? && (!last_access || last_access <= timeout_in.ago)
45
+ end
46
+
47
+ def timeout_in
48
+ self.class.timeout_in
49
+ end
50
+
51
+ def default_last_request_at
52
+ self.class.default_last_request_at
53
+ end
54
+
55
+ #:nodoc:
56
+ def self.required_fields(klass); []; end
57
+
58
+ private
59
+
60
+ def remember_exists_and_not_expired?
61
+ return false unless respond_to?(:remember_expired?)
62
+ remember_created_at && !remember_expired?
63
+ end
64
+
65
+ module ClassMethods #:nodoc:
66
+
67
+ Devise::Models.config self,
68
+ :timeout_in,
69
+ :default_last_request_at
70
+
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+ end
77
+
@@ -0,0 +1,152 @@
1
+ require 'test_helper'
2
+
3
+ class SessionExpirableIntegrationTest < ActionDispatch::IntegrationTest
4
+
5
+ def last_request_at
6
+ @controller.user_session['last_request_at']
7
+ end
8
+
9
+ test 'set last request at in user session after each request' do
10
+ sign_in_as_user
11
+ old_last_request = last_request_at
12
+ assert_not_nil last_request_at
13
+
14
+ get users_path
15
+ assert_not_nil last_request_at
16
+ assert_not_equal old_last_request, last_request_at
17
+ end
18
+
19
+ test 'set last request at in user session after each request is skipped if tracking is disabled' do
20
+ sign_in_as_user
21
+ old_last_request = last_request_at
22
+ assert_not_nil last_request_at
23
+
24
+ get users_path, {}, 'devise.skip_trackable' => true
25
+ assert_equal old_last_request, last_request_at
26
+ end
27
+
28
+ test 'does not time out user session before default limit time' do
29
+ sign_in_as_user
30
+ assert_response :success
31
+ assert warden.authenticated?(:user)
32
+
33
+ get users_path
34
+ assert_response :success
35
+ assert warden.authenticated?(:user)
36
+ end
37
+
38
+ test 'session without last_request_at is not honored' do
39
+ user = sign_in_as_user
40
+ assert_response :success
41
+ assert warden.authenticated?(:user)
42
+
43
+ get clear_timeout_user_path(user)
44
+
45
+ get users_path
46
+ assert_redirected_to users_path
47
+ assert_not warden.authenticated?(:user)
48
+ end
49
+
50
+ test 'time out user session after default limit time' do
51
+ user = sign_in_as_user
52
+ get expire_user_path(user)
53
+ assert_not_nil last_request_at
54
+
55
+ get users_path
56
+ assert_redirected_to users_path
57
+ assert_not warden.authenticated?(:user)
58
+ end
59
+
60
+ test 'time out is not triggered on sign out' do
61
+ user = sign_in_as_user
62
+ get expire_user_path(user)
63
+
64
+ get destroy_user_session_path
65
+
66
+ assert_response :redirect
67
+ assert_redirected_to root_path
68
+ follow_redirect!
69
+ assert_contain 'Signed out successfully'
70
+ end
71
+
72
+ test 'time out is not triggered on sign in' do
73
+ user = sign_in_as_user
74
+ get expire_user_path(user)
75
+
76
+ post "/users/sign_in", :email => user.email, :password => "123456"
77
+
78
+ assert_response :redirect
79
+ follow_redirect!
80
+ assert_contain 'You are signed in'
81
+ end
82
+
83
+ test 'admin does not explode on time out' do
84
+ admin = sign_in_as_admin
85
+ get expire_admin_path(admin)
86
+
87
+ Admin.send :define_method, :reset_authentication_token! do
88
+ nil
89
+ end
90
+
91
+ begin
92
+ get admins_path
93
+ assert_redirected_to admins_path
94
+ assert_not warden.authenticated?(:admin)
95
+ ensure
96
+ Admin.send(:remove_method, :reset_authentication_token!)
97
+ end
98
+ end
99
+
100
+ test 'user configured timeout limit' do
101
+ swap Devise, :timeout_in => 8.minutes do
102
+ user = sign_in_as_user
103
+
104
+ get users_path
105
+ assert_not_nil last_request_at
106
+ assert_response :success
107
+ assert warden.authenticated?(:user)
108
+
109
+ get expire_user_path(user)
110
+ get users_path
111
+ assert_redirected_to users_path
112
+ assert_not warden.authenticated?(:user)
113
+ end
114
+ end
115
+
116
+ 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
+ store_translations :en, :devise => {
131
+ :failure => { :user => { :timeout => 'Session expired!' } }
132
+ } do
133
+ user = sign_in_as_user
134
+
135
+ get expire_user_path(user)
136
+ get users_path
137
+ follow_redirect!
138
+ follow_redirect!
139
+ assert_contain 'Session expired!'
140
+ end
141
+ end
142
+
143
+ test 'time out not triggered if remembered' do
144
+ user = sign_in_as_user :remember_me => true
145
+ get expire_user_path(user)
146
+ assert_not_nil last_request_at
147
+
148
+ get users_path
149
+ assert_response :success
150
+ assert warden.authenticated?(:user)
151
+ end
152
+ end