Skip to content

Commit bc0fca2

Browse files
committed
Add support for reauthentication against OIDC to activate sudo_mode
Currently, if the sudo_mode is enabled and the user tries to perform any sensitive action (e.g. change application settings), they have to enter their *local* password. However, if the user is logged in via OIDC, they typically don't know their local password (it has been autogenerated on the first login via OIDC). This commit implements an opt-in feature that replaces this local password verification with reauthentication on the OIDC provider, i.e. even if the user is still logged in, they are forced to re-enter their password (or any other kind of authentication) on the OIDC provider.
1 parent 0733640 commit bc0fca2

File tree

10 files changed

+112
-4
lines changed

10 files changed

+112
-4
lines changed

Gemfile

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
gem 'openid_connect', '~> 1.1.8'
22
gem 'redis', '~> 4.2'
33
gem 'hiredis', '~> 0.6.3'
4+
gem 'repost', '~> 0.4.1'

README.rdoc

+4
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ admin_role::
8787
+realm_access.roles+.
8888

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

9195

9296
== Mapping users

app/controllers/oidc_controller.rb

+30-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ class OidcController < ApplicationController
2222
skip_before_action :check_if_login_required
2323

2424
def login
25-
if !User.current.logged?
25+
if params[:reauth] || !User.current.logged?
26+
# max_age 0 forces the OIDC provider to reauthenticate the user and add
27+
# the auth_time claim to the response.
2628
redirect_to OidcSession.spawn(session).authorization_endpoint(
27-
redirect_uri: oidc_callback_url(back_url: params[:back_url]))
29+
redirect_uri: oidc_callback_url(back_url: params[:back_url]),
30+
max_age: params[:reauth] ? 0 : nil)
2831
else
2932
redirect_to my_page_path
3033
end
@@ -34,7 +37,14 @@ def callback
3437
oidc_session = OidcSession.spawn(session)
3538
oidc_session.update!(params)
3639
oidc_session.acquire!
37-
oidc_session.authorized? ? login_user : lock_user
40+
41+
if session.has_key?(:oidc_sudo_deferred) && User.current.logged?
42+
perform_sudo_action
43+
elsif oidc_session.authorized?
44+
login_user
45+
else
46+
lock_user
47+
end
3848
rescue Exception => exception
3949
logger.error "#{exception.class}: #{exception.message}"
4050
render 'callback', :status => :loop_detected
@@ -110,4 +120,21 @@ def unsuccessful_login(user)
110120
end
111121
end
112122

123+
def perform_sudo_action
124+
logger.info "Successful reauthentication for '#{User.current.login}' from #{request.remote_ip} at #{Time.now.utc}"
125+
126+
back_url = validate_back_url(params[:back_url].to_s)
127+
action = session.delete(:oidc_sudo_deferred)
128+
129+
if back_url && action && action[:back_url] == params[:back_url]
130+
if action[:options][:method] == :get
131+
redirect_to(back_url)
132+
else
133+
repost(back_url, params: action[:params], options: action[:options])
134+
end
135+
else
136+
redirect_to(my_page_path)
137+
end
138+
end
139+
113140
end

app/models/oidc_session.rb

+8-1
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ def self.spawn(session)
3535
end
3636
end
3737

38-
def authorization_endpoint(redirect_uri:)
38+
def authorization_endpoint(redirect_uri:, max_age: nil)
3939
@redirect_uri = redirect_uri
4040
save!
4141
oidc_client.authorization_uri(
4242
nonce: oidc_nonce,
4343
state: oidc_nonce,
4444
scope: oidc_scope,
45+
max_age: max_age,
4546
)
4647
end
4748

@@ -111,6 +112,12 @@ def id_token_expiration_timestamp
111112
decoded_id_token.raw_attributes['exp']
112113
end
113114

115+
def auth_time
116+
auth_time = decoded_id_token.raw_attributes['auth_time']
117+
raise Exception.new("No auth_time in id_token") unless auth_time
118+
auth_time
119+
end
120+
114121
def authorized?
115122
not access_roles.disjoint?(roles)
116123
end

app/views/settings/_redmine_oidc.html.erb

+5
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,9 @@
4848
<%= text_field_tag 'settings[session_check_users_csv]', oidc_settings.session_check_users_csv, size: 60, placeholder: 'emusterfrau,jdoe' %>
4949
BETA
5050
</p>
51+
<p>
52+
<%= label_tag 'settings[sudo_mode_reauthenticate]', l('oidc.settings.sudo_mode_reauthenticate') %>
53+
<%= check_box_tag 'settings[sudo_mode_reauthenticate]', 1, oidc_settings.sudo_mode_reauthenticate %>
54+
BETA
55+
</p>
5156
<%= error_messages_for oidc_settings %>

config/locales/de.yml

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ de:
3636
admin_role: Administrationsrolle
3737
session_check_enabled: Session Check aktivieren
3838
session_check_users_csv: Komma-separierte Liste der Logins mit Session Check (* = alle)
39+
sudo_mode_reauthenticate: Reauthentifizierung von Benutzern für sensible Aktionen über OIDC (wenn der sudo_mode aktiviert ist)
3940
error:
4041
permissions:
4142
heading: >-

config/locales/en.yml

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ en:
3636
admin_role: Administration role
3737
session_check_enabled: Enable session check
3838
session_check_users_csv: Comma-separated list of logins with session check (* = all)
39+
sudo_mode_reauthenticate: Reauthenticate users for sensitive actions via OIDC (if sudo mode is enabled)
3940
error:
4041
permissions:
4142
heading: >-

lib/redmine_oidc.rb

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
require_dependency 'redmine_oidc/application_controller_patch'
2121
require_dependency 'redmine_oidc/avatars_helper_patch'
2222
require_dependency 'redmine_oidc/hooks'
23+
require_dependency 'redmine_oidc/sudo_mode_controller_patch'
2324
end
2425

2526
module RedmineOidc

lib/redmine_oidc/settings.rb

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class Settings
3333
admin_role
3434
session_check_enabled
3535
session_check_users_csv
36+
sudo_mode_reauthenticate
3637
)
3738

3839
attr_accessor *VALID_KEYS.map(&:to_sym)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# OpenID Connect Authentication for Redmine
2+
# Copyright (C) 2020-2021 Contargo GmbH & Co. KG
3+
#
4+
# This program is free software; you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation; either version 2 of the License, or
7+
# (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 along
15+
# with this program; if not, write to the Free Software Foundation, Inc.,
16+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17+
18+
require 'repost'
19+
20+
module RedmineOidc
21+
module SudoModeControllerPatch
22+
def require_sudo_mode(*param_names)
23+
return super unless RedmineOidc.settings.enabled
24+
return super unless RedmineOidc.settings.sudo_mode_reauthenticate
25+
26+
return true if Redmine::SudoMode.active?
27+
28+
if OidcSession.spawn(session).auth_time > Redmine::SudoMode.timeout.ago.to_i
29+
logger.info "Activating sudo mode for user #{User.current.login}"
30+
Redmine::SudoMode.active!
31+
return true
32+
end
33+
34+
if param_names.blank?
35+
# This list was copied from the original `require_sudo_mode`, but without `_method`.
36+
param_names = params.keys - %w(id action controller authenticity_token utf8)
37+
end
38+
39+
back_url = url_for(**params.slice(:controller, :action, :id, :project_id).to_unsafe_hash)
40+
41+
session[:oidc_sudo_deferred] = {
42+
back_url: back_url,
43+
params: params.slice(*param_names).to_unsafe_hash,
44+
options: {
45+
method: request.method_symbol,
46+
authenticity_token: :auto,
47+
},
48+
}
49+
50+
logger.info "Reauthenticating #{User.current.login} for sudo mode"
51+
redirect_to oidc_login_path(back_url: back_url, reauth: true)
52+
53+
false
54+
end
55+
end
56+
end
57+
58+
unless Redmine::SudoMode::Controller.included_modules.include?(RedmineOidc::SudoModeControllerPatch)
59+
Redmine::SudoMode::Controller.prepend(RedmineOidc::SudoModeControllerPatch)
60+
end

0 commit comments

Comments
 (0)