devise_session_expirable 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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