Skip to content

Commit

Permalink
Merge pull request #14162 from opf/fix/autologin-cookie
Browse files Browse the repository at this point in the history
OP#48384 Allow multiple autologin tokens to be present for users
  • Loading branch information
machisuji committed Nov 24, 2023
2 parents 663d404 + 7bfec43 commit 51a5df3
Show file tree
Hide file tree
Showing 39 changed files with 697 additions and 226 deletions.
2 changes: 1 addition & 1 deletion app/components/table_component.html.erb
Expand Up @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details.
++#%>

<div class="generic-table--container">
<div class="generic-table--container" data-test-selector="<%= test_selector %>">
<div class="generic-table--results-container">
<table class="generic-table">
<colgroup>
Expand Down
4 changes: 4 additions & 0 deletions app/components/table_component.rb
Expand Up @@ -145,6 +145,10 @@ def paginate_collection(query)
.per_page(helpers.per_page_param)
end

def test_selector
self.class.name.dasherize
end

def rows
model
end
Expand Down
92 changes: 92 additions & 0 deletions app/components/users/auto_login_tokens/row_component.rb
@@ -0,0 +1,92 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Users
module AutoLoginTokens
class RowComponent < ::RowComponent
delegate :current_token, to: :table

def token
model
end

def token_data
token.data
end

def current?
token == current_token
end

def is_current # rubocop:disable Naming/PredicateName
if current?
helpers.op_icon 'icon-yes'
end
end

def device
token_data[:platform] || I18n.t('users.sessions.unknown_os')
end

def browser
name = token_data[:browser] || 'unknown browser'
version = token_data[:browser_version]
"#{name} #{version ? "(Version #{version})" : ''}"
end

def platform
token_data[:platform] || 'unknown platform'
end

def expires_on
expires = token.expires_on || (token.created_at + Setting.autologin.days)
helpers.format_date(expires)
end

def button_links
[delete_link].compact
end

def delete_link
return if current?

link_to(
helpers.op_icon('icon icon-delete'),
{ controller: '/my/auto_login_tokens', action: 'destroy', id: token.id },
class: 'button--link',
role: :button,
method: :delete,
data: { confirm: I18n.t(:text_are_you_sure), disable_with: I18n.t(:label_loading) },
title: I18n.t(:button_delete)
)
end
end
end
end
@@ -1,3 +1,5 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
Expand Down Expand Up @@ -25,32 +27,25 @@
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Sessions
class InitializeSessionService
class << self
##
# Initializes a new session for the given user.
# This services provides very little for what it is called,
# mainly caused due to the many ways a user can login.
def call(user, session)
session[:user_id] = user.id
session[:updated_at] = Time.now

if drop_old_sessions?
Rails.logger.info { "Deleting all other sessions for #{user}." }
::Sessions::UserSession.for_user(user).delete_all
end
module Users
module AutoLoginTokens
class TableComponent < ::TableComponent
columns :is_current, :browser, :device, :expires_on
sortable_columns :updated_at
options :current_token

ServiceResult.success(result: session)
def sortable?
false
end

private

##
# We can only drop old sessions if they're stored in the database
# and enabled by configuration.
def drop_old_sessions?
OpenProject::Configuration.drop_old_sessions_on_login?
def headers
[
[:is_current, { caption: I18n.t('users.sessions.current') }],
[:browser, { caption: I18n.t('users.sessions.browser') }],
[:device, { caption: I18n.t('users.sessions.device') }],
[:expires_on, { caption: I18n.t('attributes.expires_at') }]
]
end
end
end
Expand Down
8 changes: 8 additions & 0 deletions app/controllers/admin/settings/users_settings_controller.rb
Expand Up @@ -44,5 +44,13 @@ def default_breadcrumb
def show_local_breadcrumb
true
end

def settings_params
super.tap do |settings|
if settings["consent_required"] == '1' && params['toggle_consent_time'] == '1'
settings["consent_time"] = Time.zone.now.iso8601
end
end
end
end
end
2 changes: 1 addition & 1 deletion app/controllers/concerns/accounts/authentication_stages.rb
Expand Up @@ -91,7 +91,7 @@ def init_authentication_stages(after_activation:)
session[:back_url] ||= params[:back_url]

# Remember the autologin cookie decision
session[:autologin_requested] = params[:autologin]
session[:autologin_requested] = params[:autologin] == '1'

stages
end
Expand Down
67 changes: 51 additions & 16 deletions app/controllers/concerns/accounts/current_user.rb
Expand Up @@ -67,22 +67,57 @@ def check_if_login_required
# Returns the current user or nil if no user is logged in
# and starts a session if needed
def find_current_user
if session[:user_id]
# existing session
User.active.find_by(id: session[:user_id])
elsif cookies[OpenProject::Configuration['autologin_cookie_name']] && Setting::Autologin.enabled?
# auto-login feature starts a new session
user = User.try_to_autologin(cookies[OpenProject::Configuration['autologin_cookie_name']])
session[:user_id] = user.id if user
%i[
current_session_user
current_autologin_user
current_rss_key_user
current_api_key_user
].each do |method|
user = send(method)
return user if user&.logged? && user&.active?
end

nil
end

def current_session_user
return if session[:user_id].nil?

User.active.find_by(id: session[:user_id])
end

def current_autologin_user
return unless Setting::Autologin.enabled?

autologin_cookie_name = OpenProject::Configuration['autologin_cookie_name']
autologin_token = cookies[autologin_cookie_name]
return unless autologin_token

user = User.try_to_autologin(autologin_token)

if user
login_user(user)
user
elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
else
cookies.delete(autologin_cookie_name)
nil
end
end

def current_rss_key_user
if params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication does not start a session
User.find_by_rss_key(params[:key])
elsif Setting.rest_api_enabled? && api_request?
if (key = api_key_from_request) && accept_key_auth_actions.include?(params[:action])
# Use API key
User.find_by_api_key(key)
end
end
end

def current_api_key_user
return unless Setting.rest_api_enabled? && api_request?
key = api_key_from_request

if key && accept_key_auth_actions.include?(params[:action])
# Use API key
User.find_by_api_key(key)
end
end

Expand All @@ -99,7 +134,7 @@ def logged_user=(user)
def logout_user
::Users::LogoutService
.new(controller: self)
.call(current_user)
.call!(current_user)
end

# Redirect the user according to the logout scheme
Expand Down Expand Up @@ -127,8 +162,8 @@ def perform_post_logout(prev_session, prev_user)
# Login the current user
def login_user(user)
::Users::LoginService
.new(controller: self, request:)
.call(user)
.new(user:, controller: self, request:)
.call!
end

def require_login
Expand Down
17 changes: 0 additions & 17 deletions app/controllers/concerns/accounts/user_login.rb
Expand Up @@ -3,28 +3,11 @@ module Accounts::UserLogin
include ::Accounts::RedirectAfterLogin

def login_user!(user)
# generate a key and set cookie if autologin
if Setting::Autologin.enabled? && (params[:autologin] || session.delete(:autologin_requested))
set_autologin_cookie(user)
end

# Set the logged user, resetting their session
self.logged_user = user

call_hook(:controller_account_success_authentication_after, user:)

redirect_after_login(user)
end

def set_autologin_cookie(user)
token = Token::AutoLogin.create(user:)
cookie_options = {
value: token.plain_value,
expires: 1.year.from_now,
path: OpenProject::Configuration['autologin_cookie_path'],
secure: OpenProject::Configuration['autologin_cookie_secure'],
httponly: true
}
cookies[OpenProject::Configuration['autologin_cookie_name']] = cookie_options
end
end
2 changes: 1 addition & 1 deletion app/controllers/concerns/auth_source_sso.rb
Expand Up @@ -46,7 +46,7 @@ def match_sso_with_logged_user(login, user)
return user if user.login.casecmp?(login)

Rails.logger.warn { "Header-based auth source SSO user changed from #{user.login} to #{login}. Re-authenticating" }
::Users::LogoutService.new(controller: self).call(user)
::Users::LogoutService.new(controller: self).call!(user)

nil
end
Expand Down
23 changes: 23 additions & 0 deletions app/controllers/my/auto_login_tokens_controller.rb
@@ -0,0 +1,23 @@
module My
class AutoLoginTokensController < ::ApplicationController
before_action :find_token, only: %i(destroy)

layout 'my'
menu_item :sessions

def destroy
@token.destroy

flash[:notice] = I18n.t(:notice_successful_delete)
redirect_to my_sessions_path
end

private

def find_token
@token = Token::AutoLogin
.for_user(current_user)
.find(params[:id])
end
end
end
9 changes: 9 additions & 0 deletions app/controllers/my/sessions_controller.rb
Expand Up @@ -13,6 +13,15 @@ def index
@sessions = ::Sessions::UserSession
.for_user(current_user)
.order(updated_at: :desc)

@autologin_tokens = ::Token::AutoLogin
.for_user(current_user)
.order(expires_on: :asc)

token = cookies[OpenProject::Configuration['autologin_cookie_name']]
if token
@current_token = @autologin_tokens.find_by_plaintext_value(token) # rubocop:disable Rails/DynamicFindBy
end
end

def show; end
Expand Down
14 changes: 0 additions & 14 deletions app/models/sessions/sql_bypass.rb
Expand Up @@ -71,13 +71,6 @@ def save
end
end

##
# Also destroy any other session when this one is actively destroyed
def destroy
delete_user_sessions
super
end

private

def user_id
Expand Down Expand Up @@ -109,12 +102,5 @@ def update!
WHERE session_id=#{connection.quote(@retrieved_by)}
SQL
end

def delete_user_sessions
uid = user_id
return unless uid && OpenProject::Configuration.drop_old_sessions_on_logout?

::Sessions::UserSession.for_user(uid).delete_all
end
end
end
7 changes: 7 additions & 0 deletions app/models/token/auto_login.rb
Expand Up @@ -28,5 +28,12 @@

module Token
class AutoLogin < HashedToken
protected

##
# Autologin tokens might have multiple data
def single_value?
false
end
end
end

0 comments on commit 51a5df3

Please sign in to comment.