diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index bab2d5639a7943c63e7e3cf0fae513bde5045287..886ad49da7dd10e4aa99eb674684425301441ecc 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -6,7 +6,99 @@ class Admin::ApplicationController < ApplicationController include EnforcesAdminAuthentication + before_action :check_current_user_auth_mode + layout 'admin' + + def check_current_user_auth_mode + return if Feature.disabled?(:omniauth_step_up_auth_admin_mode, current_user) + + auth_mode_metadata = current_user_mode.get_auth_mode_metadata + return if auth_mode_metadata.blank? || auth_mode_metadata[:provider].blank? + + step_up_auth_errors = [] + + expected_included_id_token_claims = expected_included_id_token_claims(auth_mode_metadata[:provider], :admin_mode) + if expected_included_id_token_claims.present? && !included_id_token_claims_deep_included_in_session_data?( + session_data: auth_mode_metadata[:provider_id_token], + included_extra_values: expected_included_id_token_claims) + step_up_auth_errors << format( + _( + 'Re-authentication requires step-up authentication. ' \ + 'Given id token claims: %{given_id_token_claims} . ' \ + 'Expected included id token claims: %{expected_included_id_token_claims}' + ), + given_id_token_claims: auth_mode_metadata[:provider_id_token].to_h, + expected_included_id_token_claims: expected_included_id_token_claims.to_h + ) + end + + if step_up_auth_errors.present? + current_user_mode.disable_admin_mode! + redirect_to(new_admin_session_path, notice: step_up_auth_errors.join("\n")) + return + end + + expected_required_id_token_claims = expected_required_id_token_claims(auth_mode_metadata[:provider], :admin_mode) + if expected_required_id_token_claims.present? && !required_extra_values_included_in_session_data?( + session_data: auth_mode_metadata[:provider_id_token], + required_extra_values: expected_required_id_token_claims) + step_up_auth_errors << format( + _( + 'Re-authentication with authentication context required. ' \ + 'Given id token claims: %{given_id_token_claims} . ' \ + 'Expected required id token claims: %{expected_required_id_token_claims}' + ), + given_id_token_claims: auth_mode_metadata[:provider_id_token].to_h, + expected_required_id_token_claims: expected_required_id_token_claims.to_h + ) + end + + return if step_up_auth_errors.blank? + + current_user_mode.disable_admin_mode! + redirect_to(new_admin_session_path, notice: step_up_auth_errors.join("\n")) + end + + private + + def expected_required_id_token_claims(provider_name, scope) + provider_config = Gitlab::Auth::OAuth::Provider.config_for(provider_name) + provider_config_for_scope = provider_config.fetch('step_up_auth', {})[scope] + + provider_config_for_scope&.dig('enabled') && + provider_config_for_scope&.dig('id_token', 'required') + end + + def expected_included_id_token_claims(provider_name, scope) + provider_config = Gitlab::Auth::OAuth::Provider.config_for(provider_name) + provider_config_for_scope = provider_config.fetch('step_up_auth', {})[scope] + + {} unless provider_config_for_scope&.dig('enabled') + + provider_config_for_scope&.dig('id_token', 'included') + end + + def required_extra_values_included_in_session_data?(session_data:, required_extra_values:) + session_data = {} if session_data.blank? + required_extra_values = {} if required_extra_values.blank? + + session_data >= required_extra_values + end + + def included_id_token_claims_deep_included_in_session_data?(session_data:, included_extra_values:) + session_data = {} if session_data.blank? + included_extra_values = {} if included_extra_values.blank? + + included_extra_values.to_h.all? do |key, value| + session_data_value = session_data[key] + + next Array.wrap(value).any? { |i| session_data_value.include?(i) } if value.is_a?(Array) || value.is_a?(String) + next value.to_a.any? { |i| session_data_value.include?(i) } if value.is_a?(Hash) + + true + end + end end Admin::ApplicationController.prepend_mod_with('Admin::ApplicationController') diff --git a/app/controllers/concerns/enforces_step_up_authentication.rb b/app/controllers/concerns/enforces_step_up_authentication.rb new file mode 100644 index 0000000000000000000000000000000000000000..eafbf28f86209b562823bc6eb4cefc688dd01fa9 --- /dev/null +++ b/app/controllers/concerns/enforces_step_up_authentication.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +# == EnforcesStepUpAuthentication +# +# Controller concern to enforce step-up authentication requirements +module EnforcesStepUpAuthentication + extend ActiveSupport::Concern + + included do + before_action :check_current_user_auth_mode, except: [:route_not_found] # rubocop:disable Rails/LexicallyScopedActionFilter -- As part of the POC + end + + def check_current_user_auth_mode + namespace = step_up_auth_protected_namespace + + return if Feature.disabled?(:omniauth_step_up_auth_for_group_scope, namespace) + + return if namespace.blank? || namespace.require_step_up_authentication_through_omniauth_provider.blank? + + auth_mode_metadata = Gitlab::Auth::CurrentUserMode.new(current_user, request.session).get_auth_mode_metadata + + return unless auth_mode_metadata[:provider] == namespace.require_step_up_authentication_through_omniauth_provider + + required_omniauth_provider = namespace.require_step_up_authentication_through_omniauth_provider + + step_up_auth_errors = [] + + expected_included_id_token_claims = expected_included_id_token_claims(required_omniauth_provider, :group_scope) + if expected_included_id_token_claims.present? && !included_id_token_claims_deep_included_in_session_data?( + session_data: auth_mode_metadata[:provider_id_token], + included_extra_values: expected_included_id_token_claims) + step_up_auth_errors << format( + _( + 'Re-authentication requires step-up authentication. ' \ + 'Given id token claims: %{given_id_token_claims} . ' \ + 'Expected included id token claims: %{expected_included_id_token_claims}' + ), + given_id_token_claims: auth_mode_metadata[:provider_id_token].to_h, + expected_included_id_token_claims: expected_included_id_token_claims.to_h + ) + end + + if step_up_auth_errors.present? + current_user_mode.disable_admin_mode! + redirect_to(new_admin_session_path, notice: step_up_auth_errors.join("\n")) + return + end + + expected_required_id_token_claims = expected_required_id_token_claims(required_omniauth_provider, :group_scope) + if expected_required_id_token_claims.present? && !required_extra_values_included_in_session_data?( + session_data: auth_mode_metadata[:provider_id_token], + required_extra_values: expected_required_id_token_claims) + step_up_auth_errors << format( + _( + 'Re-authentication with authentication context required. ' \ + 'Given id token claims: %{given_id_token_claims} . ' \ + 'Expected required id token claims: %{expected_required_id_token_claims}' + ), + given_id_token_claims: auth_mode_metadata[:provider_id_token].to_h, + expected_required_id_token_claims: expected_required_id_token_claims.to_h + ) + end + + return if step_up_auth_errors.blank? + + # Include the information where to redirect after the successful step-up authentication + store_location_for(:redirect, request.fullpath) + + redirect_to(new_admin_session_path, notice: step_up_auth_errors.join("\n")) + end + + def step_up_auth_protected_namespace + raise 'Not implemented' + end + + def expected_required_id_token_claims(provider_name, scope) + provider_config = Gitlab::Auth::OAuth::Provider.config_for(provider_name) + provider_config_for_scope = provider_config.fetch('step_up_auth', {})[scope] + + provider_config_for_scope&.dig('id_token', 'required') + end + + def expected_included_id_token_claims(provider_name, scope) + provider_config = Gitlab::Auth::OAuth::Provider.config_for(provider_name) + provider_config_for_scope = provider_config.fetch('step_up_auth', {})[scope] + + provider_config_for_scope&.dig('id_token', 'included') + end + + def required_extra_values_included_in_session_data?(session_data:, required_extra_values:) + session_data = {} if session_data.blank? + required_extra_values = {} if required_extra_values.blank? + + session_data >= required_extra_values + end + + def included_id_token_claims_deep_included_in_session_data?(session_data:, included_extra_values:) + session_data = {} if session_data.blank? + included_extra_values = {} if included_extra_values.blank? + + included_extra_values.to_h.all? do |key, value| + session_data_value = session_data[key] + + next Array.wrap(value).any? { |i| session_data_value.include?(i) } if value.is_a?(Array) || value.is_a?(String) + next value.to_a.any? { |i| session_data_value.include?(i) } if value.is_a?(Hash) + + true + end + end +end diff --git a/app/controllers/concerns/groups/params.rb b/app/controllers/concerns/groups/params.rb index 7649dc971171e4fcc91650b86be379e6109a77cb..54b6a6391167c1740d5dfb38f10e2fb9853e5853 100644 --- a/app/controllers/concerns/groups/params.rb +++ b/app/controllers/concerns/groups/params.rb @@ -30,6 +30,7 @@ def group_params_attributes :parent_id, :create_chat_team, :chat_team_name, + :require_step_up_authentication_through_omniauth_provider, :require_two_factor_authentication, :two_factor_grace_period, :enabled_git_access_protocol, diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 2c38625fd1f767822ccc356f330bc49aee9976ea..e133aa1f1cb614cca18ddb83c41dc0d410f17b6f 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -2,6 +2,7 @@ class Groups::ApplicationController < ApplicationController include RoutableActions + include EnforcesStepUpAuthentication include ControllerWithCrossProjectAccessCheck include SortingHelper include SortingPreference @@ -102,6 +103,10 @@ def method_missing(method_sym, *arguments, &block) super end end + + def step_up_auth_protected_namespace + group + end end Groups::ApplicationController.prepend_mod_with('Groups::ApplicationController') diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b056b94eb19b0cce2fddf1385dde33e8a36364ee..69ffa407cb0fadd0aa28b520178407b400092aac 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -49,6 +49,8 @@ class GroupsController < Groups::ApplicationController push_frontend_feature_flag(:mr_approved_filter, type: :ops) end + skip_before_action :check_current_user_auth_mode, only: [:new, :create] + helper_method :captcha_required? skip_cross_project_access_check :index, :new, :create, :edit, :update, :destroy, :projects diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 3af432cce1df618fcf3dc39f54dc3175c281209c..45e8dae16b14439d552e3d6ce57ed47066412cfb 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -144,6 +144,8 @@ def omniauth_flow(auth_module, identity_linker: nil) set_session_active_since(oauth['provider']) if ::AuthHelper.saml_providers.include?(oauth['provider'].to_sym) track_event(current_user, oauth['provider'], 'succeeded') + set_current_user_auth_mode + if Gitlab::CurrentSettings.admin_mode return admin_mode_flow(auth_module::User) if current_user_mode.admin_mode_requested? end @@ -168,6 +170,8 @@ def omniauth_flow(auth_module, identity_linker: nil) else sign_in_user_flow(auth_module::User) end + + set_current_user_auth_mode end def link_identity(identity_linker) @@ -345,6 +349,13 @@ def store_redirect_fragment(redirect_fragment) end end + def set_current_user_auth_mode + current_user_mode.set_auth_mode_metadata( + provider: oauth.provider, + provider_id_token: oauth.extra.raw_info + ) + end + def admin_mode_flow(auth_user_class) auth_user = build_auth_user(auth_user_class) diff --git a/app/controllers/profiles/step_up_auths_controller.rb b/app/controllers/profiles/step_up_auths_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..49ecfe93bdae8d6e72c423fc2320550c0d4fe86d --- /dev/null +++ b/app/controllers/profiles/step_up_auths_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Profiles + class StepUpAuthsController < Profiles::ApplicationController + def show; end + end +end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index c59de67901c83896fe0fd64616a59c3b9ffe641a..00d1cc57f73505c55fdad20debe64419a8361c8d 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -3,6 +3,7 @@ class Projects::ApplicationController < ApplicationController include CookiesHelper include RoutableActions + include EnforcesStepUpAuthentication include ChecksCollaboration skip_before_action :authenticate_user! @@ -113,6 +114,13 @@ def handle_update_result(result) render 'edit' end end + + def step_up_auth_protected_namespace + return project&.namespace if project&.namespace.present? + + Namespace.find(params[:namespace_id]) if params[:namespace_id].present? + params[:namespace_id].present? + end end Projects::ApplicationController.prepend_mod_with('Projects::ApplicationController') diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 6b839cb8156f71411f18c31572f2afac13160102..252626b8dca9a055ba0859cdbb3fc998995dbe44 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -595,6 +595,19 @@ def check_export_rate_limit! def render_edit render 'edit' end + + def step_up_auth_protected_namespace + if action_name == 'create' + namespace_id = params.dig(:project, :namespace_id) + Namespace.find(namespace_id) if namespace_id.present? + + elsif action_name == 'new' + namespace_id = params[:namespace_id] + Namespace.find(namespace_id) if namespace_id.present? + else + project&.namespace + end + end end ProjectsController.prepend_mod_with('ProjectsController') diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index c842842f023ce350df95a6208def577763a9939f..347dc0077592485a0ab4ae94f54f8cedce4db66e 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -154,6 +154,25 @@ def button_based_providers_enabled? enabled_button_based_providers.any? end + def omniauth_providers_with_step_up_auth_config(step_up_auth_scope) + auth_providers.map { |provider| Gitlab::Auth::OAuth::Provider.config_for(provider) } + .select { |provider_config| provider_config.dig("step_up_auth", step_up_auth_scope.to_s).present? } + end + + def step_up_auth_params(provider_name, scope) + provider_config = Gitlab::Auth::OAuth::Provider.config_for(provider_name) + provider_config_for_scope = provider_config.fetch('step_up_auth', {})[scope] + return {} unless provider_config_for_scope&.fetch('enabled', false) + + provider_config_for_scope.fetch('params', false).transform_values do |v| + if v.is_a?(Hash) + v.to_json + else + v + end + end + end + def provider_image_tag(provider, size = 64) label = label_for_provider(provider) diff --git a/app/models/active_session.rb b/app/models/active_session.rb index cce5580325fe0605e09d6de7f5608800660a0a50..8820f74ef7da8cda06fd02a3da75db32380aba91 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -29,7 +29,7 @@ class ActiveSession :ip_address, :browser, :os, :device_name, :device_type, :is_impersonated, :session_id, :session_private_id, - :admin_mode + :admin_mode, :acr_values ].freeze ATTR_READER_LIST = [ :created_at, :updated_at @@ -88,7 +88,8 @@ def self.set(user, request) updated_at: timestamp, session_private_id: session_private_id, is_impersonated: request.session[:impersonator_id].present?, - admin_mode: Gitlab::Auth::CurrentUserMode.new(user, request.session).admin_mode? + admin_mode: Gitlab::Auth::CurrentUserMode.new(user, request.session).admin_mode?, + acr_values: request.session[:acr_values] ) Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml index 304266c674a3bd7c49b4559268b6910cf0bf5ee6..8867a1e2fcef908c3dbb09365e3226e6c812d7f7 100644 --- a/app/views/admin/sessions/new.html.haml +++ b/app/views/admin/sessions/new.html.haml @@ -15,4 +15,4 @@ = _('No authentication methods configured.') - if omniauth_enabled? && button_based_providers_enabled? - = render 'devise/shared/omniauth_box', render_remember_me: false + = render 'devise/shared/omniauth_box', render_remember_me: false, admin_area: true diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 19296a7c006af3b2b5d7b539458674d161c87f87..c2914e8c915270e87fd932d1397d56aabcb5992d 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,14 +1,21 @@ - render_remember_me = remember_me_enabled? && local_assigns.fetch(:render_remember_me, true) +- admin_area = remember_me_enabled? && local_assigns.fetch(:admin_area, false) - if any_form_based_providers_enabled? || password_authentication_enabled_for_web? = render 'shared/divider', text: _("or sign in with") .gl-mt-5.gl-text-center.gl-flex.gl-flex-col.gl-gap-3.js-oauth-login - enabled_button_based_providers.each do |provider| - = render 'devise/shared/omniauth_provider_button', - href: omniauth_authorize_path(:user, provider), - provider: provider, - data: { testid: test_id_for_provider(provider) } + - if admin_area + = render 'devise/shared/omniauth_provider_button', + href: omniauth_authorize_path(:user, provider, **step_up_auth_params(provider, :admin_mode)), + provider: provider, + data: { testid: test_id_for_provider(provider) } + - else + = render 'devise/shared/omniauth_provider_button', + href: omniauth_authorize_path(:user, provider), + provider: provider, + data: { testid: test_id_for_provider(provider) } - if render_remember_me = render Pajamas::CheckboxTagComponent.new(name: 'js-remember-me-omniauth', value: nil) do |c| diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 25b0b34b32ef6804dda9b2ec08a9d7cd9cc5b139..0c3d160e51a2af121a2df314aa603eaab7513471 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -40,6 +40,7 @@ = render_if_exists 'groups/settings/service_access_tokens_expiration_enforced', f: f, group: @group = render_if_exists 'groups/settings/enforce_ssh_certificates', f: f, group: @group = render 'groups/settings/two_factor_auth', f: f, group: @group + = render 'groups/settings/step_up_auth', f: f, group: @group = render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group = render 'groups/settings/membership', f: f, group: @group = render 'groups/settings/remove_dormant_members', f: f, group: @group diff --git a/app/views/groups/settings/_step_up_auth.html.haml b/app/views/groups/settings/_step_up_auth.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..6fa905e0dd5ea01786dfb789e89df8c7fc07a857 --- /dev/null +++ b/app/views/groups/settings/_step_up_auth.html.haml @@ -0,0 +1,19 @@ +%h5= _('Step-up authentication') + +%p + = _('What omniauth provider should be used for step-up auth?') + - link = link_to('', help_page_path('administration/auth/oidc.md', anchor: 'enable-step-up-authentication-for-admin-mode'), target: '_blank', rel: 'noopener noreferrer') + = safe_format(_('%{docs_link_start}Learn about step-up authentication.%{docs_link_end}'), tag_pair(link, :docs_link_start, :docs_link_end)) + +.form-group + = f.gitlab_ui_radio_component :require_step_up_authentication_through_omniauth_provider, '', _('No step-up authentication necessary'), help_text: _('Users accessing this group do not require step-up authentication.') + + - omniauth_providers_with_step_up_auth_config(:group_scope).each do |provider_config| + - help_text = _("Users accessing this group require step-up authentication through this omniauth provider.") + - step_up_auth_docs_link_url = provider_config.dig('step_up_auth', 'group_scope', 'documentation_link') + - if step_up_auth_docs_link_url.present? + - link = link_to('', step_up_auth_docs_link_url, target: '_blank', rel: 'noopener noreferrer') + - help_text_step_up_auth_docs = safe_format(_('%{docs_link_start}Learn more about the step-up auth requirements in your organization.%{docs_link_end}'), tag_pair(link, :docs_link_start, :docs_link_end)) + - help_text += ' ' + help_text_step_up_auth_docs + = f.gitlab_ui_radio_component :require_step_up_authentication_through_omniauth_provider, provider_config[:name], provider_config[:label], help_text: help_text.html_safe + diff --git a/app/views/profiles/step_up_auths/show.html.haml b/app/views/profiles/step_up_auths/show.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..768d9737e90ca5be6659ebb5c6b0e61ef4d43e39 --- /dev/null +++ b/app/views/profiles/step_up_auths/show.html.haml @@ -0,0 +1,12 @@ +- page_title _('Enter step-up auth mode') +- add_page_specific_style 'page_bundles/login' + +.row.gl-mt-5.justify-content-center + .col-md-5 + .login-page + .gl-mt-5.gl-text-center.gl-flex.gl-flex-col.gl-gap-3.js-oauth-login + - enabled_button_based_providers.each do |provider| + = render 'devise/shared/omniauth_provider_button', + href: omniauth_authorize_path(:user, provider, **step_up_auth_params(provider, :group_scope)), + provider: provider, + data: { testid: test_id_for_provider(provider) } diff --git a/app/views/user_settings/active_sessions/_active_session.html.haml b/app/views/user_settings/active_sessions/_active_session.html.haml index c98af9776e37ce27375f40348b569c6967d04166..02ee031195397d76d2a7f55a246fe8a608dd9cd7 100644 --- a/app/views/user_settings/active_sessions/_active_session.html.haml +++ b/app/views/user_settings/active_sessions/_active_session.html.haml @@ -27,6 +27,11 @@ - if active_session.try(:admin_mode) %strong= _('with Admin Mode') + %div + %strong + = _('ACR Values:') + = active_session.acr_values + - unless is_current_session .gl-float-right = link_button_to revoke_session_path(active_session), data: { confirm: _('Are you sure? The device will be signed out of GitLab and all remember me tokens revoked.'), confirm_btn_variant: :danger }, method: :delete, class: 'gl-ml-3', variant: :danger, 'aria-label': _('Revoke') do diff --git a/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_group_scope.yml b/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_group_scope.yml new file mode 100644 index 0000000000000000000000000000000000000000..dac64d37ea2aff7e6d5cc7ff3136cb1b969340c5 --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_group_scope.yml @@ -0,0 +1,9 @@ +--- +name: omniauth_step_up_auth_for_group_scope +feature_issue_url: +introduced_by_url: +rollout_issue_url: +milestone: '17.5' +group: group::authentication +type: gitlab_com_derisk +default_enabled: false diff --git a/config/feature_flags/wip/omniauth_step_up_auth_admin_mode.yml b/config/feature_flags/wip/omniauth_step_up_auth_admin_mode.yml new file mode 100644 index 0000000000000000000000000000000000000000..52fd7d857102d0ac4f8a24ea768713736141337f --- /dev/null +++ b/config/feature_flags/wip/omniauth_step_up_auth_admin_mode.yml @@ -0,0 +1,9 @@ +--- +name: omniauth_step_up_auth_admin_mode +feature_issue_url: +introduced_by_url: +rollout_issue_url: +milestone: '17.4' +group: group::authentication +type: wip +default_enabled: false diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index cbef4a662f350cbe8d3c8332ec0ab863443aea92..2dad3ff356519481e617cdbaf056e35fcf568b69 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -1152,6 +1152,42 @@ production: &base # client_secret: 'YOUR_AUTH0_CLIENT_SECRET', # domain: 'YOUR_AUTH0_DOMAIN', # scope: 'openid profile email' } } + # + # - { name: 'openid_connect', + # label: 'OpenID Connect', + # args: { + # "name":"openid_connect", + # "scope":["openid", + # "profile", + # "email"],"response_type":"code", + # "issuer":"http://localhost:8080/realms/step-up-auth-gitlab-realm", + # "client_auth_method":"query", + # "discovery":false,"uid_field":"preferred_username", + # "pkce":true,"allow_authorize_params":["acr_values", "claims"], + # "client_options": { + # ... + # } + # }, + # step_up_auth: { + # admin_mode: { + # enabled: true, + # documentation_link: "https://openid.net/specs/openid-connect-core-1_0.html#IDToken", + # required_extra_values: { + # acr: 'gold' + # }, + # params: { + # claims: { id_token: { acr: { essential: true, values: ['gold'] } } } + # } + # }, + # group_scope: { + # enabled: true, + # documentation_link: "https://openid.net/specs/openid-connect-core-1_0.html#IDToken", + # params: { + # claims: { id_token: { acr: { essential: false, values: ['silver'] } } } + # } + # } + # } + # } # FortiAuthenticator settings forti_authenticator: diff --git a/config/routes/profile.rb b/config/routes/profile.rb index 6542a50798df6243c4cbd669a37fd204433d7b72..a7429c416f6d56d42a286788aae0c1a78b306eee 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -68,6 +68,10 @@ end end + resource :step_up_auth, only: [:show] + + resources :webauthn_registrations, only: [:destroy] + resources :usage_quotas, only: [:index] end end diff --git a/db/migrate/20240829163136_add_require_step_up_authentication_to_namespaces.rb b/db/migrate/20240829163136_add_require_step_up_authentication_to_namespaces.rb new file mode 100644 index 0000000000000000000000000000000000000000..1a9dace1df8755fe489cc21b5926f9e8c373491c --- /dev/null +++ b/db/migrate/20240829163136_add_require_step_up_authentication_to_namespaces.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# See https://docs.gitlab.com/ee/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddRequireStepUpAuthenticationToNamespaces < Gitlab::Database::Migration[2.2] + milestone '17.4' + + disable_ddl_transaction! + + def up + add_column :namespaces, :require_step_up_authentication_through_omniauth_provider, :text, default: '', null: false # rubocop:disable Migration/AddColumnsToWideTables -- Disabling this cop as part of this POC + add_text_limit :namespaces, :require_step_up_authentication_through_omniauth_provider, 255 + end + + def down + remove_column :namespaces, :require_step_up_authentication_through_omniauth_provider + remove_text_limit :namespaces, :require_step_up_authentication_through_omniauth_provider + end +end diff --git a/db/schema_migrations/20240829163136 b/db/schema_migrations/20240829163136 new file mode 100644 index 0000000000000000000000000000000000000000..c67c395d3abc08bd5c41722c63ac1c715c54cb8f --- /dev/null +++ b/db/schema_migrations/20240829163136 @@ -0,0 +1 @@ +56de57d13af984619c447f7f5074228220fe2d2bc1e93636e7c5d9fef65b4f10 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 435803d0bd5275e79fb078908cfd80c7daa37108..f576ded8beac6daa42ef3bac64a385d515050eb3 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -175,7 +175,9 @@ CREATE TABLE namespaces ( shared_runners_enabled boolean DEFAULT true NOT NULL, allow_descendants_override_disabled_shared_runners boolean DEFAULT false NOT NULL, traversal_ids bigint[] DEFAULT '{}'::bigint[] NOT NULL, - organization_id bigint DEFAULT 1 + organization_id bigint DEFAULT 1, + require_step_up_authentication_through_omniauth_provider text DEFAULT ''::text NOT NULL, + CONSTRAINT check_e953598df2 CHECK ((char_length(require_step_up_authentication_through_omniauth_provider) <= 255)) ); CREATE FUNCTION find_namespaces_by_id(namespaces_id bigint) RETURNS namespaces diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md index 3a822f65390c36d78257c26a5bd6483894944f8b..651be073b326e3a79fc41c398563b96bf78bfc5f 100644 --- a/doc/administration/auth/oidc.md +++ b/doc/administration/auth/oidc.md @@ -1307,6 +1307,168 @@ response to assign users as administrator based on group membership, configure G ::EndTabs +## Enable step-up authentication for admin mode + +Step-up authentication is a security mechanism that provides an additional layer of authentication for certain privileged actions or sensitive operations, e.g. admin area access. It is commonly used in scenarios where the default level of authentication is not sufficient to protect critical resources or perform high-risk actions. With step-up authentication, users are required to provide additional credentials or undergo a more rigorous authentication process before they can access certain features or perform specific actions. This can include methods such as multi-factor authentication (MFA), biometric authentication, or one-time passwords (OTP). + +The OIDC (OpenID Connect) standard includes the concept of authentication context class references (ACR). We can leverage this ACR concept to implement step-up authentication for the admin mode in combination with the OIDC OmniAuth provider. + +To enable step-up authentication for admin mode in GitLab, follow the following steps: + +1. Go to the configuration of the OIDC OmniAuth provider in the `gitlab.yml` +1. Extend the configuration of this OIDC OmniAuth provider with the following entries + +```yaml +production: &base + omniauth: + providers: + - { name: 'openid_connect', + label: 'Provider name', + args: { + name: 'openid_connect', + ... + }, + step_up_auth: { + admin_mode: { + enabled: true, + documentation_link: "https://openid.net/specs/openid-connect-core-1_0.html#IDToken", + required_extra_values: { + acr: 'gold' + }, + params: { + claims: { id_token: { acr: { essential: true, values: ['gold'] } } } + } + } + } + } +``` + +In the above example: + +- `enabled` specifies whether the step-up authentication for admin mode is enabled or not. +- `required_extra_values` specifies the additional values that are required for step-up authentication. In this case, the `acr` value is required and has to be the value `gold` +- `params` specifies the additional parameters that are sent during the authentication process. In this case, the `acr_values` parameter is set to `'urn:example:step-up'`. + +After you have made these changes, save the `gitlab.yml` file and restart GitLab for the changes to take effect. + +NOTE: +OIDC-compatible Identity providers (IdPs) have slightly different behavior. Therefore, the setting `params` accepts a hash value that allows to define the necessary parameters to trigger step-up auth on the OIDC-compatible IdPs. The `params` values vary from what is passed to the IdP. + +### Require step-up authentication for admin area using Keycloak + +Keycloak supports step-up authentication through the definition of levels of authentication and custom browser login flows. + +To configure step-up authentication for admin mode in GitLab using Keycloak, follow these steps: + +1. **Configure Keycloak OIDC provider in GitLab** + +- Follow the steps covered in the [Configure Keycloak](#configure-keycloak) section. + +1. **Configure step-up authentication in Keycloak** + +- Follow [this detailed documenation guide](https://www.keycloak.org/docs/latest/server_admin/#_step-up-flow) that covers the configuration of the browser login flow. + +1. **Enable step-up authentication in Keycloak oidc provider configuration** + +- Edit your GitLab configuration file (`gitlab.yml` or `/etc/gitlab/gitlab.rb`) to include the step-up authentication settings: + + ```yaml + production: &base + omniauth: + providers: + - { name: 'openid_connect', + label: 'Keycloak', + args: { + name: 'openid_connect', + ... + allow_authorize_params: ["claims"] # <= These allowed authorize parameters should match the params used for the step-up mechanism + }, + step_up_auth: { + admin_mode: { + enabled: true, + documentation_link: "", + id_token: { + required: { + acr: 'gold' + } + }, + # <= Make sure that the individual params are also allowed in the configuration entry argument "args => allow_authorize_params", see above + params: { + claims: { + id_token: { acr: { essential: true, values: ['gold'] } } + } + } + }, + } + } + ``` + +- NOTE: This example assumes that the acr 'gold' is a higher security level. + +1. **Restart GitLab** + +- After making these changes, save the configuration file and restart GitLab for the changes to take effect. + +### Require step-up authentication for admin area using Microsoft Azure (MS Entra ID) + +Microsoft Entra ID (formerly Azure AD) supports step-up authentication through Conditional Access policies. + +To configure step-up authentication for admin mode in GitLab using MS Entra ID, follow these steps: + +1. **Configure MS Entra ID OIDC provider in GitLab** + +- Follow the steps covered in the [Configure Microsoft Azure](#configure-microsoft-azure) section. + +1. **Configure step-up authentication in MS Entra ID** + +- Follow [this detailed documenation guide](https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview) to design the conditional access policies. + +1. **Enable step-up authentication in MS Entra ID oidc provider configuration** + +- Edit your GitLab configuration file (`gitlab.yml` or `/etc/gitlab/gitlab.rb`) to include the step-up authentication settings: + + ```yaml + production: &base + omniauth: + providers: + - { name: 'openid_connect', + label: 'Azure OIDC', + args: { + name: 'openid_connect', + ... + allow_authorize_params: ["claims"] # <= These allowed authorize parameters should match the params used for the step-up mechanism + }, + step_up_auth: { + admin_mode: { + enabled: true, + documentation_link: "", + access_token: { + included: { + acrs: ["gold"], + }, + }, + # <= Make sure that the individual params are also allowed in the configuration entry argument "args => allow_authorize_params", see above + params: { + claims: { + id_token: { acrs: { essential: true, value: 'gold' } } + } + } + }, + } + } + ``` + +- NOTE: This example assumes that the acr 'gold' is a higher security level. +- NOTE: The MS Entra ID is highly customizable and flexible. Therefore, some MS Entra instances can deviate from the official OIDC standard. For example, instead of exposing [the official claim `acr` as a single value in the ID token](https://openid.net/specs/openid-connect-core-1_0.html#IDToken), MS Entra exposes [the claim `acrs` as a JSON-array value in the access token](https://learn.microsoft.com/en-us/entra/identity-platform/access-token-claims-reference#payload-claims). Please collaborate with your IT department team to define the correct configuration. + +1. **Restart GitLab** + +- After making these changes, save the configuration file and restart GitLab for the changes to take effect. + +### Troubleshooting + +1. Ensure that the parameters (see `step_up_auth => admin_mode => params`) are allowed in the configuration entry argument "args => allow_authorize_params". This ensures that the parameters are included in the request query parameters used to redirect to authorization endpoint of the IdP. + ## Troubleshooting 1. Ensure `discovery` is set to `true`. If you set it to `false`, you must diff --git a/lib/gitlab/auth/current_user_mode.rb b/lib/gitlab/auth/current_user_mode.rb index 7354574248ccd0fd73013936a61353be096cf641..b2f5e46ae2d89fbd15944312e8f09ad8e4f3f0b6 100644 --- a/lib/gitlab/auth/current_user_mode.rb +++ b/lib/gitlab/auth/current_user_mode.rb @@ -22,6 +22,8 @@ class CurrentUserMode ADMIN_MODE_REQUESTED_TIME_KEY = :admin_mode_requested MAX_ADMIN_MODE_TIME = 6.hours ADMIN_MODE_REQUESTED_GRACE_PERIOD = 5.minutes + AUTH_MODE_PROVIDER_KEY = :auth_mode_auth_provider + AUTH_MODE_PROVIDER_ID_TOKEN_KEY = :auth_mode_auth_provider_id_token class << self # Admin mode activation requires storing a flag in the user session. Using this @@ -160,6 +162,18 @@ def current_session_data end strong_memoize_attr :current_session_data + def set_auth_mode_metadata(provider:, provider_id_token:) + current_session_data[AUTH_MODE_PROVIDER_KEY] = provider + current_session_data[AUTH_MODE_PROVIDER_ID_TOKEN_KEY] = provider_id_token + end + + def get_auth_mode_metadata + { + provider: current_session_data[AUTH_MODE_PROVIDER_KEY], + provider_id_token: current_session_data[AUTH_MODE_PROVIDER_ID_TOKEN_KEY] + } + end + private attr_reader :user diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ec653620dd616c5e27e9d61aa4c4d5ce89b22704..597653b25f5b3f2afc7d1936fb9bd4392d0728e8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -795,9 +795,15 @@ msgstr[1] "" msgid "%{docs_link_start}How to add certificates to your project?%{docs_link_end}" msgstr "" +msgid "%{docs_link_start}Learn about step-up authentication.%{docs_link_end}" +msgstr "" + msgid "%{docs_link_start}Learn about visibility levels.%{docs_link_end}" msgstr "" +msgid "%{docs_link_start}Learn more about the step-up auth requirements in your organization.%{docs_link_end}" +msgstr "" + msgid "%{docs_link_start}Setting up a verified domain%{docs_link_end} requires being linked to a project." msgstr "" @@ -2137,6 +2143,9 @@ msgstr "" msgid "A user with write access to the source branch selected this option" msgstr "" +msgid "ACR Values:" +msgstr "" + msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'" msgstr "" @@ -21277,6 +21286,9 @@ msgstr "" msgid "Enter one or more user ID separated by commas" msgstr "" +msgid "Enter step-up auth mode" +msgstr "" + msgid "Enter the %{name} description" msgstr "" @@ -36371,6 +36383,9 @@ msgstr "" msgid "No start date – %{dueDate}" msgstr "" +msgid "No step-up authentication necessary" +msgstr "" + msgid "No suggestions found" msgstr "" @@ -44906,6 +44921,12 @@ msgstr "" msgid "Re-authentication required" msgstr "" +msgid "Re-authentication requires step-up authentication. Given id token claims: %{given_id_token_claims} . Expected included id token claims: %{expected_included_id_token_claims}" +msgstr "" + +msgid "Re-authentication with authentication context required. Given id token claims: %{given_id_token_claims} . Expected required id token claims: %{expected_required_id_token_claims}" +msgstr "" + msgid "Re-import" msgstr "" @@ -53185,6 +53206,9 @@ msgstr "" msgid "Step 4." msgstr "" +msgid "Step-up authentication" +msgstr "" + msgid "Still loading..." msgstr "" @@ -59966,6 +59990,12 @@ msgstr "" msgid "Users API rate limits" msgstr "" +msgid "Users accessing this group do not require step-up authentication." +msgstr "" + +msgid "Users accessing this group require step-up authentication through this omniauth provider." +msgstr "" + msgid "Users can launch a development environment from a GitLab browser tab when the %{linkStart}Gitpod%{linkEnd} integration is enabled." msgstr "" @@ -61639,6 +61669,9 @@ msgstr "" msgid "What is squashing?" msgstr "" +msgid "What omniauth provider should be used for step-up auth?" +msgstr "" + msgid "What templates can I create?" msgstr ""