Skip to content

Add support for reauthentication against OIDC to activate sudo_mode #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
gem 'openid_connect', '~> 2.2'
gem 'redis', '~> 4.2'
gem 'hiredis', '~> 0.6.3'
gem 'repost', '~> 0.4.1'
4 changes: 4 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ admin_role::
+realm_access.roles+.

Example: ROLES/REDMINE/ADMIN
sudo_mode_reauthenticate::
If +sudo_mode+ is enabled in Redmine, require users to reauthenticate for
sensitive actions against the OIDC provider instead of entering their local
password.


== Mapping users
Expand Down
33 changes: 30 additions & 3 deletions app/controllers/oidc_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ class OidcController < ApplicationController
skip_before_action :check_if_login_required

def login
if !User.current.logged?
if params[:reauth] || !User.current.logged?
# max_age 0 forces the OIDC provider to reauthenticate the user and add
# the auth_time claim to the response.
redirect_to OidcSession.spawn(session).authorization_endpoint(
redirect_uri: oidc_callback_url(back_url: params[:back_url]))
redirect_uri: oidc_callback_url(back_url: params[:back_url]),
max_age: params[:reauth] ? 0 : nil)
else
redirect_to my_page_path
end
Expand All @@ -34,7 +37,14 @@ def callback
oidc_session = OidcSession.spawn(session)
oidc_session.update!(params)
oidc_session.acquire!
oidc_session.authorized? ? login_user : lock_user

if session.has_key?(:oidc_sudo_deferred) && User.current.logged?
perform_sudo_action
elsif oidc_session.authorized?
login_user
else
lock_user
end
rescue Exception => exception
logger.error "#{exception.class}: #{exception.message}"
render 'callback', :status => :loop_detected
Expand Down Expand Up @@ -110,4 +120,21 @@ def unsuccessful_login(user)
end
end

def perform_sudo_action
logger.info "Successful reauthentication for '#{User.current.login}' from #{request.remote_ip} at #{Time.now.utc}"

back_url = validate_back_url(params[:back_url].to_s)
action = session.delete(:oidc_sudo_deferred)

if back_url && action && action[:back_url] == params[:back_url]
if action[:options][:method] == :get
redirect_to(back_url)
else
repost(back_url, params: action[:params], options: action[:options])
end
else
redirect_to(my_page_path)
end
end

end
9 changes: 8 additions & 1 deletion app/models/oidc_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ def self.spawn(session)
end
end

def authorization_endpoint(redirect_uri:)
def authorization_endpoint(redirect_uri:, max_age: nil)
@redirect_uri = redirect_uri
save!
oidc_client.authorization_uri(
nonce: oidc_nonce,
state: oidc_nonce,
scope: oidc_scope,
max_age: max_age,
)
end

Expand Down Expand Up @@ -111,6 +112,12 @@ def id_token_expiration_timestamp
decoded_id_token.raw_attributes['exp']
end

def auth_time
auth_time = decoded_id_token.raw_attributes['auth_time']
raise Exception.new("No auth_time in id_token") unless auth_time
auth_time
end

def authorized?
not access_roles.disjoint?(roles)
end
Expand Down
5 changes: 5 additions & 0 deletions app/views/settings/_redmine_oidc.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,9 @@
<%= text_field_tag 'settings[session_check_users_csv]', oidc_settings.session_check_users_csv, size: 60, placeholder: 'emusterfrau,jdoe' %>
BETA
</p>
<p>
<%= label_tag 'settings[sudo_mode_reauthenticate]', l('oidc.settings.sudo_mode_reauthenticate') %>
<%= check_box_tag 'settings[sudo_mode_reauthenticate]', 1, oidc_settings.sudo_mode_reauthenticate %>
BETA
</p>
<%= error_messages_for oidc_settings %>
1 change: 1 addition & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ de:
admin_role: Administrationsrolle
session_check_enabled: Session Check aktivieren
session_check_users_csv: Komma-separierte Liste der Logins mit Session Check (* = alle)
sudo_mode_reauthenticate: Reauthentifizierung von Benutzern für sensible Aktionen über OIDC (wenn der sudo_mode aktiviert ist)
error:
permissions:
heading: >-
Expand Down
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ en:
admin_role: Administration role
session_check_enabled: Enable session check
session_check_users_csv: Comma-separated list of logins with session check (* = all)
sudo_mode_reauthenticate: Reauthenticate users for sensitive actions via OIDC (if sudo mode is enabled)
error:
permissions:
heading: >-
Expand Down
1 change: 1 addition & 0 deletions lib/redmine_oidc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
require_dependency 'redmine_oidc/application_controller_patch'
require_dependency 'redmine_oidc/avatars_helper_patch'
require_dependency 'redmine_oidc/hooks'
require_dependency 'redmine_oidc/sudo_mode_controller_patch'
end

module RedmineOidc
Expand Down
1 change: 1 addition & 0 deletions lib/redmine_oidc/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Settings
admin_role
session_check_enabled
session_check_users_csv
sudo_mode_reauthenticate
)

attr_accessor *VALID_KEYS.map(&:to_sym)
Expand Down
63 changes: 63 additions & 0 deletions lib/redmine_oidc/sudo_mode_controller_patch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# OpenID Connect Authentication for Redmine
# Copyright (C) 2020-2021 Contargo GmbH & Co. KG
#
# 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.

require 'repost'

module RedmineOidc
module SudoModeControllerPatch
def require_sudo_mode(*param_names)
return super unless RedmineOidc.settings.enabled
return super unless RedmineOidc.settings.sudo_mode_reauthenticate

return true if Redmine::SudoMode.active?

if OidcSession.spawn(session).auth_time > Redmine::SudoMode.timeout.ago.to_i
logger.info "Activating sudo mode for user #{User.current.login}"
Redmine::SudoMode.active!
end

# Note: This method must be called even right after `Redmine::SudoMode.active!`
# because despite its name, it has side effects!
return true if Redmine::SudoMode.active?

if param_names.blank?
# This list was copied from the original `require_sudo_mode`, but without `_method`.
param_names = params.keys - %w(id action controller authenticity_token utf8)
end

back_url = url_for(**params.slice(:controller, :action, :id, :project_id).to_unsafe_hash)

session[:oidc_sudo_deferred] = {
back_url: back_url,
params: params.slice(*param_names).to_unsafe_hash,
options: {
method: request.method_symbol,
authenticity_token: :auto,
},
}

logger.info "Reauthenticating #{User.current.login} for sudo mode"
redirect_to oidc_login_path(back_url: back_url, reauth: true)

false
end
end
end

unless Redmine::SudoMode::Controller.included_modules.include?(RedmineOidc::SudoModeControllerPatch)
Redmine::SudoMode::Controller.prepend(RedmineOidc::SudoModeControllerPatch)
end