##// END OF EJS Templates
Keep track of valid user sessions (#21058)....
Jean-Philippe Lang -
r14353:4cd22dcc5595
parent child
Show More
@@ -0,0 +1,10
1 class AddTokensUpdatedOn < ActiveRecord::Migration
2 def self.up
3 add_column :tokens, :updated_on, :timestamp
4 Token.update_all("updated_on = created_on")
5 end
6
7 def self.down
8 remove_column :tokens, :updated_on
9 end
10 end
@@ -0,0 +1,97
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../test_helper', __FILE__)
19
20 class SessionsTest < Redmine::IntegrationTest
21 fixtures :users, :email_addresses, :roles
22
23 def setup
24 Rails.application.config.redmine_verify_sessions = true
25 end
26
27 def teardown
28 Rails.application.config.redmine_verify_sessions = false
29 end
30
31 def test_change_password_kills_sessions
32 log_user('jsmith', 'jsmith')
33
34 jsmith = User.find(2)
35 jsmith.password = "somenewpassword"
36 jsmith.save!
37
38 get '/my/account'
39 assert_response 302
40 assert flash[:error].match(/Your session has expired/)
41 end
42
43 def test_lock_user_kills_sessions
44 log_user('jsmith', 'jsmith')
45
46 jsmith = User.find(2)
47 assert jsmith.lock!
48 assert jsmith.activate!
49
50 get '/my/account'
51 assert_response 302
52 assert flash[:error].match(/Your session has expired/)
53 end
54
55 def test_update_user_does_not_kill_sessions
56 log_user('jsmith', 'jsmith')
57
58 jsmith = User.find(2)
59 jsmith.firstname = 'Robert'
60 jsmith.save!
61
62 get '/my/account'
63 assert_response 200
64 end
65
66 def test_change_password_generates_a_new_token_for_current_session
67 log_user('jsmith', 'jsmith')
68 assert_not_nil token = session[:tk]
69
70 get '/my/password'
71 assert_response 200
72 post '/my/password', :password => 'jsmith',
73 :new_password => 'secret123',
74 :new_password_confirmation => 'secret123'
75 assert_response 302
76 assert_not_equal token, session[:tk]
77
78 get '/my/account'
79 assert_response 200
80 end
81
82 def test_simultaneous_sessions_should_be_valid
83 first = open_session do |session|
84 session.post "/login", :username => 'jsmith', :password => 'jsmith'
85 end
86 other = open_session do |session|
87 session.post "/login", :username => 'jsmith', :password => 'jsmith'
88 end
89
90 first.get '/my/account'
91 assert_equal 200, first.response.response_code
92 first.post '/logout'
93
94 other.get '/my/account'
95 assert_equal 200, other.response.response_code
96 end
97 end
@@ -51,7 +51,7 class ApplicationController < ActionController::Base
51 51 end
52 52 end
53 53
54 before_filter :session_expiration, :user_setup, :force_logout_if_password_changed, :check_if_login_required, :check_password_change, :set_localization
54 before_filter :session_expiration, :user_setup, :check_if_login_required, :check_password_change, :set_localization
55 55
56 56 rescue_from ::Unauthorized, :with => :deny_access
57 57 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
@@ -63,36 +63,23 class ApplicationController < ActionController::Base
63 63 include Redmine::SudoMode::Controller
64 64
65 65 def session_expiration
66 if session[:user_id]
66 if session[:user_id] && Rails.application.config.redmine_verify_sessions != false
67 67 if session_expired? && !try_to_autologin
68 68 set_localization(User.active.find_by_id(session[:user_id]))
69 69 self.logged_user = nil
70 70 flash[:error] = l(:error_session_expired)
71 71 require_login
72 else
73 session[:atime] = Time.now.utc.to_i
74 72 end
75 73 end
76 74 end
77 75
78 76 def session_expired?
79 if Setting.session_lifetime?
80 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
81 return true
82 end
83 end
84 if Setting.session_timeout?
85 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
86 return true
87 end
88 end
89 false
77 ! User.verify_session_token(session[:user_id], session[:tk])
90 78 end
91 79
92 80 def start_user_session(user)
93 81 session[:user_id] = user.id
94 session[:ctime] = Time.now.utc.to_i
95 session[:atime] = Time.now.utc.to_i
82 session[:tk] = user.generate_session_token
96 83 if user.must_change_password?
97 84 session[:pwd] = '1'
98 85 end
@@ -149,18 +136,6 class ApplicationController < ActionController::Base
149 136 user
150 137 end
151 138
152 def force_logout_if_password_changed
153 passwd_changed_on = User.current.passwd_changed_on || Time.at(0)
154 # Make sure we force logout only for web browser sessions, not API calls
155 # if the password was changed after the session creation.
156 if session[:user_id] && passwd_changed_on.utc.to_i > session[:ctime].to_i
157 reset_session
158 set_localization
159 flash[:error] = l(:error_session_expired)
160 redirect_to signin_url
161 end
162 end
163
164 139 def autologin_cookie_name
165 140 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
166 141 end
@@ -193,6 +168,7 class ApplicationController < ActionController::Base
193 168 if User.current.logged?
194 169 cookies.delete(autologin_cookie_name)
195 170 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
171 Token.delete_all(["user_id = ? AND action = ? AND value = ?", User.current.id, 'session', session[:tk]])
196 172 self.logged_user = nil
197 173 end
198 174 end
@@ -103,9 +103,8 class MyController < ApplicationController
103 103 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
104 104 @user.must_change_passwd = false
105 105 if @user.save
106 # Reset the session creation time to not log out this session on next
107 # request due to ApplicationController#force_logout_if_password_changed
108 session[:ctime] = User.current.passwd_changed_on.utc.to_i
106 # The session token was destroyed by the password change, generate a new one
107 session[:tk] = @user.generate_session_token
109 108 flash[:notice] = l(:notice_account_password_updated)
110 109 redirect_to my_account_path
111 110 end
@@ -36,7 +36,7 class Token < ActiveRecord::Base
36 36
37 37 # Delete all expired tokens
38 38 def self.destroy_expired
39 Token.where("action NOT IN (?) AND created_on < ?", ['feeds', 'api'], Time.now - validity_time).delete_all
39 Token.where("action NOT IN (?) AND created_on < ?", ['feeds', 'api', 'session'], Time.now - validity_time).delete_all
40 40 end
41 41
42 42 # Returns the active user who owns the key for the given action
@@ -79,7 +79,15 class Token < ActiveRecord::Base
79 79 # Removes obsolete tokens (same user and action)
80 80 def delete_previous_tokens
81 81 if user
82 Token.where(:user_id => user.id, :action => action).delete_all
82 scope = Token.where(:user_id => user.id, :action => action)
83 if action == 'session'
84 ids = scope.order(:updated_on => :desc).offset(9).ids
85 if ids.any?
86 Token.delete(ids)
87 end
88 else
89 scope.delete_all
90 end
83 91 end
84 92 end
85 93 end
@@ -394,6 +394,26 class User < Principal
394 394 api_token.value
395 395 end
396 396
397 # Generates a new session token and returns its value
398 def generate_session_token
399 token = Token.create!(:user_id => id, :action => 'session')
400 token.value
401 end
402
403 # Returns true if token is a valid session token for the user whose id is user_id
404 def self.verify_session_token(user_id, token)
405 return false if user_id.blank? || token.blank?
406
407 scope = Token.where(:user_id => user_id, :value => token.to_s, :action => 'session')
408 if Setting.session_lifetime?
409 scope = scope.where("created_on > ?", Setting.session_lifetime.to_i.minutes.ago)
410 end
411 if Setting.session_timeout?
412 scope = scope.where("updated_on > ?", Setting.session_timeout.to_i.minutes.ago)
413 end
414 scope.update_all(:updated_on => Time.now) == 1
415 end
416
397 417 # Return an array of project ids for which the user has explicitly turned mail notifications on
398 418 def notified_projects_ids
399 419 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
@@ -764,8 +784,8 class User < Principal
764 784 # This helps to keep the account secure in case the associated email account
765 785 # was compromised.
766 786 def destroy_tokens
767 if hashed_password_changed?
768 tokens = ['recovery', 'autologin']
787 if hashed_password_changed? || (status_changed? && !active?)
788 tokens = ['recovery', 'autologin', 'session']
769 789 Token.where(:user_id => id, :action => tokens).delete_all
770 790 end
771 791 end
@@ -26,6 +26,9 Rails.application.configure do
26 26 # Disable request forgery protection in test environment.
27 27 config.action_controller.allow_forgery_protection = false
28 28
29 # Disable sessions verifications in test environment.
30 config.redmine_verify_sessions = false
31
29 32 # Print deprecation notices to stderr and the Rails logger.
30 33 config.active_support.deprecation = [:stderr, :log]
31 34
@@ -185,18 +185,6 class MyControllerTest < ActionController::TestCase
185 185 assert User.try_to_login('jsmith', 'secret123')
186 186 end
187 187
188 def test_change_password_kills_other_sessions
189 @request.session[:ctime] = (Time.now - 30.minutes).utc.to_i
190
191 jsmith = User.find(2)
192 jsmith.passwd_changed_on = Time.now
193 jsmith.save!
194
195 get 'account'
196 assert_response 302
197 assert flash[:error].match(/Your session has expired/)
198 end
199
200 188 def test_change_password_should_redirect_if_user_cannot_change_its_password
201 189 User.find(2).update_attribute(:auth_source_id, 1)
202 190
@@ -17,95 +17,99
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 class SessionStartTest < ActionController::TestCase
21 tests AccountController
20 class SessionsControllerTest < ActionController::TestCase
21 include Redmine::I18n
22 tests WelcomeController
22 23
23 fixtures :users
24 fixtures :users, :email_addresses
24 25
25 def test_login_should_set_session_timestamps
26 post :login, :username => 'jsmith', :password => 'jsmith'
27 assert_response 302
28 assert_equal 2, session[:user_id]
29 assert_not_nil session[:ctime]
30 assert_not_nil session[:atime]
26 def setup
27 Rails.application.config.redmine_verify_sessions = true
31 28 end
32 end
33 29
34 class SessionsTest < ActionController::TestCase
35 include Redmine::I18n
36 tests WelcomeController
30 def teardown
31 Rails.application.config.redmine_verify_sessions = false
32 end
37 33
38 fixtures :users, :email_addresses
34 def test_session_token_should_be_updated
35 created = 10.hours.ago
36 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
39 37
40 def test_atime_from_user_session_should_be_updated
41 created = 2.hours.ago.utc.to_i
42 get :index, {}, {:user_id => 2, :ctime => created, :atime => created}
38 get :index, {}, {:user_id => 2, :tk => token.value}
43 39 assert_response :success
44 assert_equal created, session[:ctime]
45 assert_not_equal created, session[:atime]
46 assert session[:atime] > created
40 token.reload
41 assert_equal created, token.created_on
42 assert_not_equal created, token.updated_on
43 assert token.updated_on > created
47 44 end
48 45
49 46 def test_user_session_should_not_be_reset_if_lifetime_and_timeout_disabled
47 created = 2.years.ago
48 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
49
50 50 with_settings :session_lifetime => '0', :session_timeout => '0' do
51 get :index, {}, {:user_id => 2}
51 get :index, {}, {:user_id => 2, :tk => token.value}
52 52 assert_response :success
53 53 end
54 54 end
55 55
56 def test_user_session_without_ctime_should_be_reset_if_lifetime_enabled
57 with_settings :session_lifetime => '720' do
58 get :index, {}, {:user_id => 2}
59 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
60 end
56 def test_user_session_without_token_should_be_reset
57 get :index, {}, {:user_id => 2}
58 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
61 59 end
62 60
63 def test_user_session_with_expired_ctime_should_be_reset_if_lifetime_enabled
61 def test_expired_user_session_should_be_reset_if_lifetime_enabled
62 created = 2.days.ago
63 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
64
64 65 with_settings :session_timeout => '720' do
65 get :index, {}, {:user_id => 2, :atime => 2.days.ago.utc.to_i}
66 get :index, {}, {:user_id => 2, :tk => token.value}
66 67 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
67 68 end
68 69 end
69 70
70 def test_user_session_with_valid_ctime_should_not_be_reset_if_lifetime_enabled
71 def test_valid_user_session_should_not_be_reset_if_lifetime_enabled
72 created = 3.hours.ago
73 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
74
71 75 with_settings :session_timeout => '720' do
72 get :index, {}, {:user_id => 2, :atime => 3.hours.ago.utc.to_i}
76 get :index, {}, {:user_id => 2, :tk => token.value}
73 77 assert_response :success
74 78 end
75 79 end
76 80
77 def test_user_session_without_atime_should_be_reset_if_timeout_enabled
78 with_settings :session_timeout => '60' do
79 get :index, {}, {:user_id => 2}
80 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
81 end
82 end
81 def test_expired_user_session_should_be_reset_if_timeout_enabled
82 created = 4.hours.ago
83 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
83 84
84 def test_user_session_with_expired_atime_should_be_reset_if_timeout_enabled
85 85 with_settings :session_timeout => '60' do
86 get :index, {}, {:user_id => 2, :atime => 4.hours.ago.utc.to_i}
86 get :index, {}, {:user_id => 2, :tk => token.value}
87 87 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
88 88 end
89 89 end
90 90
91 def test_user_session_with_valid_atime_should_not_be_reset_if_timeout_enabled
91 def test_valid_user_session_should_not_be_reset_if_timeout_enabled
92 created = 10.minutes.ago
93 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
94
92 95 with_settings :session_timeout => '60' do
93 get :index, {}, {:user_id => 2, :atime => 10.minutes.ago.utc.to_i}
96 get :index, {}, {:user_id => 2, :tk => token.value}
94 97 assert_response :success
95 98 end
96 99 end
97 100
98 101 def test_expired_user_session_should_be_restarted_if_autologin
102 created = 2.hours.ago
103 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
104
99 105 with_settings :session_lifetime => '720', :session_timeout => '60', :autologin => 7 do
100 token = Token.create!(:user_id => 2, :action => 'autologin', :created_on => 1.day.ago)
101 @request.cookies['autologin'] = token.value
102 created = 2.hours.ago.utc.to_i
106 autologin_token = Token.create!(:user_id => 2, :action => 'autologin', :created_on => 1.day.ago)
107 @request.cookies['autologin'] = autologin_token.value
103 108
104 get :index, {}, {:user_id => 2, :ctime => created, :atime => created}
109 get :index, {}, {:user_id => 2, :tk => token.value}
105 110 assert_equal 2, session[:user_id]
106 111 assert_response :success
107 assert_not_equal created, session[:ctime]
108 assert session[:ctime] >= created
112 assert_not_equal token.value, session[:tk]
109 113 end
110 114 end
111 115
@@ -114,9 +118,11 class SessionsTest < ActionController::TestCase
114 118 user = User.find(2)
115 119 user.language = 'fr'
116 120 user.save!
121 created = 4.hours.ago
122 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
117 123
118 124 with_settings :session_timeout => '60' do
119 get :index, {}, {:user_id => user.id, :atime => 4.hours.ago.utc.to_i}
125 get :index, {}, {:user_id => user.id, :tk => token.value}
120 126 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
121 127 assert_include "Veuillez vous reconnecter", flash[:error]
122 128 assert_equal :fr, current_language
@@ -30,35 +30,47 class AccountTest < Redmine::IntegrationTest
30 30 assert_template "my/account"
31 31 end
32 32
33 def test_login_should_set_session_token
34 assert_difference 'Token.count' do
35 log_user('jsmith', 'jsmith')
36
37 assert_equal 2, session[:user_id]
38 assert_not_nil session[:tk]
39 end
40 end
41
33 42 def test_autologin
34 43 user = User.find(1)
35 Setting.autologin = "7"
36 44 Token.delete_all
37 45
38 # User logs in with 'autologin' checked
39 post '/login', :username => user.login, :password => 'admin', :autologin => 1
40 assert_redirected_to '/my/page'
41 token = Token.first
42 assert_not_nil token
43 assert_equal user, token.user
44 assert_equal 'autologin', token.action
45 assert_equal user.id, session[:user_id]
46 assert_equal token.value, cookies['autologin']
47
48 # Session is cleared
49 reset!
50 User.current = nil
51 # Clears user's last login timestamp
52 user.update_attribute :last_login_on, nil
53 assert_nil user.reload.last_login_on
54
55 # User comes back with user's autologin cookie
56 cookies[:autologin] = token.value
57 get '/my/page'
58 assert_response :success
59 assert_template 'my/page'
60 assert_equal user.id, session[:user_id]
61 assert_not_nil user.reload.last_login_on
46 with_settings :autologin => '7' do
47 assert_difference 'Token.count', 2 do
48 # User logs in with 'autologin' checked
49 post '/login', :username => user.login, :password => 'admin', :autologin => 1
50 assert_redirected_to '/my/page'
51 end
52 token = Token.where(:action => 'autologin').order(:id => :desc).first
53 assert_not_nil token
54 assert_equal user, token.user
55 assert_equal 'autologin', token.action
56 assert_equal user.id, session[:user_id]
57 assert_equal token.value, cookies['autologin']
58
59 # Session is cleared
60 reset!
61 User.current = nil
62 # Clears user's last login timestamp
63 user.update_attribute :last_login_on, nil
64 assert_nil user.reload.last_login_on
65
66 # User comes back with user's autologin cookie
67 cookies[:autologin] = token.value
68 get '/my/page'
69 assert_response :success
70 assert_template 'my/page'
71 assert_equal user.id, session[:user_id]
72 assert_not_nil user.reload.last_login_on
73 end
62 74 end
63 75
64 76 def test_autologin_should_use_autologin_cookie_name
@@ -69,7 +81,7 class AccountTest < Redmine::IntegrationTest
69 81 Redmine::Configuration.stubs(:[]).with('sudo_mode_timeout').returns(15)
70 82
71 83 with_settings :autologin => '7' do
72 assert_difference 'Token.count' do
84 assert_difference 'Token.count', 2 do
73 85 post '/login', :username => 'admin', :password => 'admin', :autologin => 1
74 86 assert_response 302
75 87 end
@@ -82,7 +94,7 class AccountTest < Redmine::IntegrationTest
82 94 get '/my/page'
83 95 assert_response :success
84 96
85 assert_difference 'Token.count', -1 do
97 assert_difference 'Token.count', -2 do
86 98 post '/logout'
87 99 end
88 100 assert cookies['custom_autologin'].blank?
@@ -119,7 +131,7 class AccountTest < Redmine::IntegrationTest
119 131 assert_equal 'Password was successfully updated.', flash[:notice]
120 132
121 133 log_user('jsmith', 'newpass123')
122 assert_equal 0, Token.count
134 assert_equal false, Token.exists?(token.id), "Password recovery token was not deleted"
123 135 end
124 136
125 137 def test_user_with_must_change_passwd_should_be_forced_to_change_its_password
@@ -36,6 +36,19 class TokenTest < ActiveSupport::TestCase
36 36 assert Token.exists?(t2.id)
37 37 end
38 38
39 def test_create_session_token_should_keep_last_10_tokens
40 Token.delete_all
41 user = User.find(1)
42
43 assert_difference 'Token.count', 10 do
44 10.times { Token.create!(:user => user, :action => 'session') }
45 end
46
47 assert_no_difference 'Token.count' do
48 Token.create!(:user => user, :action => 'session')
49 end
50 end
51
39 52 def test_destroy_expired_should_not_destroy_feeds_and_api_tokens
40 53 Token.delete_all
41 54
General Comments 0
You need to be logged in to leave comments. Login now