diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index faeb5f09a97..b3d6d9af022 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -149,7 +149,7 @@ def cache_issuer_in_cookie else { value: current_sp.issuer, - expires: IdentityConfig.store.session_timeout_in_minutes.minutes, + expires: IdentityConfig.store.session_timeout_in_seconds.seconds, } end end @@ -162,12 +162,12 @@ def redirect_with_flash_if_timeout flash[:info] = t( 'notices.session_timedout', app_name: APP_NAME, - minutes: IdentityConfig.store.session_timeout_in_minutes, + minutes: IdentityConfig.store.session_timeout_in_seconds.seconds.in_minutes.to_i, ) elsif current_user.blank? flash[:info] = t( 'notices.session_cleared', - minutes: IdentityConfig.store.session_timeout_in_minutes, + minutes: IdentityConfig.store.session_timeout_in_seconds.seconds.in_minutes.to_i, ) end diff --git a/app/forms/openid_connect_token_form.rb b/app/forms/openid_connect_token_form.rb index 02fc37d7648..fe07f5548e2 100644 --- a/app/forms/openid_connect_token_form.rb +++ b/app/forms/openid_connect_token_form.rb @@ -34,7 +34,7 @@ def initialize(params) ATTRS.each do |key| instance_variable_set(:"@#{key}", params[key]) end - @session_expiration = IdentityConfig.store.session_timeout_in_minutes.minutes.ago + @session_expiration = IdentityConfig.store.session_timeout_in_seconds.seconds.ago @identity = find_identity_with_code end diff --git a/app/javascript/packs/session-expire-session.ts b/app/javascript/packs/session-expire-session.ts index fd94090aeb3..37d4234b6b3 100644 --- a/app/javascript/packs/session-expire-session.ts +++ b/app/javascript/packs/session-expire-session.ts @@ -8,9 +8,8 @@ const warning = Number(warningEl.dataset.warning!) * 1000; const sessionsURL = warningEl.dataset.sessionsUrl!; const sessionTimeout = Number(warningEl.dataset.sessionTimeoutIn!) * 1000; const modal = document.querySelector('lg-modal.session-timeout-modal')!; -const keepaliveEl = document.getElementById('session-keepalive-btn'); +const keepaliveButton = document.getElementById('session-keepalive-btn')!; const countdownEls: NodeListOf = modal.querySelectorAll('lg-countdown'); -const timeoutRefreshPath = warningEl.dataset.timeoutRefreshPath || ''; let sessionExpiration = new Date(Date.now() + sessionTimeout); @@ -22,19 +21,20 @@ function showModal() { }); } -function keepalive() { +function keepalive(event: MouseEvent) { const isExpired = new Date() > sessionExpiration; if (isExpired) { - document.location.href = timeoutRefreshPath; - } else { - modal.hide(); - sessionExpiration = new Date(Date.now() + sessionTimeout); - - setTimeout(showModal, sessionTimeout - warning); - countdownEls.forEach((countdownEl) => countdownEl.stop()); - extendSession(sessionsURL); + return; } + + event.preventDefault(); + modal.hide(); + sessionExpiration = new Date(Date.now() + sessionTimeout); + + setTimeout(showModal, sessionTimeout - warning); + countdownEls.forEach((countdownEl) => countdownEl.stop()); + extendSession(sessionsURL); } -keepaliveEl?.addEventListener('click', keepalive, false); +keepaliveButton.addEventListener('click', keepalive); setTimeout(showModal, sessionTimeout - warning); diff --git a/app/javascript/packs/session-timeout-ping.ts b/app/javascript/packs/session-timeout-ping.ts index a86f60254b5..0a9b4b824f9 100644 --- a/app/javascript/packs/session-timeout-ping.ts +++ b/app/javascript/packs/session-timeout-ping.ts @@ -15,7 +15,7 @@ const timeoutURL = warningEl?.dataset.timeoutUrl!; const sessionsURL = warningEl?.dataset.sessionsUrl!; const modal = document.querySelector('lg-modal.session-timeout-modal')!; -const keepaliveEl = document.getElementById('session-keepalive-btn'); +const keepaliveButton = document.getElementById('session-keepalive-btn')!; const countdownEls: NodeListOf = modal.querySelectorAll('lg-countdown'); function success({ isLive, timeout }: SessionStatus) { @@ -46,11 +46,12 @@ function success({ isLive, timeout }: SessionStatus) { const ping = () => requestSessionStatus(sessionsURL).then(success); -function keepalive() { +function keepalive(event: MouseEvent) { + event.preventDefault(); modal.hide(); countdownEls.forEach((countdownEl) => countdownEl.stop()); extendSession(sessionsURL); } -keepaliveEl?.addEventListener('click', keepalive, false); +keepaliveButton.addEventListener('click', keepalive); setTimeout(ping, start); diff --git a/app/views/session_timeout/_warning.html.erb b/app/views/session_timeout/_warning.html.erb index 62faad7810b..316afd03c3a 100644 --- a/app/views/session_timeout/_warning.html.erb +++ b/app/views/session_timeout/_warning.html.erb @@ -38,7 +38,7 @@

<%= render ButtonComponent.new( - type: :button, + url: new_user_session_url(timeout: :session, request_id: sp_session[:request_id]), id: 'session-keepalive-btn', big: true, full_width: true, diff --git a/config/application.yml.default b/config/application.yml.default index 40b68934712..23e66c2e61f 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -383,7 +383,7 @@ session_check_delay: 30 session_check_frequency: 30 session_encryption_key: session_encryptor_alert_enabled: false -session_timeout_in_minutes: 15 +session_timeout_in_seconds: 900 session_timeout_warning_seconds: 150 session_total_duration_timeout_in_minutes: 720 short_term_phone_otp_max_attempt_window_in_seconds: 10 diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb index 8b9db230954..7b7be65b522 100644 --- a/config/initializers/ahoy.rb +++ b/config/initializers/ahoy.rb @@ -15,7 +15,7 @@ Ahoy.api = false # Period of inactivity before a new visit is created -Ahoy.visit_duration = IdentityConfig.store.session_timeout_in_minutes.minutes +Ahoy.visit_duration = IdentityConfig.store.session_timeout_in_seconds.seconds Ahoy.server_side_visits = false Ahoy.geocode = false Ahoy.user_agent_parser = :browser diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 9c625df443d..2a89edfcc1c 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -24,7 +24,7 @@ config.skip_session_storage = [:http_auth] config.strip_whitespace_keys = [] config.stretches = Rails.env.test? ? 1 : 12 - config.timeout_in = IdentityConfig.store.session_timeout_in_minutes.minutes + config.timeout_in = IdentityConfig.store.session_timeout_in_seconds.seconds config.warden do |manager| manager.failure_app = CustomDeviseFailureApp diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index b2002fb3957..6681028d13e 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -16,7 +16,7 @@ write_private_id: true, # Redis expires session after N minutes - ttl: IdentityConfig.store.session_timeout_in_minutes.minutes, + ttl: IdentityConfig.store.session_timeout_in_seconds.seconds, key_prefix: "#{IdentityConfig.store.domain_name}:session:", client_pool: REDIS_POOL, diff --git a/lib/identity_config.rb b/lib/identity_config.rb index b1b58cceb31..81a2a028c04 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -417,7 +417,7 @@ def self.store config.add(:session_check_frequency, type: :integer) config.add(:session_encryption_key, type: :string) config.add(:session_encryptor_alert_enabled, type: :boolean) - config.add(:session_timeout_in_minutes, type: :integer) + config.add(:session_timeout_in_seconds, type: :integer) config.add(:session_timeout_warning_seconds, type: :integer) config.add(:session_total_duration_timeout_in_minutes, type: :integer) config.add(:show_unsupported_passkey_platform_authentication_setup, type: :boolean) diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 9551085d265..dc190a3cb63 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -37,7 +37,7 @@ def index expect(cookies[:sp_issuer]).to eq(sp.issuer) expect(cookie_expiration).to be_within(3.seconds).of( - IdentityConfig.store.session_timeout_in_minutes.minutes.from_now, + IdentityConfig.store.session_timeout_in_seconds.seconds.from_now, ) end end @@ -414,7 +414,7 @@ def index expect(flash[:info]).to eq t( 'notices.session_timedout', app_name: APP_NAME, - minutes: IdentityConfig.store.session_timeout_in_minutes, + minutes: IdentityConfig.store.session_timeout_in_seconds.seconds.in_minutes.to_i, ) end end @@ -447,7 +447,7 @@ def index expect(flash[:info]).to eq t( 'notices.session_cleared', - minutes: IdentityConfig.store.session_timeout_in_minutes, + minutes: IdentityConfig.store.session_timeout_in_seconds.seconds.in_minutes.to_i, ) end end diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index 4228e237c53..d46d8e64c21 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -199,7 +199,10 @@ scenario 'user sees warning before session times out' do minutes_and = [ - t('datetime.dotiw.minutes', count: IdentityConfig.store.session_timeout_in_minutes - 1), + t( + 'datetime.dotiw.minutes', + count: IdentityConfig.store.session_timeout_in_seconds.seconds.in_minutes.to_i - 1, + ), t('datetime.dotiw.two_words_connector'), ].join('') @@ -218,7 +221,7 @@ scenario 'user can continue browsing with refreshed CSRF token' do token = first('[name=authenticity_token]', visible: false).value - click_button t('notices.timeout_warning.signed_in.continue') + click_on t('notices.timeout_warning.signed_in.continue') expect(page).not_to have_css('.usa-js-modal--active') expect(page).to have_css( "[name=authenticity_token]:not([value='#{token}'])", @@ -292,7 +295,7 @@ expect(page).to have_css('.usa-js-modal--active', wait: 10) - click_button t('notices.timeout_warning.partially_signed_in.continue') + click_on t('notices.timeout_warning.partially_signed_in.continue') expect(page).not_to have_css('.usa-js-modal--active') expect(find_field(t('forms.registration.labels.email')).value).not_to be_blank @@ -309,10 +312,29 @@ expect(page).to have_css('.usa-js-modal--active', wait: 10) - click_button t('notices.timeout_warning.partially_signed_in.continue') + click_on t('notices.timeout_warning.partially_signed_in.continue') expect(find_field(t('account.index.email')).value).not_to be_blank end + it 'maintains partner request if the user continues after the session expires', js: true do + allow(IdentityConfig.store).to receive(:session_timeout_in_seconds).and_return(1) + allow(IdentityConfig.store).to receive(:session_check_delay).and_return(0) + allow(Devise).to receive(:timeout_in).and_return(1) + + visit_idp_from_sp_with_ial1(:oidc) + + expect(page).to have_content( + t( + 'notices.timeout_warning.partially_signed_in.message_html', + time_left_in_session_html: t('datetime.dotiw.seconds', count: 0), + ), + wait: 10, + ) + + click_on t('notices.timeout_warning.partially_signed_in.continue') + expect_branded_experience + end + it 'reloads the sign in page when cancel is clicked', js: true do allow(Devise).to receive(:timeout_in) .and_return(IdentityConfig.store.session_timeout_warning_seconds + 1) diff --git a/spec/views/layouts/application.html.erb_spec.rb b/spec/views/layouts/application.html.erb_spec.rb index c7565dccf21..6b2d93c1c7d 100644 --- a/spec/views/layouts/application.html.erb_spec.rb +++ b/spec/views/layouts/application.html.erb_spec.rb @@ -16,6 +16,7 @@ ) allow(view.request).to receive(:original_fullpath).and_return('/foobar') allow(view).to receive(:user_fully_authenticated?).and_return(false) + allow(view).to receive(:url_for).and_return('/') view.title = title_content if title_content end