File zammad-skip-omniauth-initializer.patch of Package zammad

From 37ebcb7928f8df471400321b1330de0dab61d189 Mon Sep 17 00:00:00 2001
From: Andreas Schneider <asn@cryptomilk.org>
Date: Tue, 24 Feb 2026 09:52:01 +0100
Subject: [PATCH] Make OmniAuth provider plugins optional.

This change allows selective installation of OmniAuth provider gems.

Provider gems are now marked with 'require: false' in the Gemfile and
only loaded when needed. The initializer checks if each provider
strategy class is defined before registering it.
---
 .../concerns/handles_oidc_authorization.rb    |   3 +
 app/controllers/sessions_controller.rb        |   6 +-
 app/controllers/settings_controller.rb        |   6 +-
 .../useThirdPartyAuthentication.ts            |  27 ++-
 app/frontend/shared/types/config.ts           |   1 +
 .../logout/handles_oidc_authorization.rb      |   2 +
 app/graphql/gql/mutations/logout.rb           |   2 +
 app/graphql/gql/queries/application_config.rb |   6 +-
 config/application.rb                         |   1 +
 config/initializers/omniauth.rb               |  74 ++++--
 .../initializers/omniauth_openid_connect.rb   |  48 ++--
 lib/omni_auth/provider_availability.rb        |  45 ++++
 lib/omni_auth/strategies/facebook_database.rb |  24 +-
 lib/omni_auth/strategies/github_database.rb   |  24 +-
 lib/omni_auth/strategies/gitlab_database.rb   |  26 ++-
 .../strategies/google_oauth2_database.rb      |  24 +-
 .../strategies/linked_in_database.rb          |  34 +--
 .../microsoft_office365_database.rb           |  30 ++-
 lib/omni_auth/strategies/oidc_database.rb     |  68 +++---
 lib/omni_auth/strategies/saml_database.rb     | 211 +++++++++---------
 lib/omni_auth/strategies/twitter_database.rb  |  24 +-
 lib/omni_auth/strategies/weibo_database.rb    |  24 +-
 23 files changed, 448 insertions(+), 282 deletions(-)
 create mode 100644 lib/omni_auth/provider_availability.rb

diff --git a/app/controllers/concerns/handles_oidc_authorization.rb b/app/controllers/concerns/handles_oidc_authorization.rb
index cabd017cca..ab9485a812 100644
--- a/app/controllers/concerns/handles_oidc_authorization.rb
+++ b/app/controllers/concerns/handles_oidc_authorization.rb
@@ -7,6 +7,7 @@ module HandlesOidcAuthorization
     skip_before_action :verify_csrf_token, only: %i[oidc_destroy oidc_bc_logout] # rubocop:disable Rails/LexicallyScopedActionFilter
 
     def oidc_bc_logout
+      raise Exceptions::UnprocessableEntity, __("The required parameter 'oidc_database strategy' is not available.") if !defined?(OmniAuth::Strategies::OidcDatabase)
       raise Exceptions::UnprocessableEntity, __("The required parameter 'logout_token' is missing.") if params[:logout_token].blank?
 
       begin
@@ -24,6 +25,8 @@ module HandlesOidcAuthorization
     private
 
     def oidc_session?
+      return false if !defined?(OmniAuth::Strategies::OidcDatabase)
+
       session[:oidc_id_token].present? && oidc_end_session_endpoint.present?
     end
 
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 4916edaf73..37f2ebed14 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -316,8 +316,8 @@ class SessionsController < ApplicationController
     #   This is needed because the setting is not frontend related,
     #   but we still to display one of the options
     # https://github.com/zammad/zammad/issues/4263
-    config['auth_saml_display_name'] = Setting.get('auth_saml_credentials')[:display_name]
-    config['auth_openid_connect_display_name'] = Setting.get('auth_openid_connect_credentials')[:display_name]
+    config['auth_saml_display_name'] = Setting.get('auth_saml_credentials')&.[](:display_name)
+    config['auth_openid_connect_display_name'] = Setting.get('auth_openid_connect_credentials')&.[](:display_name)
 
     # Include the flag for JSON column type support (currently only on PostgreSQL backend).
     config['column_type_json_supported'] =
@@ -343,6 +343,8 @@ class SessionsController < ApplicationController
   end
 
   def saml_session?
+    return false if !defined?(OmniAuth::Strategies::SamlDatabase)
+
     (session['saml_uid'] || session['saml_session_index']) && OmniAuth::Strategies::SamlDatabase.setup.fetch('idp_slo_service_url', nil)
   end
 
diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb
index 3ef995c46f..6f9162d47f 100644
--- a/app/controllers/settings_controller.rb
+++ b/app/controllers/settings_controller.rb
@@ -5,11 +5,15 @@ class SettingsController < ApplicationController
 
   # GET /settings
   def index
+    unavailable = OmniAuth::ProviderAvailability.unavailable_settings
+
     list = []
     Setting.all.each do |setting|
       next if !authorized?(setting, :show?)
 
-      list.push setting
+      s = setting.as_json
+      s['preferences'] = (s['preferences'] || {}).merge('disabled' => true) if unavailable.include?(setting.name)
+      list.push s
     end
     render json: list, status: :ok
   end
diff --git a/app/frontend/shared/composables/authentication/useThirdPartyAuthentication.ts b/app/frontend/shared/composables/authentication/useThirdPartyAuthentication.ts
index b6cb4d71f8..558fae8446 100644
--- a/app/frontend/shared/composables/authentication/useThirdPartyAuthentication.ts
+++ b/app/frontend/shared/composables/authentication/useThirdPartyAuthentication.ts
@@ -12,61 +12,67 @@ export const useThirdPartyAuthentication = () => {
   const application = useApplicationStore()
   const { config } = storeToRefs(application)
 
+  const availableProviders = computed(
+    () => config.value.omniauth_available_providers ?? [],
+  )
+
+  const available = (provider: string) => availableProviders.value.includes(provider)
+
   const providers = computed<ThirdPartyAuthProvider[]>(() => {
     return [
       {
         name: EnumAuthenticationProvider.Facebook,
         label: i18n.t('Facebook'),
-        enabled: !!config.value.auth_facebook,
+        enabled: !!config.value.auth_facebook && available('facebook'),
         icon: 'facebook',
         url: '/auth/facebook',
       },
       {
         name: EnumAuthenticationProvider.Twitter,
         label: i18n.t('Twitter'),
-        enabled: !!config.value.auth_twitter,
+        enabled: !!config.value.auth_twitter && available('twitter'),
         icon: 'twitter',
         url: '/auth/twitter',
       },
       {
         name: EnumAuthenticationProvider.Linkedin,
         label: i18n.t('LinkedIn'),
-        enabled: !!config.value.auth_linkedin,
+        enabled: !!config.value.auth_linkedin && available('linkedin'),
         icon: 'linkedin',
         url: '/auth/linkedin',
       },
       {
         name: EnumAuthenticationProvider.Github,
         label: i18n.t('GitHub'),
-        enabled: !!config.value.auth_github,
+        enabled: !!config.value.auth_github && available('github'),
         icon: 'github',
         url: '/auth/github',
       },
       {
         name: EnumAuthenticationProvider.Gitlab,
         label: i18n.t('GitLab'),
-        enabled: !!config.value.auth_gitlab,
+        enabled: !!config.value.auth_gitlab && available('gitlab'),
         icon: 'gitlab',
         url: '/auth/gitlab',
       },
       {
         name: EnumAuthenticationProvider.MicrosoftOffice365,
         label: i18n.t('Microsoft'),
-        enabled: !!config.value.auth_microsoft_office365,
+        enabled: !!config.value.auth_microsoft_office365 && available('microsoft_office365'),
         icon: 'microsoft',
         url: '/auth/microsoft_office365',
       },
       {
         name: EnumAuthenticationProvider.GoogleOauth2,
         label: i18n.t('Google'),
-        enabled: !!config.value.auth_google_oauth2,
+        enabled: !!config.value.auth_google_oauth2 && available('google_oauth2'),
         icon: 'google',
         url: '/auth/google_oauth2',
       },
       {
         name: EnumAuthenticationProvider.Weibo,
         label: i18n.t('Weibo'),
-        enabled: !!config.value.auth_weibo,
+        enabled: !!config.value.auth_weibo && available('weibo'),
         icon: 'weibo',
         url: '/auth/weibo',
       },
@@ -75,11 +81,12 @@ export const useThirdPartyAuthentication = () => {
         label:
           (config.value['auth_saml_credentials.display_name'] as string) ||
           i18n.t('SAML'),
-        enabled: !!config.value.auth_saml,
+        enabled: !!config.value.auth_saml && available('saml'),
         icon: 'saml',
         url: '/auth/saml',
       },
       {
+        // SSO uses HTTP headers, has no external gem dependency, and is always available when enabled.
         name: EnumAuthenticationProvider.Sso,
         label: i18n.t('SSO'),
         enabled: !!config.value.auth_sso,
@@ -92,7 +99,7 @@ export const useThirdPartyAuthentication = () => {
           (config.value[
             'auth_openid_connect_credentials.display_name'
           ] as string) || i18n.t('OpenID Connect'),
-        enabled: !!config.value.auth_openid_connect,
+        enabled: !!config.value.auth_openid_connect && available('openid_connect'),
         icon: 'openid-connect',
         url: '/auth/openid_connect',
       },
diff --git a/app/frontend/shared/types/config.ts b/app/frontend/shared/types/config.ts
index d912b065a1..c8d74cae55 100644
--- a/app/frontend/shared/types/config.ts
+++ b/app/frontend/shared/types/config.ts
@@ -43,6 +43,7 @@ export interface ConfigList {
   maintenance_login: boolean
   maintenance_login_message: string
   maintenance_mode: boolean
+  omniauth_available_providers?: string[]
   organization: string
   password_max_login_failed?: 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | null
   pgp_config: unknown
diff --git a/app/graphql/gql/mutations/concerns/logout/handles_oidc_authorization.rb b/app/graphql/gql/mutations/concerns/logout/handles_oidc_authorization.rb
index 997d72509a..fde1cecf8f 100644
--- a/app/graphql/gql/mutations/concerns/logout/handles_oidc_authorization.rb
+++ b/app/graphql/gql/mutations/concerns/logout/handles_oidc_authorization.rb
@@ -5,6 +5,8 @@ module Gql::Mutations::Concerns::Logout::HandlesOidcAuthorization
 
   included do
     def oidc_session?
+      return false if !defined?(OmniAuth::Strategies::OidcDatabase)
+
       session[:oidc_id_token].present? && oidc_end_session_endpoint.present?
     end
 
diff --git a/app/graphql/gql/mutations/logout.rb b/app/graphql/gql/mutations/logout.rb
index 441e7d5b30..1f33f353a7 100644
--- a/app/graphql/gql/mutations/logout.rb
+++ b/app/graphql/gql/mutations/logout.rb
@@ -38,6 +38,8 @@ module Gql::Mutations
     end
 
     def saml_session?
+      return false if !defined?(OmniAuth::Strategies::SamlDatabase)
+
       (session['saml_uid'] || session['saml_session_index']) && OmniAuth::Strategies::SamlDatabase.setup.fetch('idp_slo_service_url', nil)
     end
 
diff --git a/app/graphql/gql/queries/application_config.rb b/app/graphql/gql/queries/application_config.rb
index eb9d707fa6..5c8a3870e4 100644
--- a/app/graphql/gql/queries/application_config.rb
+++ b/app/graphql/gql/queries/application_config.rb
@@ -47,7 +47,7 @@ module Gql::Queries
     end
 
     def custom_settings
-      [
+      result = [
         'auth_saml_credentials.display_name',
         'auth_openid_connect_credentials.display_name',
       ].filter_map do |config_name|
@@ -59,6 +59,10 @@ module Gql::Queries
 
         { key: config_name, value: value }
       end
+
+      # Intentionally included for unauthenticated users: the login page needs
+      # this to show only providers whose gem is installed.
+      result << { key: 'omniauth_available_providers', value: OmniAuth::ProviderAvailability.available_providers }
     end
   end
 end
diff --git a/config/application.rb b/config/application.rb
index d6756a2fdc..29c99ae90a 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -25,6 +25,7 @@ module Zammad
     Rails.autoloaders.each do |autoloader|
       autoloader.ignore            "#{config.root}/app/frontend"
       autoloader.do_not_eager_load "#{config.root}/lib/core_ext"
+      autoloader.ignore            "#{config.root}/lib/omni_auth/strategies"
       autoloader.collapse          "#{config.root}/lib/omniauth"
       autoloader.collapse          "#{config.root}/lib/generators"
       autoloader.inflector.inflect(
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 208b7f090d..45a1e753e8 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -1,47 +1,77 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
+# Explicitly load these files rather than relying on Zeitwerk autoloading.
+# OmniAuth is a cuckoo namespace (defined by the omniauth gem), so Zeitwerk
+# cannot set up autoloads under it reliably. Strategy files are also in the
+# Zeitwerk ignore list. Each file rescues LoadError when its provider gem is absent.
+require Rails.root.join('lib/omni_auth/provider_availability')
+Rails.root.glob('lib/omni_auth/strategies/*.rb').each { |f| require f }
+
+OmniAuth::ProviderAvailability.load!
+
 Rails.application.config.middleware.use OmniAuth::Builder do
 
   # twitter database connect
-  provider :twitter_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', {
-    client_options: {
-      authorize_path: '/oauth/authorize',
-      site:           'https://api.twitter.com',
+  if OmniAuth::ProviderAvailability.available?('twitter')
+    provider :twitter_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', {
+      client_options: {
+        authorize_path: '/oauth/authorize',
+        site:           'https://api.twitter.com',
+      }
     }
-  }
+  end
 
   # facebook database connect
-  provider :facebook_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
+  if OmniAuth::ProviderAvailability.available?('facebook')
+    provider :facebook_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
+  end
 
   # linkedin database connect
-  provider :linked_in_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
+  if OmniAuth::ProviderAvailability.available?('linkedin')
+    provider :linked_in_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
+  end
 
   # google database connect
-  provider :google_oauth2_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', access_type: 'online', prompt: ''
+  if OmniAuth::ProviderAvailability.available?('google_oauth2')
+    provider :google_oauth2_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', access_type: 'online', prompt: ''
+  end
 
   # github database connect
-  provider :github_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
+  if OmniAuth::ProviderAvailability.available?('github')
+    provider :github_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
+  end
 
   # gitlab database connect
-  provider :git_lab_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', {
-    client_options: {
-      site:          'https://not_change_will_be_set_by_database',
-      authorize_url: '/oauth/authorize',
-      token_url:     '/oauth/token'
-    },
-    scope:          'read_user',
-  }
+  if OmniAuth::ProviderAvailability.available?('gitlab')
+    provider :git_lab_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', {
+      client_options: {
+        site:          'https://not_change_will_be_set_by_database',
+        authorize_url: '/oauth/authorize',
+        token_url:     '/oauth/token'
+      },
+      scope:          'read_user',
+    }
+  end
 
   # microsoft_office365 database connect
-  provider :microsoft_office365_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
+  if OmniAuth::ProviderAvailability.available?('microsoft_office365')
+    provider :microsoft_office365_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
+  end
 
   # weibo database connect
-  provider :weibo_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
+  if OmniAuth::ProviderAvailability.available?('weibo')
+    provider :weibo_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
+  end
 
-  # SAML database connect
-  provider :saml_database
+  # saml database connect
+  if OmniAuth::ProviderAvailability.available?('saml')
+    provider :saml_database
+  end
 
-  provider :oidc_database
+  # openid_connect database connect
+  if OmniAuth::ProviderAvailability.available?('openid_connect')
+    provider :oidc_database
+  end
 end
 
 # This fixes issue #1642 and is required for setups in which Zammad is used
diff --git a/config/initializers/omniauth_openid_connect.rb b/config/initializers/omniauth_openid_connect.rb
index 3d87376a5b..daa45464b1 100644
--- a/config/initializers/omniauth_openid_connect.rb
+++ b/config/initializers/omniauth_openid_connect.rb
@@ -1,34 +1,38 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
-require 'omniauth/openid_connect'
+begin
+  require 'omniauth/openid_connect'
 
-# Monkey patch to support more different token endpoints. Can be removed when this PR is merged:
-# https://github.com/omniauth/omniauth_openid_connect/pull/192
-module OmniAuth
-  module Strategies
-    class OpenIDConnect
-      def access_token
-        return @access_token if @access_token
+  # Monkey patch to support more different token endpoints. Can be removed when this PR is merged:
+  # https://github.com/omniauth/omniauth_openid_connect/pull/192
+  module OmniAuth
+    module Strategies
+      class OpenIDConnect
+        def access_token
+          return @access_token if @access_token
 
-        token_request_params = {
-          scope:              (options.scope if options.send_scope_to_token_endpoint),
-          client_auth_method: options.client_auth_method,
-        }
+          token_request_params = {
+            scope:              (options.scope if options.send_scope_to_token_endpoint),
+            client_auth_method: options.client_auth_method,
+          }
 
-        token_request_params[:code_verifier] = params['code_verifier'] || session.delete('omniauth.pkce.verifier') if options.pkce
+          token_request_params[:code_verifier] = params['code_verifier'] || session.delete('omniauth.pkce.verifier') if options.pkce
 
-        if configured_response_type == 'code'
-          token_request_params[:grant_type] = :authorization_code
-          token_request_params[:code] = authorization_code
-          token_request_params[:redirect_uri] = redirect_uri
-          token_request_params[:client_id] = client_options.identifier
-        end
+          if configured_response_type == 'code'
+            token_request_params[:grant_type] = :authorization_code
+            token_request_params[:code] = authorization_code
+            token_request_params[:redirect_uri] = redirect_uri
+            token_request_params[:client_id] = client_options.identifier
+          end
 
-        @access_token = client.access_token!(token_request_params)
-        verify_id_token!(@access_token.id_token) if configured_response_type == 'code'
+          @access_token = client.access_token!(token_request_params)
+          verify_id_token!(@access_token.id_token) if configured_response_type == 'code'
 
-        @access_token
+          @access_token
+        end
       end
     end
   end
+rescue LoadError
+  # Gem not installed, skip monkey patch
 end
diff --git a/lib/omni_auth/provider_availability.rb b/lib/omni_auth/provider_availability.rb
new file mode 100644
index 0000000000..41889ff815
--- /dev/null
+++ b/lib/omni_auth/provider_availability.rb
@@ -0,0 +1,45 @@
+# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
+
+module OmniAuth
+  module ProviderAvailability
+    # Map from auth setting name suffix to strategy class name.
+    # Used to determine which providers have their gem installed.
+    # NOTE: Keep this in sync with the strategy files in lib/omni_auth/strategies/.
+    # Any strategy file added there must also be listed here.
+    PROVIDER_STRATEGIES = {
+      'twitter'             => 'OmniAuth::Strategies::TwitterDatabase',
+      'facebook'            => 'OmniAuth::Strategies::FacebookDatabase',
+      'linkedin'            => 'OmniAuth::Strategies::LinkedInDatabase',
+      'google_oauth2'       => 'OmniAuth::Strategies::GoogleOauth2Database',
+      'github'              => 'OmniAuth::Strategies::GithubDatabase',
+      'gitlab'              => 'OmniAuth::Strategies::GitLabDatabase',
+      'microsoft_office365' => 'OmniAuth::Strategies::MicrosoftOffice365Database',
+      'weibo'               => 'OmniAuth::Strategies::WeiboDatabase',
+      'saml'                => 'OmniAuth::Strategies::SamlDatabase',
+      'openid_connect'      => 'OmniAuth::Strategies::OidcDatabase',
+    }.freeze
+
+    class << self
+      attr_reader :available_providers
+    end
+
+    # Populate available_providers based on which strategy classes are defined.
+    # Called from the omniauth initializer after strategy files are required.
+    def self.load!
+      @available_providers = PROVIDER_STRATEGIES.filter_map do |name, klass|
+        name if Object.const_defined?(klass)
+      end.freeze
+    end
+
+    def self.available?(provider)
+      raise 'OmniAuth::ProviderAvailability.load! has not been called' if @available_providers.nil?
+
+      @available_providers.include?(provider)
+    end
+
+    def self.unavailable_settings
+      unavailable_providers = PROVIDER_STRATEGIES.keys - available_providers
+      unavailable_providers.flat_map { |p| ["auth_#{p}", "auth_#{p}_credentials"] }.to_set
+    end
+  end
+end
diff --git a/lib/omni_auth/strategies/facebook_database.rb b/lib/omni_auth/strategies/facebook_database.rb
index 4c95e3f037..cd504f73fc 100644
--- a/lib/omni_auth/strategies/facebook_database.rb
+++ b/lib/omni_auth/strategies/facebook_database.rb
@@ -1,15 +1,21 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
-class OmniAuth::Strategies::FacebookDatabase < OmniAuth::Strategies::Facebook
-  option :name, 'facebook'
+begin
+  require 'omniauth-facebook'
 
-  def initialize(app, *args, &)
+  class OmniAuth::Strategies::FacebookDatabase < OmniAuth::Strategies::Facebook
+    option :name, 'facebook'
 
-    # database lookup
-    config  = Setting.get('auth_facebook_credentials') || {}
-    args[0] = config['app_id']
-    args[1] = config['app_secret']
-    super
-  end
+    def initialize(app, *args, &)
+
+      # database lookup
+      config  = Setting.get('auth_facebook_credentials') || {}
+      args[0] = config['app_id']
+      args[1] = config['app_secret']
+      super
+    end
 
+  end
+rescue LoadError
+  # Gem not installed, strategy not available.
 end
diff --git a/lib/omni_auth/strategies/github_database.rb b/lib/omni_auth/strategies/github_database.rb
index 9cb763226f..1a7fdc9e36 100644
--- a/lib/omni_auth/strategies/github_database.rb
+++ b/lib/omni_auth/strategies/github_database.rb
@@ -1,15 +1,21 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
-class OmniAuth::Strategies::GithubDatabase < OmniAuth::Strategies::GitHub
-  option :name, 'github'
+begin
+  require 'omniauth-github'
 
-  def initialize(app, *args, &)
+  class OmniAuth::Strategies::GithubDatabase < OmniAuth::Strategies::GitHub
+    option :name, 'github'
 
-    # database lookup
-    config  = Setting.get('auth_github_credentials') || {}
-    args[0] = config['app_id']
-    args[1] = config['app_secret']
-    super
-  end
+    def initialize(app, *args, &)
+
+      # database lookup
+      config  = Setting.get('auth_github_credentials') || {}
+      args[0] = config['app_id']
+      args[1] = config['app_secret']
+      super
+    end
 
+  end
+rescue LoadError
+  # Gem not installed, strategy not available.
 end
diff --git a/lib/omni_auth/strategies/gitlab_database.rb b/lib/omni_auth/strategies/gitlab_database.rb
index b8f30da6da..f69383a8c6 100644
--- a/lib/omni_auth/strategies/gitlab_database.rb
+++ b/lib/omni_auth/strategies/gitlab_database.rb
@@ -1,16 +1,22 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
-class OmniAuth::Strategies::GitLabDatabase < OmniAuth::Strategies::GitLab
-  option :name, 'gitlab'
+begin
+  require 'omniauth-gitlab'
 
-  def initialize(app, *args, &)
+  class OmniAuth::Strategies::GitLabDatabase < OmniAuth::Strategies::GitLab
+    option :name, 'gitlab'
 
-    # database lookup
-    config  = Setting.get('auth_gitlab_credentials') || {}
-    args[0] = config['app_id']
-    args[1] = config['app_secret']
-    args[2][:client_options] = args[2][:client_options].merge(config.symbolize_keys)
-    super
-  end
+    def initialize(app, *args, &)
+
+      # database lookup
+      config  = Setting.get('auth_gitlab_credentials') || {}
+      args[0] = config['app_id']
+      args[1] = config['app_secret']
+      args[2][:client_options] = args[2][:client_options].merge(config.symbolize_keys)
+      super
+    end
 
+  end
+rescue LoadError
+  # Gem not installed, strategy not available.
 end
diff --git a/lib/omni_auth/strategies/google_oauth2_database.rb b/lib/omni_auth/strategies/google_oauth2_database.rb
index 5746d9fcf4..6a65677d5f 100644
--- a/lib/omni_auth/strategies/google_oauth2_database.rb
+++ b/lib/omni_auth/strategies/google_oauth2_database.rb
@@ -1,15 +1,21 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
-class OmniAuth::Strategies::GoogleOauth2Database < OmniAuth::Strategies::GoogleOauth2
-  option :name, 'google_oauth2'
+begin
+  require 'omniauth-google-oauth2'
 
-  def initialize(app, *args, &)
+  class OmniAuth::Strategies::GoogleOauth2Database < OmniAuth::Strategies::GoogleOauth2
+    option :name, 'google_oauth2'
 
-    # database lookup
-    config  = Setting.get('auth_google_oauth2_credentials') || {}
-    args[0] = config['client_id']
-    args[1] = config['client_secret']
-    super
-  end
+    def initialize(app, *args, &)
+
+      # database lookup
+      config  = Setting.get('auth_google_oauth2_credentials') || {}
+      args[0] = config['client_id']
+      args[1] = config['client_secret']
+      super
+    end
 
+  end
+rescue LoadError
+  # Gem not installed, strategy not available.
 end
diff --git a/lib/omni_auth/strategies/linked_in_database.rb b/lib/omni_auth/strategies/linked_in_database.rb
index 116c1f1242..b98f41529d 100644
--- a/lib/omni_auth/strategies/linked_in_database.rb
+++ b/lib/omni_auth/strategies/linked_in_database.rb
@@ -1,22 +1,28 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
-class OmniAuth::Strategies::LinkedInDatabase < OmniAuth::Strategies::LinkedIn
-  option :name, 'linkedin'
+begin
+  require 'omniauth-linkedin-oauth2'
 
-  def initialize(app, *args, &)
+  class OmniAuth::Strategies::LinkedInDatabase < OmniAuth::Strategies::LinkedIn
+    option :name, 'linkedin'
 
-    # database lookup
-    config  = Setting.get('auth_linkedin_credentials') || {}
-    args[0] = config['app_id']
-    args[1] = config['app_secret']
-    super
-  end
+    def initialize(app, *args, &)
+
+      # database lookup
+      config  = Setting.get('auth_linkedin_credentials') || {}
+      args[0] = config['app_id']
+      args[1] = config['app_secret']
+      super
+    end
 
-  # Workaround from current omniauth-linkedin gem issue:
-  # https://github.com/decioferreira/omniauth-linkedin-oauth2/issues/68
-  def token_params
-    super.tap do |params|
-      params.client_secret = options.client_secret
+    # Workaround from current omniauth-linkedin gem issue:
+    # https://github.com/decioferreira/omniauth-linkedin-oauth2/issues/68
+    def token_params
+      super.tap do |params|
+        params.client_secret = options.client_secret
+      end
     end
   end
+rescue LoadError
+  # Gem not installed, strategy not available.
 end
diff --git a/lib/omni_auth/strategies/microsoft_office365_database.rb b/lib/omni_auth/strategies/microsoft_office365_database.rb
index 6a4222ac9a..e0589b8d36 100644
--- a/lib/omni_auth/strategies/microsoft_office365_database.rb
+++ b/lib/omni_auth/strategies/microsoft_office365_database.rb
@@ -1,20 +1,26 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
-class OmniAuth::Strategies::MicrosoftOffice365Database < OmniAuth::Strategies::MicrosoftOffice365
-  option :name, 'microsoft_office365'
+begin
+  require 'omniauth-microsoft-office365'
 
-  def initialize(app, *args, &)
+  class OmniAuth::Strategies::MicrosoftOffice365Database < OmniAuth::Strategies::MicrosoftOffice365
+    option :name, 'microsoft_office365'
 
-    # database lookup
-    config  = Setting.get('auth_microsoft_office365_credentials') || {}
-    args[0] = config['app_id']
-    args[1] = config['app_secret']
-    tenant  = config['app_tenant'].presence || 'common'
+    def initialize(app, *args, &)
 
-    super
+      # database lookup
+      config  = Setting.get('auth_microsoft_office365_credentials') || {}
+      args[0] = config['app_id']
+      args[1] = config['app_secret']
+      tenant  = config['app_tenant'].presence || 'common'
 
-    @options[:client_options][:authorize_url] = "/#{tenant}/oauth2/v2.0/authorize"
-    @options[:client_options][:token_url]     = "/#{tenant}/oauth2/v2.0/token"
-  end
+      super
+
+      @options[:client_options][:authorize_url] = "/#{tenant}/oauth2/v2.0/authorize"
+      @options[:client_options][:token_url]     = "/#{tenant}/oauth2/v2.0/token"
+    end
 
+  end
+rescue LoadError
+  # Gem not installed, strategy not available.
 end
diff --git a/lib/omni_auth/strategies/oidc_database.rb b/lib/omni_auth/strategies/oidc_database.rb
index 69e0109e28..313747e9cc 100644
--- a/lib/omni_auth/strategies/oidc_database.rb
+++ b/lib/omni_auth/strategies/oidc_database.rb
@@ -1,46 +1,52 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
-class OmniAuth::Strategies::OidcDatabase < OmniAuth::Strategies::OpenIDConnect
-  option :name, 'openid_connect'
+begin
+  require 'omniauth_openid_connect'
 
-  def self.setup
-    auth_openid_connect_credentials = Setting.get('auth_openid_connect_credentials') || {}
+  class OmniAuth::Strategies::OidcDatabase < OmniAuth::Strategies::OpenIDConnect
+    option :name, 'openid_connect'
 
-    http_type = Setting.get('http_type')
-    fqdn      = Setting.get('fqdn')
+    def self.setup
+      auth_openid_connect_credentials = Setting.get('auth_openid_connect_credentials') || {}
 
-    client_options = {
-      identifier:   auth_openid_connect_credentials['identifier'],
-      redirect_uri: "#{http_type}://#{fqdn}/auth/openid_connect/callback",
-    }
+      http_type = Setting.get('http_type')
+      fqdn      = Setting.get('fqdn')
 
-    auth_openid_connect_credentials['scope'] = %i[openid email profile] if auth_openid_connect_credentials['scope'].blank?
-    auth_openid_connect_credentials['scope'] = auth_openid_connect_credentials['scope'].split.map(&:to_sym) if auth_openid_connect_credentials['scope'].is_a?(String)
+      client_options = {
+        identifier:   auth_openid_connect_credentials['identifier'],
+        redirect_uri: "#{http_type}://#{fqdn}/auth/openid_connect/callback",
+      }
 
-    auth_openid_connect_credentials.compact_blank.merge(
-      discovery:      true,
-      response_type:  :code,
-      pkce:           ActiveModel::Type::Boolean.new.cast(auth_openid_connect_credentials['pkce']),
-      client_options:,
-    )
-  end
+      auth_openid_connect_credentials['scope'] = %i[openid email profile] if auth_openid_connect_credentials['scope'].blank?
+      auth_openid_connect_credentials['scope'] = auth_openid_connect_credentials['scope'].split.map(&:to_sym) if auth_openid_connect_credentials['scope'].is_a?(String)
 
-  def self.destroy_session(env, session)
-    session.delete('oidc_id_token')
+      auth_openid_connect_credentials.compact_blank.merge(
+        discovery:      true,
+        response_type:  :code,
+        pkce:           ActiveModel::Type::Boolean.new.cast(auth_openid_connect_credentials['pkce']),
+        client_options:,
+      )
+    end
 
-    @_current_user = nil
-    env['rack.session.options'][:expire_after] = nil
+    def self.destroy_session(env, session)
+      session.delete('oidc_id_token')
 
-    session.destroy
-  end
+      @_current_user = nil
+      env['rack.session.options'][:expire_after] = nil
 
-  def initialize(app, *args, &)
-    args[0] = self.class.setup
+      session.destroy
+    end
 
-    super
-  end
+    def initialize(app, *args, &)
+      args[0] = self.class.setup
+
+      super
+    end
 
-  def decode_logout_token(logout_token)
-    decode_id_token(logout_token)
+    def decode_logout_token(logout_token)
+      decode_id_token(logout_token)
+    end
   end
+rescue LoadError
+  # Gem not installed, strategy not available.
 end
diff --git a/lib/omni_auth/strategies/saml_database.rb b/lib/omni_auth/strategies/saml_database.rb
index 0a8c151923..c514248fac 100644
--- a/lib/omni_auth/strategies/saml_database.rb
+++ b/lib/omni_auth/strategies/saml_database.rb
@@ -1,138 +1,145 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
-class OmniAuth::Strategies::SamlDatabase < OmniAuth::Strategies::SAML
-  option :name, 'saml'
+begin
+  require 'omniauth-saml'
 
-  def self.setup
-    auth_saml_credentials = Setting.get('auth_saml_credentials') || {}
+  class OmniAuth::Strategies::SamlDatabase < OmniAuth::Strategies::SAML
+    option :name, 'saml'
 
-    http_type = Setting.get('http_type')
-    fqdn      = Setting.get('fqdn')
+    def self.setup
+      auth_saml_credentials = Setting.get('auth_saml_credentials') || {}
 
-    # Use meta URL as entity id/issues as it is best practice.
-    # See: https://community.zammad.org/t/saml-oidc-third-party-authentication/2533/13
-    entity_id                      = "#{http_type}://#{fqdn}/auth/saml/metadata"
-    assertion_consumer_service_url = "#{http_type}://#{fqdn}/auth/saml/callback"
-    single_logout_service_url      = "#{http_type}://#{fqdn}/auth/saml/slo"
+      http_type = Setting.get('http_type')
+      fqdn      = Setting.get('fqdn')
 
-    config = auth_saml_credentials.compact_blank
-      .merge(
-        assertion_consumer_service_url: assertion_consumer_service_url,
-        sp_entity_id:                   entity_id,
-        single_logout_service_url:      single_logout_service_url,
-        idp_slo_session_destroy:        proc { |env, session| destroy_session(env, session) },
-      )
+      # Use meta URL as entity id/issues as it is best practice.
+      # See: https://community.zammad.org/t/saml-oidc-third-party-authentication/2533/13
+      entity_id                      = "#{http_type}://#{fqdn}/auth/saml/metadata"
+      assertion_consumer_service_url = "#{http_type}://#{fqdn}/auth/saml/callback"
+      single_logout_service_url      = "#{http_type}://#{fqdn}/auth/saml/slo"
 
-    apply_security_settings(config)
+      config = auth_saml_credentials.compact_blank
+        .merge(
+          assertion_consumer_service_url: assertion_consumer_service_url,
+          attribute_service_name:         Setting.get('product_name'),
+          sp_entity_id:                   entity_id,
+          single_logout_service_url:      single_logout_service_url,
+          idp_slo_session_destroy:        proc { |env, session| destroy_session(env, session) },
+        )
 
-    config
-  end
-
-  def self.destroy_session(env, session)
-    session.delete('saml_uid')
-    session.delete('saml_transaction_id')
-    session.delete('saml_session_index')
+      apply_security_settings(config)
 
-    @_current_user = nil
-    env['rack.session.options'][:expire_after] = nil
+      config
+    end
 
-    session.destroy
-  end
+    def self.destroy_session(env, session)
+      session.delete('saml_uid')
+      session.delete('saml_transaction_id')
+      session.delete('saml_session_index')
 
-  def initialize(app, *args, &)
-    args[0] = self.class.setup
+      @_current_user = nil
+      env['rack.session.options'][:expire_after] = nil
 
-    super
-  end
+      session.destroy
+    end
 
-  def self.apply_security_settings(settings)
-    security           = settings.delete(:security)           || {}
-    private_key        = settings.delete(:private_key)        || ''
-    private_key_secret = settings.delete(:private_key_secret) || ''
-    certificate        = settings.delete(:certificate)        || ''
+    def initialize(app, *args, &)
+      args[0] = self.class.setup
 
-    return if !check_security_settings(settings, security, private_key, private_key_secret, certificate)
+      super
+    end
 
-    apply_security_default_settings(settings)
-    apply_sign_only_settings(settings, security)
-    apply_encrypt_only_settings(settings, security)
+    def self.apply_security_settings(settings)
+      security           = settings.delete(:security)           || {}
+      private_key        = settings.delete(:private_key)        || ''
+      private_key_secret = settings.delete(:private_key_secret) || ''
+      certificate        = settings.delete(:certificate)        || ''
 
-    true
-  end
+      return if !check_security_settings(settings, security, private_key, private_key_secret, certificate)
 
-  def self.check_security_settings(settings, security, private_key, private_key_secret, certificate)
-    return false if security.blank?    || security.eql?('off')
-    return false if private_key.blank? || certificate.blank?
+      apply_security_default_settings(settings)
+      apply_sign_only_settings(settings, security)
+      apply_encrypt_only_settings(settings, security)
 
-    begin
-      pkey = OpenSSL::PKey.read(private_key, private_key_secret)
-    rescue
-      return false
+      true
     end
 
-    settings[:private_key] = pkey.to_pem
-    settings[:certificate] = certificate
+    def self.check_security_settings(settings, security, private_key, private_key_secret, certificate)
+      return false if security.blank?    || security.eql?('off')
+      return false if private_key.blank? || certificate.blank?
 
-    true
-  end
+      begin
+        pkey = OpenSSL::PKey.read(private_key, private_key_secret)
+      rescue
+        return false
+      end
 
-  def self.apply_security_default_settings(settings)
-    settings[:security] = {
-      digest_method:             XMLSecurity::Document::SHA256,
-      signature_method:          XMLSecurity::Document::RSA_SHA256,
-      authn_requests_signed:     true,
-      logout_requests_signed:    true,
-      want_assertions_signed:    true,
-      want_assertions_encrypted: true,
-    }
-
-    true
-  end
+      settings[:private_key] = pkey.to_pem
+      settings[:certificate] = certificate
 
-  def self.apply_encrypt_only_settings(settings, security)
-    return if !security.eql?('encrypt')
+      true
+    end
 
-    settings[:security][:authn_requests_signed]  = false
-    settings[:security][:logout_requests_signed] = false
-    settings[:security][:want_assertions_signed] = false
+    def self.apply_security_default_settings(settings)
+      settings[:security] = {
+        digest_method:             XMLSecurity::Document::SHA256,
+        signature_method:          XMLSecurity::Document::RSA_SHA256,
+        authn_requests_signed:     true,
+        logout_requests_signed:    true,
+        want_assertions_signed:    true,
+        want_assertions_encrypted: true,
+      }
+
+      true
+    end
 
-    true
-  end
+    def self.apply_encrypt_only_settings(settings, security)
+      return if !security.eql?('encrypt')
 
-  def self.apply_sign_only_settings(settings, security)
-    return if !security.eql?('sign')
+      settings[:security][:authn_requests_signed]  = false
+      settings[:security][:logout_requests_signed] = false
+      settings[:security][:want_assertions_signed] = false
 
-    settings[:security][:want_assertions_encrypted] = false
+      true
+    end
 
-    true
-  end
+    def self.apply_sign_only_settings(settings, security)
+      return if !security.eql?('sign')
+
+      settings[:security][:want_assertions_encrypted] = false
 
-  private_class_method %i[
-    apply_security_settings
-    check_security_settings
-    apply_security_default_settings
-    apply_encrypt_only_settings
-    apply_sign_only_settings
-  ].freeze
+      true
+    end
 
-  private
+    private_class_method %i[
+      apply_security_settings
+      check_security_settings
+      apply_security_default_settings
+      apply_encrypt_only_settings
+      apply_sign_only_settings
+    ].freeze
 
-  def handle_logout_response(raw_response, settings)
-    logout_response = OneLogin::RubySaml::Logoutresponse.new(raw_response, settings, matches_request_id: session['saml_transaction_id'])
-    logout_response.soft = false
-    logout_response.validate
+    private
 
-    redirect_path = if session['omniauth.origin']&.include?('/mobile')
-                      '/mobile'
-                    elsif session['omniauth.origin']&.include?('/desktop')
-                      '/desktop'
-                    else
-                      '/'
-                    end
+    def handle_logout_response(raw_response, settings)
+      logout_response = OneLogin::RubySaml::Logoutresponse.new(raw_response, settings, matches_request_id: session['saml_transaction_id'])
+      logout_response.soft = false
+      logout_response.validate
 
-    self.class.destroy_session(env, session)
+      redirect_path = if session['omniauth.origin']&.include?('/mobile')
+                        '/mobile'
+                      elsif session['omniauth.origin']&.include?('/desktop')
+                        '/desktop'
+                      else
+                        '/'
+                      end
 
-    redirect "#{Setting.get('http_type')}://#{Setting.get('fqdn')}#{redirect_path}"
-  end
+      self.class.destroy_session(env, session)
 
+      redirect "#{Setting.get('http_type')}://#{Setting.get('fqdn')}#{redirect_path}"
+    end
+
+  end
+rescue LoadError
+  # Gem not installed, strategy not available.
 end
diff --git a/lib/omni_auth/strategies/twitter_database.rb b/lib/omni_auth/strategies/twitter_database.rb
index 0ea40e0b2f..abdce49e62 100644
--- a/lib/omni_auth/strategies/twitter_database.rb
+++ b/lib/omni_auth/strategies/twitter_database.rb
@@ -1,15 +1,21 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
-class OmniAuth::Strategies::TwitterDatabase < OmniAuth::Strategies::Twitter
-  option :name, 'twitter'
+begin
+  require 'omniauth-twitter'
 
-  def initialize(app, *args, &)
+  class OmniAuth::Strategies::TwitterDatabase < OmniAuth::Strategies::Twitter
+    option :name, 'twitter'
 
-    # database lookup
-    config  = Setting.get('auth_twitter_credentials') || {}
-    args[0] = config['key']
-    args[1] = config['secret']
-    super
-  end
+    def initialize(app, *args, &)
+
+      # database lookup
+      config  = Setting.get('auth_twitter_credentials') || {}
+      args[0] = config['key']
+      args[1] = config['secret']
+      super
+    end
 
+  end
+rescue LoadError
+  # Gem not installed, strategy not available.
 end
diff --git a/lib/omni_auth/strategies/weibo_database.rb b/lib/omni_auth/strategies/weibo_database.rb
index 297dd9ffd8..41bda6edc7 100644
--- a/lib/omni_auth/strategies/weibo_database.rb
+++ b/lib/omni_auth/strategies/weibo_database.rb
@@ -1,15 +1,21 @@
 # Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
 
-class OmniAuth::Strategies::WeiboDatabase < OmniAuth::Strategies::Weibo
-  option :name, 'weibo'
+begin
+  require 'omniauth-weibo-oauth2'
 
-  def initialize(app, *args, &)
+  class OmniAuth::Strategies::WeiboDatabase < OmniAuth::Strategies::Weibo
+    option :name, 'weibo'
 
-    # database lookup
-    config  = Setting.get('auth_weibo_credentials') || {}
-    args[0] = config['client_id']
-    args[1] = config['client_secret']
-    super
-  end
+    def initialize(app, *args, &)
+
+      # database lookup
+      config  = Setting.get('auth_weibo_credentials') || {}
+      args[0] = config['client_id']
+      args[1] = config['client_secret']
+      super
+    end
 
+  end
+rescue LoadError
+  # Gem not installed, strategy not available.
 end
-- 
2.53.0

openSUSE Build Service is sponsored by