From 89f4b9004386005d4c681a0f83487e80e646dd72 Mon Sep 17 00:00:00 2001 From: Gerardo Navarro Date: Thu, 18 Jul 2024 08:33:30 +0200 Subject: [PATCH 01/10] Step-up auth: Add omniauth step-up auth for admin mode Step-up authentication is a security feature that requires additional verification for accessing sensitive data or performing critical actions. This commit introduces step-up authentication for the admin mode and includes the following aspects: - Extend configuration of omniauth providers in GitLab config `gitlab.yml` to allow the definition of required and included ID token claims. - Extract the ID token claims when the user signs in successfully through the omniauth provider - Check for required ID token claims and enforc step-up auth before accessing admin area - Hide feature behind the feature flag `:omniauth_step_up_auth_for_admin_mode` - Extend documentation for step-up auth configuration Changelog: added --- .../admin/application_controller.rb | 92 ++++++++ .../omniauth_callbacks_controller.rb | 11 + .../profiles/step_up_auths_controller.rb | 7 + app/helpers/auth_helper.rb | 21 ++ app/models/active_session.rb | 7 +- app/views/admin/sessions/new.html.haml | 2 +- .../devise/shared/_omniauth_box.html.haml | 15 +- .../profiles/step_up_auths/show.html.haml | 12 ++ .../active_sessions/_active_session.html.haml | 6 + .../omniauth_step_up_auth_for_admin_mode.yml | 9 + config/gitlab.yml.example | 58 ++++++ config/routes/profile.rb | 4 + doc/administration/auth/oidc.md | 196 ++++++++++++++++++ lib/gitlab/auth/current_user_mode.rb | 14 ++ locale/gitlab.pot | 12 ++ 15 files changed, 459 insertions(+), 7 deletions(-) create mode 100644 app/controllers/profiles/step_up_auths_controller.rb create mode 100644 app/views/profiles/step_up_auths/show.html.haml create mode 100644 config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_admin_mode.yml diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index bab2d5639a7943..57531ddbb84326 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_for_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] + + return {} 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/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 8c5bc248efd0d8..05e2c4a377c238 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -163,6 +163,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 @@ -408,6 +410,15 @@ def store_redirect_fragment(redirect_fragment) end end + def set_current_user_auth_mode + return if Feature.disabled?(:omniauth_step_up_auth_for_admin_mode, current_user) + + 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 00000000000000..49ecfe93bdae8d --- /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/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 7fb09d04d25226..6cfe6bed9554b8 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -165,6 +165,27 @@ 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) + return {} if Feature.disabled?(:omniauth_step_up_auth_for_admin_mode, current_user) + + 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 b7f97b1c28bf05..cad61c8e44c861 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -29,7 +29,8 @@ class ActiveSession :ip_address, :browser, :os, :device_name, :device_type, :is_impersonated, :session_id, :session_private_id, - :admin_mode + :admin_mode, + :provider_id_token ].freeze ATTR_READER_LIST = [ :created_at, :updated_at @@ -89,7 +90,9 @@ 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?, + provider_id_token: Gitlab::Auth::CurrentUserMode.new(user, + request.session).get_auth_mode_metadata[:provider_id_token] ) 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 304266c674a3bd..3c944fedca52f8 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_mode: true diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 19296a7c006af3..01f7baf118ca7f 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_mode = remember_me_enabled? && local_assigns.fetch(:admin_mode, 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_mode + = 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/profiles/step_up_auths/show.html.haml b/app/views/profiles/step_up_auths/show.html.haml new file mode 100644 index 00000000000000..768d9737e90ca5 --- /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 c98af9776e37ce..e1c69b6564ec14 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,12 @@ - if active_session.try(:admin_mode) %strong= _('with Admin Mode') + - if Feature.enabled?(:omniauth_step_up_auth_for_admin_mode, current_user) && active_session.provider_id_token.present? + %div + %strong + = _('Provider ID token') + = active_session.provider_id_token + - 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_admin_mode.yml b/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_admin_mode.yml new file mode 100644 index 00000000000000..113583e784fd6f --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_admin_mode.yml @@ -0,0 +1,9 @@ +--- +name: omniauth_step_up_auth_for_admin_mode +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/474650 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/502544 +milestone: '17.10' +group: group::authentication +type: gitlab_com_derisk +default_enabled: false diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 706cfc5c90446f..10b80c25525542 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -1163,6 +1163,64 @@ 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: { + # # Enables or disables step-up authentication for admin mode + # enabled: true, + # # URL to custom (internal) documentation explaining the identity provider configuration. + # # This link can be used to provide specific guidance to administrators on how to set up + # # and manage the identity provider for step-up authentication in your organization. + # documentation_link: "https://openid.net/specs/openid-connect-core-1_0.html#IDToken", + # id_token: { + # # The config field `included` declares which claims need to be included in the ID token + # # Example: In this (empty in this case) + # # Example: The 'amr' (Authentication Method References) claim + # # must have at least one of the defined values to pass the step-up authentication challenge. + # included: { + # amr: ['mfa', 'fpt'] + # }, + # # The config field `required` declares which claims are required in the ID token with exact match. + # # Example: The 'acr' (Authentication Context Class Reference) claim + # # must have the value 'gold' to pass the step-up authentication challenge. + # # This ensures a specific level of authentication assurance. + # required: { + # acr: 'gold' + # } + # }, + # # The `params` field defines request query parameters sent to the identity provider's authorization endpoint. + # # Example: The `claims` parameter is added to the authorization request. Its value is a JSON object, encoded and URL-escaped. + # # Hence, the resulting URL will be https://identity.provider.example.com/api?claims=%7B%22id_token%22%3A%7B%22acr%22%3A%7B%22essential%22%3Atrue%2C%22values%22%3A%5B%22gold%22%5D%7D%7D%7D + # # This instructs the identity provider to include an 'acr' claim with the value 'gold' in the ID token. + # # The 'essential: true' indicates that this claim is required for successful authentication. + # params: { + # claims: { + # id_token: { + # acr: { + # essential: true, + # values: ['gold'] + # } + # } + # } + # } + # }, + # } + # } # FortiAuthenticator settings forti_authenticator: diff --git a/config/routes/profile.rb b/config/routes/profile.rb index 2db59ec384b396..a3f865eca6a15b 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -69,6 +69,10 @@ end end + resource :step_up_auth, only: [:show] + + resources :webauthn_registrations, only: [:destroy] + resources :usage_quotas, only: [:index] end end diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md index a2f0af0de986a5..4bc2158f2b425a 100644 --- a/doc/administration/auth/oidc.md +++ b/doc/administration/auth/oidc.md @@ -1421,6 +1421,202 @@ To configure a custom duration for your ID tokens: {{< /tabs >}} +## 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 OIDC's ACR concept to configure and implement step-up authentication for different scenarios, e.g. admin mode. + +### Step-up authentication for the admin mode + +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 (`gitlab.yml`)** +1. **Choose the designated OmniAuth provider for the step-up authentication** +1. **Extend this OmniAuth provider configuration with the following entries** + +```yaml +production: &base + omniauth: + providers: + - { name: 'openid_connect', + label: 'Provider name', + args: { + name: 'openid_connect', + ... + }, + step_up_auth: { + admin_mode: { + # Enables or disables step-up authentication for admin mode + enabled: true, + # URL to custom (internal) documentation explaining the identity provider configuration. + # This link can be used to provide specific guidance to administrators on how to set up + # and manage the identity provider for step-up authentication in your organization. + documentation_link: "", + id_token: { + # The config field `included` declares which claims need to be included in the ID token + # Example: In this (empty in this case) + # Example: The 'amr' (Authentication Method References) claim + # must have at least one of the defined values to pass the step-up authentication challenge. + included: { + amr: ['mfa', 'fpt'] + }, + # The config field `required` declares which claims are required in the ID token with exact match. + # Example: The 'acr' (Authentication Context Class Reference) claim + # must have the value 'gold' to pass the step-up authentication challenge. + # This ensures a specific level of authentication assurance. + required: { + acr: 'gold' + } + }, + # The `params` field defines request query parameters sent to the identity provider's authorization endpoint. + # Example: The `claims` parameter is added to the authorization request. Its value is a JSON object, encoded and URL-escaped. + # Hence, the resulting URL will be https://identity.provider.example.com/api?claims=%7B%22id_token%22%3A%7B%22acr%22%3A%7B%22essential%22%3Atrue%2C%22values%22%3A%5B%22gold%22%5D%7D%7D%7D + # This instructs the identity provider to include an 'acr' claim with the value 'gold' in the ID token. + # The 'essential: true' indicates that this claim is required for successful authentication. + 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. +- `id_token` specifies the claims that need to be included or required in the ID token 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 `claims` parameter is set to request the `acr` value `gold`. + +1. **After you have made these changes, save the `gitlab.yml` file** + +1. **Restart GitLab for the changes to take effect** + +NOTE: +Although OIDC is standardized, different Identity Providers (IdPs) may have unique requirements. The `params` setting allows a flexible hash to define necessary parameters for step-up authentication. These values vary based on the IdP's requirements. + +#### Step-up authentication for admin mode using Keycloak + +Keycloak supports step-up authentication by defining 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. +- The Keycloak documenation defines two values for Level of Authentication, i.e. `silver` and `gold`. This values will also be used in the configuration example below. + +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' + } + }, + params: { + claims: { id_token: { acr: { essential: true, values: ['gold'] } } } + } + }, + } + } + ``` + + - NOTE: In this example config, the acr value 'gold' has been taken from - 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. + and represents a higer security level. + +1. **After you have made these changes, save the `gitlab.yml` file** + +1. **Restart GitLab for the changes to take effect** + +### Step-up authentication for admin mode 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"], + }, + }, + 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. **After you have made these changes, save the `gitlab.yml` file** + +1. **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 695a2561f16dd8..f23f086e49fd77 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 049196ff533a6b..fb46e9ed297c8d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23617,6 +23617,9 @@ msgstr "" msgid "Enter one or more user ID separated by commas" msgstr "" +msgid "Enter step-up auth mode" +msgstr "" + msgid "Enter text" msgstr "" @@ -48623,6 +48626,9 @@ msgstr "" msgid "Provider ID" msgstr "" +msgid "Provider ID token" +msgstr "" + msgid "Provision instructions" msgstr "" @@ -48971,6 +48977,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 "" -- GitLab From 8ec375366ac8b8d9b54adafc341315cce6f7d5b6 Mon Sep 17 00:00:00 2001 From: Gerardo Navarro Date: Thu, 14 Nov 2024 15:19:32 +0000 Subject: [PATCH 02/10] --- This is a combination of 14 commits. --- --- This is the 1st commit message: docs: Apply suggestions from @jglassman1 --- This is the commit message #2: refactor: Check step-up auth conditions and store result in session During a pre review of this MR, we noticed that the previous implementation checked the step-up auth conditions everytime the user wants to access the admin area. This can cause a potential performance bottleneck. As a solution, we "moved" the step-auth condition check to the handling of the callback request. This way, the step-up auth conditions have to be evaluated only once. The result of this check is stored in the session. When the user wants to access the admin area (protected with step-up auth), then it is only necessary to check the stored result of the step-up auth condition check in the session. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643#note_2195631559 - This commit als includes tests for these changes --- This is the commit message #3: Extract evaluation of step-up authentication conditions in util module --- This is the commit message #4: refactor: Apply suggestion from @GitLabDuo review - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643#note_2224763115 --- This is the commit message #5: docs: Apply suggestion from @hsutor --- This is the commit message #6: refactor: Apply suggestion from @tachyons-gitlab --- This is the commit message #7: refactor: Add states in step-up auth session Before this commit, the step-up auth implementation would store the session entry `omniauth_step_up_auth` everytime the OIDC omniauth callback is called and processed. This is unnecessary, increases the size of the session and might lead to unexpected behavior. This has been discussed during the MR review: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643#note_2226668638 As a solution, this commit adds state information to the `omniauth_step_up_auth` session entry. This way, we can know if and when the step-up auth has been requested, authenticated, or rejected. With this information, we can determine the step-up auth status and act accordingly. --- This is the commit message #8: refactor: Apply suggestion from @tachyons-gitlab - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643#note_2341186991 --- This is the commit message #9: refactor: Cherry-picking suggestions from @tachyons-gitlab - Cherry-picking commit with suggestions from @tachyons-gitlab, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181066/diffs?commit_id=3967963db6b1c427e0cb01eb61013d09103f9e08 --- This is the commit message #10: refactor: Finalize the suggestions from @tachyons-gitlab @tachyons-gitlab proposed some changes in a separate MR, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181066/diffs?commit_id=3967963db6b1c427e0cb01eb61013d09103f9e08 This commit includes the suggestions (refactoring ideas) from @tachyons-gitlab and builds on top of them. --- This is the commit message #11: refactor: Introduced StepUpAuthenticationFlow Adding the StepUpAuthenticationFlow class to encapsulate the state of the step-up authentication flow in a single class. This class can be reused accross the codebase to manage the step-up auth flow. The refactoring includes: - Adding the class `StepUpAuthenticationFlow` --- This is the commit message #12: refactor: Better step-up auth state names Renaming the state name `authenticated` to `succeeded` in order to avoid ambiguity when refering to the state of step-up authentication. --- This is the commit message #13: refactor: Remove include-matching of id token claims For the implementation of step-up authentication, we need to support two different matching options for incoming id token claims, i.e. required id token claims and included id token claims. Before this commit, we implemented both matching options. In this commit, we removed the matching option "included id token claims" in order to keep the MR more focussed on the basic implementation of step-up authenticataion. The second matching option "included id token claims" will be implemented in a follow-up MR. --- This is the commit message #14: refactor: Add documenation for why we need to disable the admin mode --- .../admin/application_controller.rb | 93 +----- .../enforces_step_up_authentication.rb | 53 ++++ .../omniauth_callbacks_controller.rb | 15 +- .../profiles/step_up_auths_controller.rb | 7 - app/helpers/auth_helper.rb | 30 +- app/models/active_session.rb | 7 +- app/views/admin/sessions/new.html.haml | 2 +- .../devise/shared/_omniauth_box.html.haml | 6 +- .../profiles/step_up_auths/show.html.haml | 12 - .../active_sessions/_active_session.html.haml | 6 - config/gitlab.yml.example | 61 +--- config/initializers/omniauth.rb | 1 + config/routes/profile.rb | 4 - doc/administration/auth/oidc.md | 278 +++++++----------- lib/gitlab/auth/current_user_mode.rb | 14 - .../oidc/step_up_auth_before_request_phase.rb | 57 ++++ .../auth/oidc/step_up_authentication.rb | 104 +++++++ .../auth/oidc/step_up_authentication_flow.rb | 80 +++++ locale/gitlab.pot | 15 +- .../enforces_admin_authentication_spec.rb | 2 +- .../enforces_step_up_authentication_spec.rb | 91 ++++++ .../omniauth_callbacks_controller_spec.rb | 89 ++++++ spec/helpers/auth_helper_spec.rb | 76 +++++ .../step_up_auth_before_request_phase_spec.rb | 99 +++++++ .../oidc/step_up_authentication_flow_spec.rb | 121 ++++++++ .../auth/oidc/step_up_authentication_spec.rb | 133 +++++++++ .../rack_middlewares/omniauth_spec.rb | 83 ++++++ .../admin/sessions/new.html.haml_spec.rb | 60 +++- 28 files changed, 1190 insertions(+), 409 deletions(-) create mode 100644 app/controllers/concerns/enforces_step_up_authentication.rb delete mode 100644 app/controllers/profiles/step_up_auths_controller.rb delete mode 100644 app/views/profiles/step_up_auths/show.html.haml create mode 100644 lib/gitlab/auth/oidc/step_up_auth_before_request_phase.rb create mode 100644 lib/gitlab/auth/oidc/step_up_authentication.rb create mode 100644 lib/gitlab/auth/oidc/step_up_authentication_flow.rb create mode 100644 spec/controllers/concerns/enforces_step_up_authentication_spec.rb create mode 100644 spec/lib/gitlab/auth/oidc/step_up_auth_before_request_phase_spec.rb create mode 100644 spec/lib/gitlab/auth/oidc/step_up_authentication_flow_spec.rb create mode 100644 spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index 57531ddbb84326..7ecbe4d2ae81f1 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -5,100 +5,9 @@ # Automatically sets the layout and ensures an administrator is logged in class Admin::ApplicationController < ApplicationController include EnforcesAdminAuthentication - - before_action :check_current_user_auth_mode + include EnforcesStepUpAuthentication layout 'admin' - - def check_current_user_auth_mode - return if Feature.disabled?(:omniauth_step_up_auth_for_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] - - return {} 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 00000000000000..8ad17e5a69f64c --- /dev/null +++ b/app/controllers/concerns/enforces_step_up_authentication.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# Enforces step-up authentication requirements for admin access +# +# This controller concern ensures users complete step-up authentication +# before accessing admin functionality. Include this module in admin +# controllers to enforce the authentication check. +# +# @example +# class Admin::ApplicationController < ApplicationController +# include EnforcesStepUpAuthentication +# end +module EnforcesStepUpAuthentication + extend ActiveSupport::Concern + + included do + before_action :enforce_step_up_authentication + end + + private + + def enforce_step_up_authentication + return if Feature.disabled?(:omniauth_step_up_auth_for_admin_mode, current_user) + + return if step_up_auth_disabled_for_admin_mode? + return if step_up_auth_flow_state_success? + + handle_failed_authentication + end + + def step_up_auth_disabled_for_admin_mode? + !::Gitlab::Auth::Oidc::StepUpAuthentication.enabled_by_config? + end + + def step_up_auth_flow_state_success? + ::Gitlab::Auth::Oidc::StepUpAuthentication.succeeded?(session) + end + + def handle_failed_authentication + # We need to disable (reset) the admin mode in order to redirect the user to the admin login page. + # If we do not do this, the Admin::SessionsController will thinks that the admin mode has been successfully reached + # and will redirect the user to the path 'admin/dashboard'. But, the check in this EnforceStepUpAuthentication + # will fail again and redirect the user to the login page which will end up in a loop. + disable_admin_mode + + redirect_to(new_admin_session_path, notice: _('Step-up auth not successful')) + end + + # TODO Do we really need this? + def disable_admin_mode + current_user_mode.disable_admin_mode! if current_user_mode.admin_mode? + end +end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 05e2c4a377c238..e61445b34b5ffe 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -163,7 +163,7 @@ 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 + handle_step_up_auth if Gitlab::CurrentSettings.admin_mode return admin_mode_flow(auth_module::User) if current_user_mode.admin_mode_requested? @@ -410,13 +410,16 @@ def store_redirect_fragment(redirect_fragment) end end - def set_current_user_auth_mode + def handle_step_up_auth return if Feature.disabled?(:omniauth_step_up_auth_for_admin_mode, current_user) - current_user_mode.set_auth_mode_metadata( - provider: oauth.provider, - provider_id_token: oauth.extra.raw_info - ) + step_up_auth_flow = + ::Gitlab::Auth::Oidc::StepUpAuthentication.build_flow(session: session, provider: oauth.provider) + + return unless step_up_auth_flow.enabled_by_config? + return unless step_up_auth_flow.requested? + + step_up_auth_flow.evaluate!(oauth.extra.raw_info) end def admin_mode_flow(auth_user_class) diff --git a/app/controllers/profiles/step_up_auths_controller.rb b/app/controllers/profiles/step_up_auths_controller.rb deleted file mode 100644 index 49ecfe93bdae8d..00000000000000 --- a/app/controllers/profiles/step_up_auths_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module Profiles - class StepUpAuthsController < Profiles::ApplicationController - def show; end - end -end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 6cfe6bed9554b8..e0ef20c80818af 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -165,25 +165,23 @@ 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) + def step_up_auth_params(provider_name, step_up_auth_scope) return {} if Feature.disabled?(:omniauth_step_up_auth_for_admin_mode, current_user) - 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) + # Get provider configuration for step up auth scope + provider_config = Gitlab::Auth::OAuth::Provider + .config_for(provider_name) + &.dig('step_up_auth', step_up_auth_scope.to_s) + &.to_h - provider_config_for_scope.fetch('params', false).transform_values do |v| - if v.is_a?(Hash) - v.to_json - else - v - end - end + return {} if provider_config.blank? + + base_params = { step_up_auth_scope: step_up_auth_scope } + config_params = provider_config['params'].to_h + + base_params + .merge!(config_params) + .transform_values { |v| v.is_a?(Hash) ? v.to_json : v } end def provider_image_tag(provider, size = 64) diff --git a/app/models/active_session.rb b/app/models/active_session.rb index cad61c8e44c861..b7f97b1c28bf05 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -29,8 +29,7 @@ class ActiveSession :ip_address, :browser, :os, :device_name, :device_type, :is_impersonated, :session_id, :session_private_id, - :admin_mode, - :provider_id_token + :admin_mode ].freeze ATTR_READER_LIST = [ :created_at, :updated_at @@ -90,9 +89,7 @@ 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?, - provider_id_token: Gitlab::Auth::CurrentUserMode.new(user, - request.session).get_auth_mode_metadata[:provider_id_token] + admin_mode: Gitlab::Auth::CurrentUserMode.new(user, request.session).admin_mode? ) 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 3c944fedca52f8..03580cbb71d738 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, admin_mode: true + = render 'devise/shared/omniauth_box', render_remember_me: false, step_up_auth_scope: 'admin_mode' diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 01f7baf118ca7f..8918c8d6adaba7 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,14 +1,14 @@ - render_remember_me = remember_me_enabled? && local_assigns.fetch(:render_remember_me, true) -- admin_mode = remember_me_enabled? && local_assigns.fetch(:admin_mode, false) +- step_up_auth_scope = local_assigns[:step_up_auth_scope] - 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| - - if admin_mode + - if step_up_auth_scope.present? = render 'devise/shared/omniauth_provider_button', - href: omniauth_authorize_path(:user, provider, **step_up_auth_params(provider, :admin_mode)), + href: omniauth_authorize_path(:user, provider, **step_up_auth_params(provider, step_up_auth_scope)), provider: provider, data: { testid: test_id_for_provider(provider) } - else diff --git a/app/views/profiles/step_up_auths/show.html.haml b/app/views/profiles/step_up_auths/show.html.haml deleted file mode 100644 index 768d9737e90ca5..00000000000000 --- a/app/views/profiles/step_up_auths/show.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- 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 e1c69b6564ec14..c98af9776e37ce 100644 --- a/app/views/user_settings/active_sessions/_active_session.html.haml +++ b/app/views/user_settings/active_sessions/_active_session.html.haml @@ -27,12 +27,6 @@ - if active_session.try(:admin_mode) %strong= _('with Admin Mode') - - if Feature.enabled?(:omniauth_step_up_auth_for_admin_mode, current_user) && active_session.provider_id_token.present? - %div - %strong - = _('Provider ID token') - = active_session.provider_id_token - - 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/gitlab.yml.example b/config/gitlab.yml.example index 10b80c25525542..1af38f49647ae1 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -1163,64 +1163,6 @@ 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: { - # # Enables or disables step-up authentication for admin mode - # enabled: true, - # # URL to custom (internal) documentation explaining the identity provider configuration. - # # This link can be used to provide specific guidance to administrators on how to set up - # # and manage the identity provider for step-up authentication in your organization. - # documentation_link: "https://openid.net/specs/openid-connect-core-1_0.html#IDToken", - # id_token: { - # # The config field `included` declares which claims need to be included in the ID token - # # Example: In this (empty in this case) - # # Example: The 'amr' (Authentication Method References) claim - # # must have at least one of the defined values to pass the step-up authentication challenge. - # included: { - # amr: ['mfa', 'fpt'] - # }, - # # The config field `required` declares which claims are required in the ID token with exact match. - # # Example: The 'acr' (Authentication Context Class Reference) claim - # # must have the value 'gold' to pass the step-up authentication challenge. - # # This ensures a specific level of authentication assurance. - # required: { - # acr: 'gold' - # } - # }, - # # The `params` field defines request query parameters sent to the identity provider's authorization endpoint. - # # Example: The `claims` parameter is added to the authorization request. Its value is a JSON object, encoded and URL-escaped. - # # Hence, the resulting URL will be https://identity.provider.example.com/api?claims=%7B%22id_token%22%3A%7B%22acr%22%3A%7B%22essential%22%3Atrue%2C%22values%22%3A%5B%22gold%22%5D%7D%7D%7D - # # This instructs the identity provider to include an 'acr' claim with the value 'gold' in the ID token. - # # The 'essential: true' indicates that this claim is required for successful authentication. - # params: { - # claims: { - # id_token: { - # acr: { - # essential: true, - # values: ['gold'] - # } - # } - # } - # } - # }, - # } - # } # FortiAuthenticator settings forti_authenticator: @@ -1722,7 +1664,8 @@ test: app_id: 'YOUR_CLIENT_ID', app_secret: 'YOUR_CLIENT_SECRET', args: { scope: 'offline_access read:jira-user read:jira-work', prompt: 'consent' } - } + } + - { name: 'openid_connect' } ldap: enabled: false servers: diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 3ea1157adc8a56..e12fc1e0162171 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -22,4 +22,5 @@ module OmniAuth::Strategies OmniAuth.config.before_request_phase do |env| Gitlab::Auth::OAuth::BeforeRequestPhaseOauthLoginCounterIncrement.call(env) + Gitlab::Auth::Oidc::StepUpAuthBeforeRequestPhase.call(env) end diff --git a/config/routes/profile.rb b/config/routes/profile.rb index a3f865eca6a15b..2db59ec384b396 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -69,10 +69,6 @@ end end - resource :step_up_auth, only: [:show] - - resources :webauthn_registrations, only: [:destroy] - resources :usage_quotas, only: [:index] end end diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md index 4bc2158f2b425a..2cc243361f838f 100644 --- a/doc/administration/auth/oidc.md +++ b/doc/administration/auth/oidc.md @@ -1421,201 +1421,137 @@ To configure a custom duration for your ID tokens: {{< /tabs >}} -## Enable step-up authentication for admin mode +## 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). +DETAILS: +**Tier:** Free +**Offering:** GitLab Self-Managed +**Status:** Experiment -The OIDC (OpenID Connect) standard includes the concept of authentication context class references (ACR). We can leverage this OIDC's ACR concept to configure and implement step-up authentication for different scenarios, e.g. admin mode. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/474650) in GitLab 17.6 [with a flag](../feature_flags.md) named `gitlab_com_derisk`. Disabled by default. -### Step-up authentication for the admin mode +Step-up authentication provides an additional layer of authentication for certain privileged actions or sensitive operations, such as Admin area access. It is used in scenarios where the default level of authentication is not sufficient to protect critical resources or perform high-risk actions. -To enable step-up authentication for admin mode in GitLab, follow the following steps: +With step-up authentication, users must 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 two-factor authentication (2FA), biometric authentication, or one-time passwords (OTP). -1. **Go to the configuration of the OIDC OmniAuth provider in the GitLab (`gitlab.yml`)** -1. **Choose the designated OmniAuth provider for the step-up authentication** -1. **Extend this OmniAuth provider configuration with the following entries** +The OIDC standard includes the concept of authentication context class references (ACR). The ACR concept is used to configure and implement step-up authentication for different scenarios, such as Admin Mode. -```yaml -production: &base - omniauth: - providers: - - { name: 'openid_connect', - label: 'Provider name', - args: { - name: 'openid_connect', - ... - }, - step_up_auth: { - admin_mode: { - # Enables or disables step-up authentication for admin mode - enabled: true, - # URL to custom (internal) documentation explaining the identity provider configuration. - # This link can be used to provide specific guidance to administrators on how to set up - # and manage the identity provider for step-up authentication in your organization. - documentation_link: "", - id_token: { - # The config field `included` declares which claims need to be included in the ID token - # Example: In this (empty in this case) - # Example: The 'amr' (Authentication Method References) claim - # must have at least one of the defined values to pass the step-up authentication challenge. - included: { - amr: ['mfa', 'fpt'] - }, - # The config field `required` declares which claims are required in the ID token with exact match. - # Example: The 'acr' (Authentication Context Class Reference) claim - # must have the value 'gold' to pass the step-up authentication challenge. - # This ensures a specific level of authentication assurance. - required: { - acr: 'gold' - } - }, - # The `params` field defines request query parameters sent to the identity provider's authorization endpoint. - # Example: The `claims` parameter is added to the authorization request. Its value is a JSON object, encoded and URL-escaped. - # Hence, the resulting URL will be https://identity.provider.example.com/api?claims=%7B%22id_token%22%3A%7B%22acr%22%3A%7B%22essential%22%3Atrue%2C%22values%22%3A%5B%22gold%22%5D%7D%7D%7D - # This instructs the identity provider to include an 'acr' claim with the value 'gold' in the ID token. - # The 'essential: true' indicates that this claim is required for successful authentication. - 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. -- `id_token` specifies the claims that need to be included or required in the ID token 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 `claims` parameter is set to request the `acr` value `gold`. - -1. **After you have made these changes, save the `gitlab.yml` file** - -1. **Restart GitLab for the changes to take effect** - -NOTE: -Although OIDC is standardized, different Identity Providers (IdPs) may have unique requirements. The `params` setting allows a flexible hash to define necessary parameters for step-up authentication. These values vary based on the IdP's requirements. - -#### Step-up authentication for admin mode using Keycloak +### Enable step-up authentication for Admin Mode -Keycloak supports step-up authentication by defining 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. -- The Keycloak documenation defines two values for Level of Authentication, i.e. `silver` and `gold`. This values will also be used in the configuration example below. - -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' - } - }, - params: { - claims: { id_token: { acr: { essential: true, values: ['gold'] } } } - } - }, - } - } - ``` +To enable step-up authentication for Admin Mode: - - NOTE: In this example config, the acr value 'gold' has been taken from - 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. - and represents a higer security level. +1. Go to the OIDC OmniAuth provider configuration YAML file in GitLab (`gitlab.yml`). +1. Choose the designated OmniAuth provider for the step-up authentication. +1. Update this OmniAuth provider's configuration: -1. **After you have made these changes, save the `gitlab.yml` file** + ```yaml + production: &base + omniauth: + providers: + - { name: 'openid_connect', + label: 'Provider name', + args: { + name: 'openid_connect', + ... + }, + step_up_auth: { + admin_mode: { + # URL to custom (internal) documentation explaining the identity provider configuration. + # This link can be used to provide specific guidance to administrators on how to set up + # and manage the identity provider for step-up authentication in your organization. + documentation_link: "", + id_token: { + # The config field `required` declares which claims are required in the ID token with exact match. + # Example: The 'acr' (Authentication Context Class Reference) claim + # must have the value 'gold' to pass the step-up authentication challenge. + # This ensures a specific level of authentication assurance. + required: { + acr: 'gold' + } + }, + # The `params` field defines request query parameters sent to the identity provider's authorization endpoint. + # Example: The `claims` parameter is added to the authorization request. Its value is a JSON object, encoded and URL-escaped. + # Hence, the resulting URL will be https://identity.provider.example.com/api?claims=%7B%22id_token%22%3A%7B%22acr%22%3A%7B%22essential%22%3Atrue%2C%22values%22%3A%5B%22gold%22%5D%7D%7D%7D + # This instructs the identity provider to include an 'acr' claim with the value 'gold' in the ID token. + # The 'essential: true' indicates that this claim is required for successful authentication. + params: { + claims: { + id_token: { + acr: { + essential: true, + values: ['gold'] + } + } + } + } + }, + } + } + ``` -1. **Restart GitLab for the changes to take effect** + In the preceding example: -### Step-up authentication for admin mode using Microsoft Azure (MS Entra ID) + - `enabled` specifies whether the step-up authentication for Admin Mode is enabled or not. + - `id_token` specifies the claims that need to be required in the ID token 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 `claims` parameter is set to request the `acr` value `gold`. -Microsoft Entra ID (formerly Azure AD) supports step-up authentication through Conditional Access policies. +1. Save the `gitlab.yml` configuration file. -To configure step-up authentication for admin mode in GitLab using MS Entra ID, follow these steps: +1. Restart GitLab for the changes to take effect. -1. **Configure MS Entra ID OIDC provider in GitLab** +NOTE: +Although OIDC is standardized, different Identity Providers (IdPs) might have unique requirements. The `params` setting allows a flexible hash to define necessary parameters for step-up authentication. These values vary based on the IdP's requirements. -- Follow the steps covered in the [Configure Microsoft Azure](#configure-microsoft-azure) section. +#### Enable step-up authentication for Admin Mode using Keycloak -1. **Configure step-up authentication in MS Entra ID** +Keycloak supports step-up authentication by defining levels of authentication and custom browser login flows. -- Follow [this detailed documenation guide](https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview) to design the conditional access policies. +To configure step-up authentication for Admin Mode in GitLab using Keycloak: -1. **Enable step-up authentication in MS Entra ID oidc provider configuration** +1. [Configure Keycloak](#configure-keycloak) in GitLab. -- Edit your GitLab configuration file (`gitlab.yml` or `/etc/gitlab/gitlab.rb`) to include the step-up authentication settings: +1. [Create a browser login flow with step-up authentication in Keycloak](https://www.keycloak.org/docs/latest/server_admin/#_step-up-flow). - ```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"], - }, - }, - params: { - claims: { - id_token: { - acrs: { essential: true, value: 'gold' } - } - }, - } - }, - } - } - ``` + The Keycloak documentation defines two values for level of authentication, `silver` and `gold`. + Use these values in the following configuration example. -- 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. Edit your GitLab configuration file (`gitlab.yml` or `/etc/gitlab/gitlab.rb`) to enable + step-up authentication in the Keycloak OIDC provider configuration. -1. **After you have made these changes, save the `gitlab.yml` file** + ```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: { + documentation_link: "", + id_token: { + required: { + acr: 'gold' + } + }, + params: { + claims: { id_token: { acr: { essential: true, values: ['gold'] } } } + } + }, + } + } + ``` -1. **Restart GitLab for the changes to take effect** + - In this example configuration file, the `acr` value is 'gold'. Follow the [Keycloak documentation on creating a browser login flow with step-up authentication](https://www.keycloak.org/docs/latest/server_admin/#_step-up-flow) that represents a higher security level. -### Troubleshooting +1. Save the `gitlab.yml` file. -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. +1. Restart GitLab for the changes to take effect. ## Troubleshooting @@ -1635,3 +1571,5 @@ To configure step-up authentication for admin mode in GitLab using MS Entra ID, your OpenID web server configuration. For example, for [`oauth2-server-php`](https://github.com/bshaffer/oauth2-server-php), you may have to [add a configuration parameter to Apache](https://github.com/bshaffer/oauth2-server-php/issues/926#issuecomment-387502778). + +1. **Step-up authentication only**: 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 the authorization endpoint of the IdP. diff --git a/lib/gitlab/auth/current_user_mode.rb b/lib/gitlab/auth/current_user_mode.rb index f23f086e49fd77..695a2561f16dd8 100644 --- a/lib/gitlab/auth/current_user_mode.rb +++ b/lib/gitlab/auth/current_user_mode.rb @@ -22,8 +22,6 @@ 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 @@ -162,18 +160,6 @@ 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/lib/gitlab/auth/oidc/step_up_auth_before_request_phase.rb b/lib/gitlab/auth/oidc/step_up_auth_before_request_phase.rb new file mode 100644 index 00000000000000..2b75a371fc241f --- /dev/null +++ b/lib/gitlab/auth/oidc/step_up_auth_before_request_phase.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Oidc + # Handles the step-up authentication request phase for OAuth flow + # + # This module manages the initial phase of step-up authentication, + # setting up the session state for admin mode authentication. + module StepUpAuthBeforeRequestPhase + class << self + def call(env) + return if current_user_from(env).blank? + return if Feature.disabled?(:omniauth_step_up_auth_for_admin_mode, current_user_from(env)) + + # If the step-up authentication scope is not included in the request params, + # then step-up authentication is likely not requested and we do not need to proceed. + return unless step_up_auth_requested_for_admin_mode?(env) + + session = session_from(env) + provider = current_provider_from(env) + step_up_auth_flow = + ::Gitlab::Auth::Oidc::StepUpAuthentication.build_flow(session: session, provider: provider) + + return unless step_up_auth_flow.enabled_by_config? + + # This method will set the state to 'requested' in the session + step_up_auth_flow.request! + end + + private + + def step_up_auth_requested_for_admin_mode?(env) + request_param_step_up_auth_scope_from(env) == + ::Gitlab::Auth::Oidc::StepUpAuthentication::STEP_UP_AUTH_SCOPE_ADMIN_MODE.to_s + end + + def current_user_from(env) + env['warden']&.user + end + + def current_provider_from(env) + env['omniauth.strategy']&.name + end + + def request_param_step_up_auth_scope_from(env) + env.dig('rack.request.query_hash', 'step_up_auth_scope').to_s + end + + def session_from(env) + env['rack.session'] + end + end + end + end + end +end diff --git a/lib/gitlab/auth/oidc/step_up_authentication.rb b/lib/gitlab/auth/oidc/step_up_authentication.rb new file mode 100644 index 00000000000000..20aa8851ae8c1a --- /dev/null +++ b/lib/gitlab/auth/oidc/step_up_authentication.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Oidc + # Handles step-up authentication configuration and validation for OAuth providers + # + # This module manages the configuration and validation of step-up authentication + # requirements for OAuth providers, particularly focusing on admin mode access. + module StepUpAuthentication + STEP_UP_AUTH_SCOPE_ADMIN_MODE = :admin_mode + + class << self + # Checks if step-up authentication is enabled for the step-up auth scope 'admin_mode' + # + # @return [Boolean] true if any OAuth provider requires step-up auth for admin mode + def enabled_by_config?(scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE) + oauth_providers.any? do |provider| + enabled_for_provider?(provider_name: provider, scope: scope) + end + end + + # Checks if step-up authentication configuration exists for a provider name + # + # @param oauth_provider_name [String] the name of the OAuth provider + # @param scope [Symbol] the scope to check configuration for (default: :admin_mode) + # @return [Boolean] true if configuration exists + def enabled_for_provider?(provider_name:, scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE) + has_required_claims?(provider_name, scope) + end + + # Verifies if step-up authentication has succeeded for any provider + # with the step-up auth scope 'admin_mode' + # + # @param session [Hash] the session hash containing authentication state + # @return [Boolean] true if step-up authentication is authenticated + def succeeded?(session, scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE) + step_up_auth_flows = + session&.dig('omniauth_step_up_auth') + .to_h + .flat_map do |provider, step_up_auth_object| + step_up_auth_object.map do |step_up_auth_scope, _| + ::Gitlab::Auth::Oidc::StepUpAuthenticationFlow.new( + session: session, + provider: provider, + scope: step_up_auth_scope + ) + end + end + step_up_auth_flows + .select { |step_up_auth_flow| step_up_auth_flow.scope.to_s == scope.to_s } + .select(&:enabled_by_config?) + .any?(&:succeeded?) + end + + # Validates if all step-up authentication conditions are met + # + # @param oauth [OAuth2::AccessToken] the OAuth object to validate + # @param scope [Symbol] the scope to validate conditions for (default: :admin_mode) + # @return [Boolean] true if all conditions are fulfilled + def conditions_fulfilled?(oauth_extra_metadata:, provider:, scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE) + required_conditions_fulfilled?(oauth_extra_metadata: oauth_extra_metadata, provider: provider, scope: scope) + end + + def build_flow(provider:, session:, scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE) + Gitlab::Auth::Oidc::StepUpAuthenticationFlow.new(provider: provider, scope: scope, session: session) + end + + private + + def oauth_providers + Gitlab::Auth::OAuth::Provider.providers || [] + end + + def has_required_claims?(provider_name, scope) + get_id_token_claims_required_conditions(provider_name, scope).present? + end + + def get_id_token_claims_required_conditions(provider_name, scope) + dig_provider_config(provider_name, scope, 'required') + end + + def dig_provider_config(provider_name, scope, claim_type) + Gitlab::Auth::OAuth::Provider + .config_for(provider_name.to_s) + &.dig('step_up_auth', scope.to_s, 'id_token', claim_type) + end + + def required_conditions_fulfilled?(oauth_extra_metadata:, provider:, scope:) + conditions = get_id_token_claims_required_conditions(provider, scope) + return false if conditions.blank? + + raw_info = oauth_extra_metadata.presence || {} + subset?(raw_info, conditions) + end + + def subset?(hash, subset_hash) + hash.with_indifferent_access >= subset_hash + end + end + end + end + end +end diff --git a/lib/gitlab/auth/oidc/step_up_authentication_flow.rb b/lib/gitlab/auth/oidc/step_up_authentication_flow.rb new file mode 100644 index 00000000000000..c2fd0de5310541 --- /dev/null +++ b/lib/gitlab/auth/oidc/step_up_authentication_flow.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Oidc + class StepUpAuthenticationFlow + STATE_REQUESTED = :requested + STATE_SUCCEEDED = :succeeded + STATE_FAILED = :failed + + attr_reader :session, :provider, :scope + + def initialize(session:, provider:, scope:) + @session = session + @provider = provider + @scope = scope + end + + def requested? + state.to_s == STATE_REQUESTED.to_s + end + + def succeeded? + state.to_s == STATE_SUCCEEDED.to_s + end + + def rejected? + state.to_s == STATE_FAILED.to_s + end + + def enabled_by_config? + ::Gitlab::Auth::Oidc::StepUpAuthentication + .enabled_for_provider?(provider_name: provider, scope: scope) + end + + def evaluate!(oidc_id_token_claims) + if conditions_fulfilled?(oidc_id_token_claims) + succeed! + else + fail! + end + end + + def request! + update_session_state(STATE_REQUESTED) + end + + def succeed! + update_session_state(STATE_SUCCEEDED) + end + + def fail! + update_session_state(STATE_FAILED) + end + + private + + def state + provider_scope_session_data&.[]('state').to_s.presence + end + + def provider_scope_session_data + session.dig('omniauth_step_up_auth', provider.to_s, scope.to_s) + end + + def update_session_state(new_state) + session['omniauth_step_up_auth'] ||= {} + session['omniauth_step_up_auth'][provider.to_s] ||= {} + session['omniauth_step_up_auth'][provider.to_s][scope.to_s] ||= {} + session['omniauth_step_up_auth'][provider.to_s][scope.to_s]['state'] = new_state.to_s + end + + def conditions_fulfilled?(oidc_id_token_claims) + ::Gitlab::Auth::Oidc::StepUpAuthentication + .conditions_fulfilled?(oauth_extra_metadata: oidc_id_token_claims, provider: provider, scope: scope) + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fb46e9ed297c8d..1afed3351159be 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23617,9 +23617,6 @@ msgstr "" msgid "Enter one or more user ID separated by commas" msgstr "" -msgid "Enter step-up auth mode" -msgstr "" - msgid "Enter text" msgstr "" @@ -48626,9 +48623,6 @@ msgstr "" msgid "Provider ID" msgstr "" -msgid "Provider ID token" -msgstr "" - msgid "Provision instructions" msgstr "" @@ -48977,12 +48971,6 @@ 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 "" @@ -58075,6 +58063,9 @@ msgstr "" msgid "Step 4." msgstr "" +msgid "Step-up auth not successful" +msgstr "" + msgid "Still loading..." msgstr "" diff --git a/spec/controllers/concerns/enforces_admin_authentication_spec.rb b/spec/controllers/concerns/enforces_admin_authentication_spec.rb index 331e1ada73b2ab..9b2b2fbdb235a0 100644 --- a/spec/controllers/concerns/enforces_admin_authentication_spec.rb +++ b/spec/controllers/concerns/enforces_admin_authentication_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe EnforcesAdminAuthentication do +RSpec.describe EnforcesAdminAuthentication, feature_category: :system_access do let(:user) { create(:user) } before do diff --git a/spec/controllers/concerns/enforces_step_up_authentication_spec.rb b/spec/controllers/concerns/enforces_step_up_authentication_spec.rb new file mode 100644 index 00000000000000..76b7f8e23c82c9 --- /dev/null +++ b/spec/controllers/concerns/enforces_step_up_authentication_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EnforcesStepUpAuthentication, feature_category: :system_access do + include AdminModeHelper + + controller(ApplicationController) do + include EnforcesStepUpAuthentication + + def index + head :ok + end + end + + let_it_be(:user) { create(:user) } + + subject do + get :index + response + end + + before do + sign_in(user) + end + + shared_examples 'passing check for step up authentication' do + it { is_expected.to have_gitlab_http_status(:ok) } + end + + shared_examples 'redirecting to new_admin_session_path' do + it { is_expected.to redirect_to(new_admin_session_path) } + + context 'when feature flag :omniauth_step_up_auth_for_admin_mode is disabled' do + before do + stub_feature_flags(omniauth_step_up_auth_for_admin_mode: false) + end + + it_behaves_like 'passing check for step up authentication' + end + end + + describe '#enforce_step_up_authentication' do + using RSpec::Parameterized::TableSyntax + + let(:provider_without_step_up_auth) { GitlabSettings::Options.new(name: 'google_oauth2') } + let(:provider_with_step_up_auth) do + GitlabSettings::Options.new( + name: 'openid_connect', + step_up_auth: { + admin_mode: { + id_token: { + required: { + acr: 'gold' + } + } + } + } + ) + end + + let(:step_up_auth_session_succeeded) do + { 'openid_connect' => { 'admin_mode' => { 'state' => 'succeeded' } } } + end + + let(:step_up_auth_session_failed) { { 'openid_connect' => { 'admin_mode' => { 'state' => 'failed' } } } } + + before do + stub_omniauth_setting(enabled: true, providers: oauth_providers) + + session.merge!(omniauth_step_up_auth: step_up_auth_session) + end + + # rubocop:disable Layout/LineLength -- Avoid formatting table to keep oneline table syntax + where(:oauth_providers, :step_up_auth_session, :expected_examples) do + [] | nil | 'passing check for step up authentication' + [] | ref(:step_up_auth_session_succeeded) | 'passing check for step up authentication' + [] | ref(:step_up_auth_session_failed) | 'passing check for step up authentication' + [ref(:provider_without_step_up_auth)] | nil | 'passing check for step up authentication' + + [ref(:provider_with_step_up_auth), ref(:provider_without_step_up_auth)] | nil | 'redirecting to new_admin_session_path' + [ref(:provider_with_step_up_auth)] | ref(:step_up_auth_session_succeeded) | 'passing check for step up authentication' + [ref(:provider_with_step_up_auth)] | ref(:step_up_auth_session_failed) | 'redirecting to new_admin_session_path' + end + # rubocop:enable Layout/LineLength + + with_them do + it_behaves_like params[:expected_examples] + end + end +end diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index 562377490e8f6a..b5c8167d121374 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -809,6 +809,95 @@ end it_behaves_like "sets provider_2FA session variable according to bypass_two_factor return value" + + context 'for step-up authentication' do + context 'with different step-up authentication configurations' do + using RSpec::Parameterized::TableSyntax + + let(:ommiauth_provider_config_with_step_up_auth) do + GitlabSettings::Options.new( + name: "openid_connect", + step_up_auth: { + admin_mode: { + id_token: { + required: required_id_token_claims + } + } + } + ) + end + + before do + mock_auth_hash(provider, extern_uid, user.email, additional_info: { extra: { raw_info: mock_auth_hash_extra_raw_info } }) + + request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth'] + session['omniauth_step_up_auth'] = { 'openid_connect' => { 'admin_mode' => { 'state' => 'requested' } } } + + stub_omniauth_setting(enabled: true, auto_link_user: true, block_auto_created_users: false, providers: [ommiauth_provider_config_with_step_up_auth]) + end + + where(:required_id_token_claims, :mock_auth_hash_extra_raw_info, :step_up_auth_authenticated) do + { claim_1: 'gold' } | { claim_1: 'gold' } | 'succeeded' + { claim_1: 'gold' } | { claim_1: 'gold', claim_2: 'mfa' } | 'succeeded' + { claim_1: 'gold' } | { claim_1: 'gold', claim_2: 'other_amr' } | 'succeeded' + { claim_1: 'gold' } | { claim_1: 'silver' } | 'failed' + { claim_1: 'gold' } | { claim_1: 'silver', claim_2: 'other_amr' } | 'failed' + { claim_1: 'gold' } | { claim_1: nil } | 'failed' + { claim_1: 'gold' } | { claim_3: 'other_value' } | 'failed' + { claim_1: 'gold' } | {} | 'failed' + end + + with_them do + context 'when user is signed in' do + before do + sign_in user + end + + it 'evaluates step-up authentication conditions and stores result in session' do + get provider + + expect(session.to_h.dig('omniauth_step_up_auth', 'openid_connect', 'admin_mode', 'state')) + .to eq step_up_auth_authenticated + end + + context 'when feature flag :omniauth_step_up_auth_for_admin_mode disabled' do + before do + stub_feature_flags(omniauth_step_up_auth_for_admin_mode: false) + session.delete 'omniauth_step_up_auth' + end + + it 'does not store step-up authentication evaluation result in session' do + get provider + + expect(session).not_to include 'omniauth_step_up_auth' + end + end + end + + context 'when user is not signed in' do + before do + session.delete 'omniauth_step_up_auth' + end + + it 'does not store step-up authentication evaluation result in session' do + get provider + + expect(session).not_to include 'omniauth_step_up_auth' + end + end + end + end + + context 'without step-up authentication configuration' do + let(:ommiauth_provider_config_with_step_up_auth) { GitlabSettings::Options.new(name: "openid_connect") } + + it 'does not add session key "step_up_auth"' do + get provider + + expect(session).not_to include 'step_up_auth' + end + end + end end describe '#saml' do diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index 032ffd280a7170..52f07b3ddcad9b 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -708,4 +708,80 @@ def auth_active? .to eq('http://www.w3.org/2001/04/xmlenc#sha256') end end + + describe '#step_up_auth_params' do + using RSpec::Parameterized::TableSyntax + + let(:current_user) { instance_double('User', flipper_id: '1') } + + let(:oidc_setting_with_step_up_auth) do + GitlabSettings::Options.new( + name: "openid_connect", + step_up_auth: { + admin_mode: { + params: { + claims: { acr_values: 'gold' }, + prompt: 'login' + } + } + } + ) + end + + let(:oidc_setting_without_step_up_auth_params) do + GitlabSettings::Options.new(name: "openid_connect", + step_up_auth: { admin_mode: { id_token: { required: { acr: 'gold' } } } }) + end + + let(:oidc_setting_without_step_up_auth) do + GitlabSettings::Options.new(name: "openid_connect") + end + + subject { helper.step_up_auth_params(provider_name, scope) } + + before do + allow(helper).to receive(:current_user).and_return(current_user) + stub_omniauth_setting(enabled: true, providers: omniauth_setting_providers) + end + + # rubocop:disable Layout/LineLength -- Ensure one-line table syntax for better readability + where(:omniauth_setting_providers, :provider_name, :scope, :expected_result) do + [ref(:oidc_setting_with_step_up_auth)] | 'openid_connect' | :admin_mode | { step_up_auth_scope: :admin_mode, 'claims' => '{"acr_values":"gold"}', 'prompt' => 'login' } + [ref(:oidc_setting_with_step_up_auth)] | 'openid_connect' | 'admin_mode' | { step_up_auth_scope: 'admin_mode', 'claims' => '{"acr_values":"gold"}', 'prompt' => 'login' } + [ref(:oidc_setting_with_step_up_auth)] | 'openid_connect' | nil | {} + [ref(:oidc_setting_with_step_up_auth)] | 'missing_provider_name' | :admin_mode | {} + [ref(:oidc_setting_with_step_up_auth)] | nil | nil | {} + + [] | 'openid_connect' | :admin_mode | {} + [ref(:oidc_setting_without_step_up_auth)] | 'openid_connect' | :admin_mode | {} + [ref(:oidc_setting_without_step_up_auth_params)] | 'openid_connect' | :admin_mode | { step_up_auth_scope: :admin_mode } + end + # rubocop:enable Layout/LineLength + + with_them do + it { is_expected.to eq expected_result } + + context 'when feature flag :omniauth_step_up_auth_for_admin_mode is disabled' do + before do + stub_feature_flags(omniauth_step_up_auth_for_admin_mode: false) + end + + it { is_expected.to eq({}) } + end + end + 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) + return {} if Feature.disabled?(:omniauth_step_up_auth_for_admin_mode, current_user) + + provider_config_for_scope = Gitlab::Auth::OAuth::Provider.config_for(provider_name)&.dig('step_up_auth', scope) + return {} if provider_config_for_scope&.dig('enabled').blank? + + provider_config_for_scope&.dig('params')&.transform_values { |v| v.is_a?(Hash) ? v.to_json : v } + end end diff --git a/spec/lib/gitlab/auth/oidc/step_up_auth_before_request_phase_spec.rb b/spec/lib/gitlab/auth/oidc/step_up_auth_before_request_phase_spec.rb new file mode 100644 index 00000000000000..56e41502a2ef95 --- /dev/null +++ b/spec/lib/gitlab/auth/oidc/step_up_auth_before_request_phase_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Oidc::StepUpAuthBeforeRequestPhase, feature_category: :system_access do + using RSpec::Parameterized::TableSyntax + + let(:env) do + { + 'omniauth.strategy' => class_double(OmniAuth::Strategies, name: provider), + 'rack.request.query_hash' => { 'step_up_auth_scope' => step_up_auth_scope }, + 'rack.session' => session, + 'warden' => instance_double(Warden::Proxy, user: build_stubbed(:user)) + } + end + + let(:session) { {} } + let(:session_with_step_up_state_requested) do + { 'omniauth_step_up_auth' => { 'openid_connect' => { 'admin_mode' => { 'state' => 'requested' } } } } + end + + let(:provider_step_up_auth_name) { provider_step_up_auth.name } + let(:provider_step_up_auth) do + GitlabSettings::Options.new( + name: 'openid_connect', + step_up_auth: { + admin_mode: { + id_token: { + required: { + acr: 'gold' + } + } + } + } + ) + end + + before do + stub_omniauth_setting(enabled: true, providers: [provider_step_up_auth]) + end + + describe '.call' do + subject(:call_middleware) { described_class.call(env) } + + # rubocop:disable Layout/LineLength -- Avoid formatting to keep one-line table syntax + where(:provider, :step_up_auth_scope, :session, :expected_session) do + ref(:provider_step_up_auth_name) | 'admin_mode' | {} | ref(:session_with_step_up_state_requested) + ref(:provider_step_up_auth_name) | 'admin_mode' | ref(:session_with_step_up_state_requested) | ref(:session_with_step_up_state_requested) + ref(:provider_step_up_auth_name) | 'admin_mode' | { 'omniauth_step_up_auth' => { 'other_provider' => { 'admin_mode' => { 'state' => 'requested' } } } } | lazy { session.deep_merge(session_with_step_up_state_requested) } + ref(:provider_step_up_auth_name) | 'admin_mode' | { 'other_session_key' => 'other_session_value' } | lazy { session.deep_merge(session_with_step_up_state_requested) } + ref(:provider_step_up_auth_name) | 'other_scope' | {} | {} + ref(:provider_step_up_auth_name) | 'other_scope' | ref(:session_with_step_up_state_requested) | ref(:session_with_step_up_state_requested) + ref(:provider_step_up_auth_name) | '' | {} | {} + ref(:provider_step_up_auth_name) | nil | {} | {} + 'other_provider' | 'admin_mode' | {} | {} + nil | 'admin_mode' | {} | {} + nil | nil | {} | {} + end + # rubocop:enable Layout/LineLength + + with_them do + it 'sets the session state to requested' do + call_middleware + + expect(session).to eq(expected_session) + end + + context 'when feature flag :omniauth_step_up_auth_for_admin_mode is disabled' do + before do + stub_feature_flags(omniauth_step_up_auth_for_admin_mode: false) + end + + it 'does not modify the session' do + expect { call_middleware }.not_to change { session } + end + end + end + + context 'when user is not authenticated' do + let(:env) { super().merge('warden' => instance_double(Warden::Proxy, user: nil)) } + let(:provider) { provider_step_up_auth_name } + let(:step_up_auth_scope) { 'admin_mode' } + + it 'does not modify the session' do + expect { call_middleware }.not_to change { session } + end + end + + context 'when warden is blank' do + let(:env) { super().merge('warden' => nil) } + let(:provider) { provider_step_up_auth_name } + let(:step_up_auth_scope) { 'admin_mode' } + + it 'does not modify the session' do + expect { call_middleware }.not_to change { session } + end + end + end +end diff --git a/spec/lib/gitlab/auth/oidc/step_up_authentication_flow_spec.rb b/spec/lib/gitlab/auth/oidc/step_up_authentication_flow_spec.rb new file mode 100644 index 00000000000000..9f645d21030707 --- /dev/null +++ b/spec/lib/gitlab/auth/oidc/step_up_authentication_flow_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Oidc::StepUpAuthenticationFlow, feature_category: :system_access do + let(:session) { { 'omniauth_step_up_auth' => { provider => { scope => { 'state' => 'requested' } } } } } + let(:provider) { 'openid_connect' } + let(:scope) { 'admin_mode' } + + subject(:flow) { described_class.new(session: session, provider: provider, scope: scope) } + + describe '#requested?' do + context 'when state is requested' do + let(:session) do + { 'omniauth_step_up_auth' => { provider => { scope => { 'state' => 'requested' } } } } + end + + it { is_expected.to be_requested } + end + + context 'when state is not requested' do + let(:session) do + { 'omniauth_step_up_auth' => { provider => { scope => { 'state' => 'succeeded' } } } } + end + + it { is_expected.not_to be_requested } + end + end + + describe '#succeeded?' do + context 'when state is authenticated' do + let(:session) do + { 'omniauth_step_up_auth' => { provider => { scope => { 'state' => 'succeeded' } } } } + end + + it { is_expected.to be_succeeded } + end + + context 'when state is not authenticated' do + let(:session) do + { 'omniauth_step_up_auth' => { provider => { scope => { 'state' => 'requested' } } } } + end + + it { is_expected.not_to be_succeeded } + end + end + + describe '#enabled_by_config?' do + before do + allow(::Gitlab::Auth::Oidc::StepUpAuthentication).to receive(:enabled_for_provider?).and_return(true) + end + + it { is_expected.to be_enabled_by_config } + + context 'when not enabled by config' do + before do + allow(::Gitlab::Auth::Oidc::StepUpAuthentication).to receive(:enabled_for_provider?).and_return(false) + end + + it { is_expected.not_to be_enabled_by_config } + end + end + + describe '#evaluate!' do + let(:oidc_id_token_claims) { { 'claim_1' => 'gold' } } + + subject(:evaluate_flow) { flow.evaluate!(oidc_id_token_claims) } + + context 'when conditions are fulfilled' do + before do + allow(::Gitlab::Auth::Oidc::StepUpAuthentication).to receive(:conditions_fulfilled?).and_return(true) + end + + it 'sets the state to authenticated' do + expect { evaluate_flow }.to change { flow.succeeded? }.from(false).to(true) + end + end + + context 'when conditions are not fulfilled' do + before do + allow(::Gitlab::Auth::Oidc::StepUpAuthentication).to receive(:conditions_fulfilled?).and_return(false) + end + + it 'sets the state to authenticated' do + expect { evaluate_flow }.not_to change { flow.succeeded? } + end + + it 'sets the state to rejected' do + expect { evaluate_flow }.to change { flow.rejected? }.from(false).to(true) + end + end + end + + describe '#request!' do + let(:session) { { 'omniauth_step_up_auth' => { provider => { scope => { 'state' => 'requested' } } } } } + + it 'does not change the state' do + expect { flow.request! }.not_to change { flow.requested? } + end + + context 'when the state is authenticated' do + let(:session) { { 'omniauth_step_up_auth' => { provider => { scope => { 'state' => 'succeeded' } } } } } + + it 'does not change the state' do + expect { flow.request! }.to change { flow.requested? }.from(false).to(true) + end + end + end + + describe '#succeed!' do + it 'sets the state to authenticated' do + expect { flow.succeed! }.to change { flow.succeeded? }.from(false).to(true) + end + end + + describe '#fail!' do + it 'sets the state to rejected' do + expect { flow.fail! }.to change { flow.rejected? }.from(false).to(true) + end + end +end diff --git a/spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb b/spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb new file mode 100644 index 00000000000000..89cef53f92acb8 --- /dev/null +++ b/spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Oidc::StepUpAuthentication, feature_category: :system_access do + using RSpec::Parameterized::TableSyntax + + let(:ommiauth_provider_config) do + GitlabSettings::Options.new( + name: "openid_connect", + step_up_auth: { + admin_mode: { + id_token: { + required: required_id_token_claims + } + } + } + ) + end + + let(:auth_hash_openid_connect) do + OmniAuth::AuthHash.new({ + provider: 'openid_connect', + info: { + name: 'mockuser', + email: 'mockuser@example.com', + image: 'mock_user_thumbnail_url' + }, + extra: { + raw_info: { + info: { + name: 'mockuser', + email: 'mockuser@example.com', + image: 'mock_user_thumbnail_url' + }, + **auth_hash_openid_connect_extra_raw_info + } + } + }) + end + + let(:auth_hash_openid_connect_extra_raw_info) { {} } + + describe '.config_exists?' do + let(:auth_hash_other_provider) { OmniAuth::AuthHash.new({ provider: 'other_provider' }) } + + subject { described_class.enabled_for_provider?(provider_name: oauth_auth_hash.provider, scope: scope) } + + before do + stub_omniauth_setting(enabled: true, providers: [ommiauth_provider_config]) + end + + where(:required_id_token_claims, :oauth_auth_hash, :scope, :expected_result) do + { claim_1: 'gold' } | ref(:auth_hash_openid_connect) | :admin_mode | true + { claim_1: 'gold' } | ref(:auth_hash_openid_connect) | :unsupported_scope | false + {} | ref(:auth_hash_openid_connect) | :admin_mode | false + {} | ref(:auth_hash_openid_connect) | 'admin_mode' | false + {} | ref(:auth_hash_openid_connect) | nil | false + {} | ref(:auth_hash_other_provider) | :admin_mode | false + nil | ref(:auth_hash_openid_connect) | :admin_mode | false + end + + with_them do + it { is_expected.to eq expected_result } + end + end + + describe '.conditions_fulfilled?' do + subject do + described_class.conditions_fulfilled?( + oauth_extra_metadata: auth_hash_openid_connect_extra_raw_info, + provider: 'openid_connect', + scope: :admin_mode + ) + end + + before do + stub_omniauth_setting(enabled: true, providers: [ommiauth_provider_config]) + end + + # -- Avoid formatting to ensure one-line table syntax + where(:required_id_token_claims, :auth_hash_openid_connect_extra_raw_info, :expected_result) do + { claim_1: 'gold' } | { claim_1: 'gold' } | true + { claim_1: 'gold' } | { claim_1: 'gold', claim_3: 'other_value' } | true + { claim_1: 'gold' } | { claim_1: 'silver' } | false + { claim_1: 'gold' } | { claim_1: ['gold'] } | false + { claim_1: 'gold' } | { claim_1: nil } | false + { claim_1: 'gold' } | { claim_3: 'other_value' } | false + { claim_1: 'gold' } | {} | false + { claim_1: 'gold', claim_3: 'other_value' } | { claim_1: 'gold' } | false + { claim_1: 'gold', claim_3: 'other_value' } | { claim_1: 'gold', claim_3: 'other_value' } | true + { claim_1: ['gold'] } | { claim_1: 'gold' } | false + { claim_1: ['gold'] } | { claim_1: ['gold'] } | true + {} | { claim_1: 'gold' } | false + nil | { claim_1: 'gold' } | false + end + with_them do + it { is_expected.to eq expected_result } + end + end + + describe '.succeeded?' do + let(:required_id_token_claims) { { claim_1: 'gold' } } + + let(:session) { { 'omniauth_step_up_auth' => step_up_auth_session } } + + subject { described_class.succeeded?(session) } + + before do + stub_omniauth_setting(enabled: true, providers: [ommiauth_provider_config]) + end + + # rubocop:disable Layout/LineLength -- Avoid formatting to ensure one-line table syntax + where(:step_up_auth_session, :expected_result) do + lazy { { 'openid_connect' => { 'admin_mode' => { 'state' => 'succeeded' } } } } | true + lazy { { 'openid_connect' => { 'admin_mode' => { 'state' => 'failed' } } } } | false + lazy { { 'other_provider' => { 'admin_mode' => { 'state' => 'succeeded' } } } } | false + lazy { { 'openid_connect' => { 'admin_mode' => { 'state' => 'succeeded' } }, 'other_provider' => { 'admin_mode' => { 'state' => 'failed' } } } } | true + lazy { { 'openid_connect' => { 'admin_mode' => { 'state' => 'succeeded' } }, 'other_provider' => { 'admin_mode' => { 'state' => 'succeeded' } } } } | true + lazy { { 'openid_connect' => { 'admin_mode' => { 'state' => 'failed' } }, 'other_provider' => { 'admin_mode' => { 'state' => 'failed' } } } } | false + lazy { { 'openid_connect' => { 'admin_mode' => { 'state' => 'failed' } }, 'other_provider' => { 'admin_mode' => { 'state' => 'succeeded' } } } } | false + lazy { { 'openid_connect' => { 'other_mode' => { 'state' => 'succeeded' } }, 'other_provider' => { 'admin_mode' => { 'state' => 'failed' } } } } | false + + nil | false + {} | false + end + # rubocop:enable Layout/LineLength + + with_them do + it { is_expected.to eq expected_result } + end + end +end diff --git a/spec/requests/rack_middlewares/omniauth_spec.rb b/spec/requests/rack_middlewares/omniauth_spec.rb index ae5fddb0ec0e4e..be4ff6c76c03f6 100644 --- a/spec/requests/rack_middlewares/omniauth_spec.rb +++ b/spec/requests/rack_middlewares/omniauth_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'OmniAuth Rack middlewares', feature_category: :system_access do + include SessionHelpers + describe 'OmniAuth before_request_phase callback' do it 'increments Prometheus counter' do expect { post('/users/auth/google_oauth2') } @@ -13,4 +15,85 @@ }.by(1) end end + + describe 'OmniAuth before_request_phase callback for step-up authentication' do + let_it_be(:user) { create(:user) } + + let(:oauth_provider_config_with_step_up_auth) do + GitlabSettings::Options.new( + name: "openid_connect", + step_up_auth: { + admin_mode: { + id_token: { + required: { + acr: 'gold' + } + } + } + } + ) + end + + let(:step_up_auth_scope) { 'admin_mode' } + + let(:expected_step_up_auth_session_hash) do + { 'omniauth_step_up_auth' => { 'openid_connect' => { 'admin_mode' => { 'state' => 'requested' } } } } + end + + subject(:response_session) do + post("/users/auth/openid_connect?step_up_auth_scope=#{step_up_auth_scope}") + session.to_h + end + + before do + stub_omniauth_setting(enabled: true, providers: [oauth_provider_config_with_step_up_auth]) + + sign_in(user) + end + + it { is_expected.to include(expected_step_up_auth_session_hash) } + + context 'with blank step_up_auth_scope param' do + let(:step_up_auth_scope) { '' } + + it { is_expected.not_to include('omniauth_step_up_auth') } + end + + context 'with invalid step_up_auth_scope param' do + let(:step_up_auth_scope) { 'invalid_scope' } + + it { is_expected.not_to include('omniauth_step_up_auth') } + end + + context 'without step_up_auth_scope param' do + subject(:response_session) do + post('/users/auth/openid_connect') + session.to_h + end + + it { is_expected.not_to include('omniauth_step_up_auth') } + end + + context 'when session for omniauth_step_up_auth is available', :clean_gitlab_redis_sessions do + before do + stub_session( + session_data: { + 'omniauth_step_up_auth' => { 'openid_connect' => { 'admin_mode' => { 'state' => 'requested' } } } + }, + user_id: user.id + ) + end + + it { is_expected.to include(expected_step_up_auth_session_hash) } + end + + context 'when requesting step-up auth for unconfigured provider' do + subject(:response_session) do + post('/users/auth/auth0?step_up_auth_scope=admin_mode') + session.to_h + end + + it { is_expected.not_to include('omniauth_step_up_auth') } + end + end end diff --git a/spec/views/admin/sessions/new.html.haml_spec.rb b/spec/views/admin/sessions/new.html.haml_spec.rb index f244c124f5f26f..4707abc8e9116a 100644 --- a/spec/views/admin/sessions/new.html.haml_spec.rb +++ b/spec/views/admin/sessions/new.html.haml_spec.rb @@ -2,9 +2,13 @@ require 'spec_helper' -RSpec.describe 'admin/sessions/new.html.haml' do +RSpec.describe 'admin/sessions/new.html.haml', feature_category: :system_access do + include RenderedHtml + let(:user) { create(:admin) } + let(:page) { rendered_html } + before do disable_all_signin_methods @@ -39,6 +43,12 @@ allow(view).to receive(:password_authentication_enabled_for_web?).and_return(true) end + let(:openid_connect_button_action_url) do + URI(rendered_html.find_button('Openid Connect').ancestor('form')[:action]) + end + + let(:openid_connect_button_action_url_query) { Rack::Utils.parse_query(openid_connect_button_action_url.query) } + it 'shows omniauth form' do render @@ -48,6 +58,54 @@ end expect(rendered).to have_css('.js-oauth-login') end + + context 'when step-up auth config is set' do + let(:oidc_step_up_auth_options) do + GitlabSettings::Options.new( + name: "openid_connect", + step_up_auth: { + admin_mode: { + params: { + claims: { acr_values: 'gold' } + } + } + } + ) + end + + let(:oidc_step_up_auth_options_without_params) do + GitlabSettings::Options.new(name: "openid_connect", step_up_auth: { admin_mode: {} }) + end + + before do + stub_omniauth_setting(enabled: true, providers: [oidc_step_up_auth_options]) + + allow(view).to receive(:omniauth_enabled?).and_return(true) + allow(view).to receive(:auth_providers).and_return(['openid_connect']) + end + + it 'includes additional params related to step-up auth in form action url' do + render + + expect(rendered).to have_button('Openid Connect') + + expect(openid_connect_button_action_url).to have_attributes(path: '/users/auth/openid_connect') + expect(openid_connect_button_action_url_query).to include('claims' => '{"acr_values":"gold"}') + end + + context 'when feature flag :omniauth_step_up_auth_for_admin_mode is disabled' do + before do + stub_feature_flags(omniauth_step_up_auth_for_admin_mode: false) + end + + it 'does not include additional params related to step-up auth in form action url' do + render + + expect(rendered).to have_button('Openid Connect') + expect(openid_connect_button_action_url).to have_attributes(path: '/users/auth/openid_connect', query: nil) + end + end + end end context 'ldap authentication' do -- GitLab From 3bc16adcabeec4bbe90efbe0d662bc739834ce8e Mon Sep 17 00:00:00 2001 From: Gerardo Navarro Date: Wed, 26 Feb 2025 10:11:36 +0100 Subject: [PATCH 03/10] refactor: Apply suggestions from @tachyons-gitlab - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643#note_2363088884 - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643#note_2363092621 --- .../concerns/enforces_step_up_authentication.rb | 1 - lib/gitlab/auth/oidc/step_up_authentication.rb | 11 +++++++++++ lib/gitlab/auth/oidc/step_up_authentication_flow.rb | 10 ++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/controllers/concerns/enforces_step_up_authentication.rb b/app/controllers/concerns/enforces_step_up_authentication.rb index 8ad17e5a69f64c..29f508f61f1a59 100644 --- a/app/controllers/concerns/enforces_step_up_authentication.rb +++ b/app/controllers/concerns/enforces_step_up_authentication.rb @@ -46,7 +46,6 @@ def handle_failed_authentication redirect_to(new_admin_session_path, notice: _('Step-up auth not successful')) end - # TODO Do we really need this? def disable_admin_mode current_user_mode.disable_admin_mode! if current_user_mode.admin_mode? end diff --git a/lib/gitlab/auth/oidc/step_up_authentication.rb b/lib/gitlab/auth/oidc/step_up_authentication.rb index 20aa8851ae8c1a..a8261bb8f4e768 100644 --- a/lib/gitlab/auth/oidc/step_up_authentication.rb +++ b/lib/gitlab/auth/oidc/step_up_authentication.rb @@ -66,6 +66,17 @@ def build_flow(provider:, session:, scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE) Gitlab::Auth::Oidc::StepUpAuthenticationFlow.new(provider: provider, scope: scope, session: session) end + # Slices the relevant ID token claims from the provided OAuth raw information. + # + # @param oauth_raw_info [Hash] The raw information received from the OAuth provider. + # @param provider [String] The name of the OAuth provider. + # @param scope [String] The scope of the authentication request, default is STEP_UP_AUTH_SCOPE_ADMIN_MODE. + # @return [Hash] A hash containing only the relevant ID token claims. + def slice_relevant_id_token_claims(oauth_raw_info:, provider:, scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE) + relevant_id_token_claims = (get_id_token_claims_required_conditions(provider, scope) || {}).keys + oauth_raw_info.slice(*relevant_id_token_claims) + end + private def oauth_providers diff --git a/lib/gitlab/auth/oidc/step_up_authentication_flow.rb b/lib/gitlab/auth/oidc/step_up_authentication_flow.rb index c2fd0de5310541..a45b1537b5ce15 100644 --- a/lib/gitlab/auth/oidc/step_up_authentication_flow.rb +++ b/lib/gitlab/auth/oidc/step_up_authentication_flow.rb @@ -29,11 +29,17 @@ def rejected? end def enabled_by_config? - ::Gitlab::Auth::Oidc::StepUpAuthentication - .enabled_for_provider?(provider_name: provider, scope: scope) + ::Gitlab::Auth::Oidc::StepUpAuthentication.enabled_for_provider?(provider_name: provider, scope: scope) end def evaluate!(oidc_id_token_claims) + oidc_id_token_claims = + ::Gitlab::Auth::Oidc::StepUpAuthentication.slice_relevant_id_token_claims( + oauth_raw_info: oidc_id_token_claims, + provider: provider, + scope: scope + ) + if conditions_fulfilled?(oidc_id_token_claims) succeed! else -- GitLab From f3b0025f9367072b4d17208111f2fa28f4b28675 Mon Sep 17 00:00:00 2001 From: Gerardo Navarro Date: Wed, 26 Feb 2025 12:17:27 +0100 Subject: [PATCH 04/10] docs: Fix vale errors after rebase --- doc/administration/auth/oidc.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md index 2cc243361f838f..4806a31288fd89 100644 --- a/doc/administration/auth/oidc.md +++ b/doc/administration/auth/oidc.md @@ -1423,18 +1423,25 @@ To configure a custom duration for your ID tokens: ## Step-up authentication for Admin Mode -DETAILS: -**Tier:** Free -**Offering:** GitLab Self-Managed -**Status:** Experiment +{{< details >}} + +- Tier: Free +- Offering: GitLab Self-Managed +- Status: Experiment + +{{< /details >}} + +{{< history >}} -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/474650) in GitLab 17.6 [with a flag](../feature_flags.md) named `gitlab_com_derisk`. Disabled by default. +- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/474650) in GitLab 17.10 [with a flag](../feature_flags.md) named `gitlab_com_derisk`. Disabled by default. + +{{< /history >}} Step-up authentication provides an additional layer of authentication for certain privileged actions or sensitive operations, such as Admin area access. It is 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 must 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 two-factor authentication (2FA), biometric authentication, or one-time passwords (OTP). -The OIDC standard includes the concept of authentication context class references (ACR). The ACR concept is used to configure and implement step-up authentication for different scenarios, such as Admin Mode. +The OIDC standard includes the concept of authentication context class references (`ACR`). The `ACR` concept is used to configure and implement step-up authentication for different scenarios, such as Admin Mode. ### Enable step-up authentication for Admin Mode @@ -1501,8 +1508,11 @@ To enable step-up authentication for Admin Mode: 1. Restart GitLab for the changes to take effect. -NOTE: -Although OIDC is standardized, different Identity Providers (IdPs) might have unique requirements. The `params` setting allows a flexible hash to define necessary parameters for step-up authentication. These values vary based on the IdP's requirements. +{{< alert type="note" >}} + +Although OIDC is standardized, different Identity Providers (IdPs) might have unique requirements. The `params` setting allows a flexible hash to define necessary parameters for step-up authentication. These values vary based on the requirements of the IdP. + +{{< /alert >}} #### Enable step-up authentication for Admin Mode using Keycloak -- GitLab From 5fdbff73c6a63d8528ac6dac437924007239d252 Mon Sep 17 00:00:00 2001 From: Gerardo Navarro Date: Mon, 24 Mar 2025 15:12:01 +0100 Subject: [PATCH 05/10] docs: Update the feature flag omniauth_step_up_auth_for_admin_mode --- .../gitlab_com_derisk/omniauth_step_up_auth_for_admin_mode.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_admin_mode.yml b/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_admin_mode.yml index 113583e784fd6f..abfbbe2e5eef8d 100644 --- a/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_admin_mode.yml +++ b/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_admin_mode.yml @@ -3,7 +3,7 @@ name: omniauth_step_up_auth_for_admin_mode feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/474650 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/502544 -milestone: '17.10' +milestone: '17.11' group: group::authentication type: gitlab_com_derisk default_enabled: false -- GitLab From dd28d0af417e2081e285eb11ef57259f53e1a0a4 Mon Sep 17 00:00:00 2001 From: Gerardo Navarro Date: Mon, 24 Mar 2025 15:23:18 +0100 Subject: [PATCH 06/10] ci: Fix undercoverage job failure In the CI pipeline, the undercoverage job failed in https://gitlab.com/gitlab-community/gitlab/-/jobs/9499747662#L129 . Unfortunately, this undercoverage failure was due to a false positive because the code lines mentinoed in the undercoverage output are actually executed during the tests. --- app/helpers/auth_helper.rb | 4 +++- lib/gitlab/auth/oidc/step_up_authentication.rb | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index e0ef20c80818af..101bcc9a28bff8 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -181,7 +181,9 @@ def step_up_auth_params(provider_name, step_up_auth_scope) base_params .merge!(config_params) - .transform_values { |v| v.is_a?(Hash) ? v.to_json : v } + .transform_values do |v| + v.is_a?(Hash) ? v.to_json : v + end end def provider_image_tag(provider, size = 64) diff --git a/lib/gitlab/auth/oidc/step_up_authentication.rb b/lib/gitlab/auth/oidc/step_up_authentication.rb index a8261bb8f4e768..a59f6a405f78fe 100644 --- a/lib/gitlab/auth/oidc/step_up_authentication.rb +++ b/lib/gitlab/auth/oidc/step_up_authentication.rb @@ -48,7 +48,9 @@ def succeeded?(session, scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE) end end step_up_auth_flows - .select { |step_up_auth_flow| step_up_auth_flow.scope.to_s == scope.to_s } + .select do |step_up_auth_flow| + step_up_auth_flow.scope.to_s == scope.to_s + end .select(&:enabled_by_config?) .any?(&:succeeded?) end -- GitLab From a74191f2ae7e0685975a943be0acde6dcc7789a9 Mon Sep 17 00:00:00 2001 From: Gerardo Navarro Date: Mon, 24 Mar 2025 17:14:14 +0100 Subject: [PATCH 07/10] docs: Apply suggestions from @idurham --- doc/administration/auth/oidc.md | 67 +++++++++++++++------------------ 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md index 4806a31288fd89..f6c42f715d4115 100644 --- a/doc/administration/auth/oidc.md +++ b/doc/administration/auth/oidc.md @@ -1425,7 +1425,7 @@ To configure a custom duration for your ID tokens: {{< details >}} -- Tier: Free +- Tier: Free, Premium, Ultimate - Offering: GitLab Self-Managed - Status: Experiment @@ -1433,23 +1433,27 @@ To configure a custom duration for your ID tokens: {{< history >}} -- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/474650) in GitLab 17.10 [with a flag](../feature_flags.md) named `gitlab_com_derisk`. Disabled by default. +- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/474650) in GitLab 17.11 [with a flag](../feature_flags.md) named `gitlab_com_derisk`. Disabled by default. {{< /history >}} -Step-up authentication provides an additional layer of authentication for certain privileged actions or sensitive operations, such as Admin area access. It is used in scenarios where the default level of authentication is not sufficient to protect critical resources or perform high-risk actions. +In some cases, default authentication methods don't sufficiently protect critical resources or +high-risk actions. Step-up authentication adds an extra authentication layer for privileged actions +or sensitive operations, such as accessing the Admin area. -With step-up authentication, users must 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 two-factor authentication (2FA), biometric authentication, or one-time passwords (OTP). +With step-up authentication, users must provide additional credentials before they can access +certain features or perform specific actions. These additional methods can include methods such +as two-factor authentication (2FA), biometric authentication, or one-time passwords (OTP). -The OIDC standard includes the concept of authentication context class references (`ACR`). The `ACR` concept is used to configure and implement step-up authentication for different scenarios, such as Admin Mode. +The OIDC standard includes authentication context class references (`ACR`). The `ACR` concept +helps configure and implement step-up authentication for different scenarios, such as Admin Mode. ### Enable step-up authentication for Admin Mode To enable step-up authentication for Admin Mode: -1. Go to the OIDC OmniAuth provider configuration YAML file in GitLab (`gitlab.yml`). -1. Choose the designated OmniAuth provider for the step-up authentication. -1. Update this OmniAuth provider's configuration: +1. Edit your GitLab configuration file (`gitlab.yml` or `/etc/gitlab/gitlab.rb`) to enable + step-up authentication for an specific OmniAuth provider. ```yaml production: &base @@ -1463,23 +1467,22 @@ To enable step-up authentication for Admin Mode: }, step_up_auth: { admin_mode: { - # URL to custom (internal) documentation explaining the identity provider configuration. - # This link can be used to provide specific guidance to administrators on how to set up + # The `documentation_link` field is a URL to custom (internal) documentation that explains + # the identity provider configuration. Use this to provide guidance on how to set up # and manage the identity provider for step-up authentication in your organization. documentation_link: "", id_token: { - # The config field `required` declares which claims are required in the ID token with exact match. - # Example: The 'acr' (Authentication Context Class Reference) claim + # The `required` field declares which claims are required in the ID token with exact match. + # In this example, the 'acr' (Authentication Context Class Reference) claim # must have the value 'gold' to pass the step-up authentication challenge. # This ensures a specific level of authentication assurance. required: { acr: 'gold' } }, - # The `params` field defines request query parameters sent to the identity provider's authorization endpoint. - # Example: The `claims` parameter is added to the authorization request. Its value is a JSON object, encoded and URL-escaped. - # Hence, the resulting URL will be https://identity.provider.example.com/api?claims=%7B%22id_token%22%3A%7B%22acr%22%3A%7B%22essential%22%3Atrue%2C%22values%22%3A%5B%22gold%22%5D%7D%7D%7D - # This instructs the identity provider to include an 'acr' claim with the value 'gold' in the ID token. + # The `params` field defines any additional parameters that are sent during the authentication process. + # In this example, the `claims` parameter is added to the authorization request and instructs the + # identity provider to include an 'acr' claim with the value 'gold' in the ID token. # The 'essential: true' indicates that this claim is required for successful authentication. params: { claims: { @@ -1496,21 +1499,13 @@ To enable step-up authentication for Admin Mode: } ``` - In the preceding example: - - - `enabled` specifies whether the step-up authentication for Admin Mode is enabled or not. - - `id_token` specifies the claims that need to be required in the ID token 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 `claims` parameter is set to request the `acr` value `gold`. - -1. Save the `gitlab.yml` configuration file. - -1. Restart GitLab for the changes to take effect. +1. Save the configuration file and restart GitLab for the changes to take effect. {{< alert type="note" >}} -Although OIDC is standardized, different Identity Providers (IdPs) might have unique requirements. The `params` setting allows a flexible hash to define necessary parameters for step-up authentication. These values vary based on the requirements of the IdP. +Although OIDC is standardized, different Identity Providers (IdPs) might have unique requirements. +The `params` setting allows a flexible hash to define necessary parameters for step-up authentication. +These values can vary based on the requirements for each IdP. {{< /alert >}} @@ -1522,14 +1517,14 @@ To configure step-up authentication for Admin Mode in GitLab using Keycloak: 1. [Configure Keycloak](#configure-keycloak) in GitLab. -1. [Create a browser login flow with step-up authentication in Keycloak](https://www.keycloak.org/docs/latest/server_admin/#_step-up-flow). - - The Keycloak documentation defines two values for level of authentication, `silver` and `gold`. - Use these values in the following configuration example. +1. Follow the steps in the Keycloak documentation to [create a browser login flow with step-up authentication in Keycloak](https://www.keycloak.org/docs/latest/server_admin/#_step-up-flow). 1. Edit your GitLab configuration file (`gitlab.yml` or `/etc/gitlab/gitlab.rb`) to enable step-up authentication in the Keycloak OIDC provider configuration. + Keycloak defines two different authentication levels: `silver` and `gold`. The following example + uses `gold` to represent the increased security level. + ```yaml production: &base omniauth: @@ -1539,7 +1534,7 @@ To configure step-up authentication for Admin Mode in GitLab using Keycloak: args: { name: 'openid_connect', # ... - allow_authorize_params: ["claims"] # <= These allowed authorize parameters should match the params used for the step-up mechanism + allow_authorize_params: ["claims"] # Match this to the parameters defined in `step_up_auth => admin_mode => params` }, step_up_auth: { admin_mode: { @@ -1559,9 +1554,7 @@ To configure step-up authentication for Admin Mode in GitLab using Keycloak: - In this example configuration file, the `acr` value is 'gold'. Follow the [Keycloak documentation on creating a browser login flow with step-up authentication](https://www.keycloak.org/docs/latest/server_admin/#_step-up-flow) that represents a higher security level. -1. Save the `gitlab.yml` file. - -1. Restart GitLab for the changes to take effect. +1. Save the configuration file and restart GitLab for the changes to take effect. ## Troubleshooting @@ -1582,4 +1575,4 @@ To configure step-up authentication for Admin Mode in GitLab using Keycloak: [`oauth2-server-php`](https://github.com/bshaffer/oauth2-server-php), you may have to [add a configuration parameter to Apache](https://github.com/bshaffer/oauth2-server-php/issues/926#issuecomment-387502778). -1. **Step-up authentication only**: 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 the authorization endpoint of the IdP. +1. **Step-up authentication only**: Ensure that any parameters defined in `step_up_auth => admin_mode => params` are also defined in `args => allow_authorize_params`. This includes the parameters in the request query parameters used to redirect to the IdP authorization endpoint. -- GitLab From c30177a0e81c1e106908854c66374504cb337bdf Mon Sep 17 00:00:00 2001 From: Gerardo Navarro Date: Mon, 24 Mar 2025 17:47:01 +0100 Subject: [PATCH 08/10] docs: Refined based on ideas from @idurham - Aligned example yaml config for Keycloak - Removed the config param `documentation_link` because it is not used in this MR --- doc/administration/auth/oidc.md | 48 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md index f6c42f715d4115..564955a428fe3f 100644 --- a/doc/administration/auth/oidc.md +++ b/doc/administration/auth/oidc.md @@ -1463,14 +1463,11 @@ To enable step-up authentication for Admin Mode: label: 'Provider name', args: { name: 'openid_connect', - ... + # ... + allow_authorize_params: ["claims"], # Match this to the parameters defined in `step_up_auth => admin_mode => params` }, step_up_auth: { admin_mode: { - # The `documentation_link` field is a URL to custom (internal) documentation that explains - # the identity provider configuration. Use this to provide guidance on how to set up - # and manage the identity provider for step-up authentication in your organization. - documentation_link: "", id_token: { # The `required` field declares which claims are required in the ID token with exact match. # In this example, the 'acr' (Authentication Context Class Reference) claim @@ -1530,30 +1527,28 @@ To configure step-up authentication for Admin Mode in GitLab using Keycloak: omniauth: providers: - { name: 'openid_connect', - label: 'Keycloak', - args: { - name: 'openid_connect', - # ... - allow_authorize_params: ["claims"] # Match this to the parameters defined in `step_up_auth => admin_mode => params` - }, - step_up_auth: { - admin_mode: { - documentation_link: "", - id_token: { - required: { - acr: 'gold' + label: 'Keycloak', + args: { + name: 'openid_connect', + # ... + allow_authorize_params: ["claims"] # Match this to the parameters defined in `step_up_auth => admin_mode => params` + }, + step_up_auth: { + admin_mode: { + id_token: { + # In this example, the 'acr' claim must have the value 'gold' that is also defined in the Keycloak documentation. + required: { + acr: 'gold' + } + }, + params: { + claims: { id_token: { acr: { essential: true, values: ['gold'] } } } } }, - params: { - claims: { id_token: { acr: { essential: true, values: ['gold'] } } } - } - }, + } } - } ``` - - In this example configuration file, the `acr` value is 'gold'. Follow the [Keycloak documentation on creating a browser login flow with step-up authentication](https://www.keycloak.org/docs/latest/server_admin/#_step-up-flow) that represents a higher security level. - 1. Save the configuration file and restart GitLab for the changes to take effect. ## Troubleshooting @@ -1575,4 +1570,7 @@ To configure step-up authentication for Admin Mode in GitLab using Keycloak: [`oauth2-server-php`](https://github.com/bshaffer/oauth2-server-php), you may have to [add a configuration parameter to Apache](https://github.com/bshaffer/oauth2-server-php/issues/926#issuecomment-387502778). -1. **Step-up authentication only**: Ensure that any parameters defined in `step_up_auth => admin_mode => params` are also defined in `args => allow_authorize_params`. This includes the parameters in the request query parameters used to redirect to the IdP authorization endpoint. +1. **Step-up authentication only**: Ensure that any parameters defined in + `step_up_auth => admin_mode => params` are also defined in `args => allow_authorize_params`. + This includes the parameters in the request query parameters used to + redirect to the IdP authorization endpoint. -- GitLab From e2ef7b82c000a388b5c5638ee93d1e37e8c8f125 Mon Sep 17 00:00:00 2001 From: Gerardo Navarro Date: Tue, 25 Mar 2025 18:38:04 +0100 Subject: [PATCH 09/10] docs: Apply suggestions from @idurham - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643#note_2414124428 - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643#note_2414124408 --- doc/administration/auth/oidc.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md index 564955a428fe3f..e9339a30ef8913 100644 --- a/doc/administration/auth/oidc.md +++ b/doc/administration/auth/oidc.md @@ -1437,6 +1437,14 @@ To configure a custom duration for your ID tokens: {{< /history >}} +{{< alert type="flag" >}} + +The availability of this feature is controlled by a feature flag. +For more information, see the history. +This feature is available for testing, but not ready for production use. + +{{< /alert >}} + In some cases, default authentication methods don't sufficiently protect critical resources or high-risk actions. Step-up authentication adds an extra authentication layer for privileged actions or sensitive operations, such as accessing the Admin area. @@ -1448,6 +1456,8 @@ as two-factor authentication (2FA), biometric authentication, or one-time passwo The OIDC standard includes authentication context class references (`ACR`). The `ACR` concept helps configure and implement step-up authentication for different scenarios, such as Admin Mode. +This feature is an [experiment](../../policy/development_stages_support.md) and subject to change without notice. This feature is not ready for production use. If you want to use this feature, you should test outside of production first. + ### Enable step-up authentication for Admin Mode To enable step-up authentication for Admin Mode: -- GitLab From b86994931311642401c98a3e088760b7b1f889b0 Mon Sep 17 00:00:00 2001 From: Gerardo Navarro Date: Wed, 2 Apr 2025 18:01:51 +0200 Subject: [PATCH 10/10] refactor: Apply suggestions from @atevans - Apply suggestion regarding documentation, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643#note_2429673443 - Apply suggestion regarding `with_indifferent_access`, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171643#note_2431969616 --- doc/administration/auth/oidc.md | 2 +- lib/gitlab/auth/oidc/step_up_authentication.rb | 2 +- spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md index e9339a30ef8913..2573d10441114a 100644 --- a/doc/administration/auth/oidc.md +++ b/doc/administration/auth/oidc.md @@ -1433,7 +1433,7 @@ To configure a custom duration for your ID tokens: {{< history >}} -- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/474650) in GitLab 17.11 [with a flag](../feature_flags.md) named `gitlab_com_derisk`. Disabled by default. +- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/474650) in GitLab 17.11 [with a flag](../feature_flags.md) named `omniauth_step_up_auth_for_admin_mode`. Disabled by default. {{< /history >}} diff --git a/lib/gitlab/auth/oidc/step_up_authentication.rb b/lib/gitlab/auth/oidc/step_up_authentication.rb index a59f6a405f78fe..c4c8e1f665a188 100644 --- a/lib/gitlab/auth/oidc/step_up_authentication.rb +++ b/lib/gitlab/auth/oidc/step_up_authentication.rb @@ -108,7 +108,7 @@ def required_conditions_fulfilled?(oauth_extra_metadata:, provider:, scope:) end def subset?(hash, subset_hash) - hash.with_indifferent_access >= subset_hash + hash.with_indifferent_access >= subset_hash.with_indifferent_access end end end diff --git a/spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb b/spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb index 89cef53f92acb8..c01df2518ef709 100644 --- a/spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb +++ b/spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb @@ -81,6 +81,8 @@ # -- Avoid formatting to ensure one-line table syntax where(:required_id_token_claims, :auth_hash_openid_connect_extra_raw_info, :expected_result) do { claim_1: 'gold' } | { claim_1: 'gold' } | true + { 'claim_1' => 'gold' } | { claim_1: 'gold' } | true + { claim_1: 'gold' } | { 'claim_1' => 'gold' } | true { claim_1: 'gold' } | { claim_1: 'gold', claim_3: 'other_value' } | true { claim_1: 'gold' } | { claim_1: 'silver' } | false { claim_1: 'gold' } | { claim_1: ['gold'] } | false -- GitLab