Browse Source

Fixes #4997 - Implement OpenID Connect.

Co-authored-by: Tobias Schäfer <ts@zammad.com>
Florian Liebe 1 month ago
parent
commit
9bc6c7940a

+ 1 - 0
Gemfile

@@ -107,6 +107,7 @@ gem 'omniauth-gitlab'
 gem 'omniauth-google-oauth2'
 gem 'omniauth-linkedin-oauth2'
 gem 'omniauth-microsoft-office365'
+gem 'omniauth_openid_connect'
 gem 'omniauth-saml'
 gem 'omniauth-twitter'
 gem 'omniauth-weibo-oauth2', git: 'https://github.com/zammad-deps/omniauth-weibo-oauth2', branch: 'unpin-dependencies'

+ 49 - 0
Gemfile.lock

@@ -109,11 +109,13 @@ GEM
       activesupport (>= 6.1)
     addressable (2.8.7)
       public_suffix (>= 2.0.2, < 7.0)
+    aes_key_wrap (1.1.0)
     android_key_attestation (0.3.0)
     argon2 (2.3.0)
       ffi (~> 1.15)
       ffi-compiler (~> 1.0)
     ast (2.4.2)
+    attr_required (1.0.2)
     autoprefixer-rails (10.4.19.0)
       execjs (~> 2)
     aws-eventstream (1.3.0)
@@ -235,6 +237,8 @@ GEM
     email_address (0.2.5)
       base64
       simpleidn
+    email_validator (2.2.4)
+      activemodel
     equalizer (0.0.11)
     erubi (1.13.0)
     eventmachine (1.2.7)
@@ -250,6 +254,8 @@ GEM
       faraday-net_http (>= 2.0, < 3.5)
       json
       logger
+    faraday-follow_redirects (0.3.0)
+      faraday (>= 1, < 3)
     faraday-mashify (0.1.1)
       faraday (~> 2.0)
       hashie
@@ -310,6 +316,13 @@ GEM
       reline (>= 0.4.2)
     jmespath (1.6.2)
     json (2.9.1)
+    json-jwt (1.16.7)
+      activesupport (>= 4.2)
+      aes_key_wrap
+      base64
+      bindata
+      faraday (~> 2.0)
+      faraday-follow_redirects
     jwt (2.3.0)
     koala (3.6.0)
       addressable
@@ -431,6 +444,22 @@ GEM
     omniauth-twitter (1.4.0)
       omniauth-oauth (~> 1.1)
       rack
+    omniauth_openid_connect (0.8.0)
+      omniauth (>= 1.9, < 3)
+      openid_connect (~> 2.2)
+    openid_connect (2.3.1)
+      activemodel
+      attr_required (>= 1.0.0)
+      email_validator
+      faraday (~> 2.0)
+      faraday-follow_redirects
+      json-jwt (>= 1.16)
+      mail
+      rack-oauth2 (~> 2.2)
+      swd (~> 2.0)
+      tzinfo
+      validate_url
+      webfinger (~> 2.0)
     openssl (3.3.0)
     openssl-signature_algorithm (1.3.0)
       openssl (> 2.0)
@@ -485,6 +514,13 @@ GEM
     rack (2.2.10)
     rack-attack (6.7.0)
       rack (>= 1.0, < 4)
+    rack-oauth2 (2.2.1)
+      activesupport
+      attr_required
+      faraday (~> 2.0)
+      faraday-follow_redirects
+      json-jwt (>= 1.11.0)
+      rack (>= 2.1.0)
     rack-protection (3.2.0)
       base64 (>= 0.1.0)
       rack (~> 2.2, >= 2.2.4)
@@ -657,6 +693,11 @@ GEM
       activesupport (>= 6.1)
       sprockets (>= 3.0.0)
     stringio (3.1.2)
+    swd (2.0.3)
+      activesupport (>= 3)
+      attr_required (>= 0.0.5)
+      faraday (~> 2.0)
+      faraday-follow_redirects
     systemu (2.6.5)
     tcr (0.4.1)
     telegram-bot-ruby (2.2.0)
@@ -701,6 +742,9 @@ GEM
     unicode-display_width (2.6.0)
     uri (0.13.1)
     useragent (0.16.11)
+    validate_url (1.0.15)
+      activemodel (>= 3.0.0)
+      public_suffix
     vcr (6.3.1)
       base64
     version_gem (1.1.4)
@@ -725,6 +769,10 @@ GEM
       openssl (>= 2.2)
       safety_net_attestation (~> 0.4.0)
       tpm-key_attestation (~> 0.12.0)
+    webfinger (2.1.3)
+      activesupport
+      faraday (~> 2.0)
+      faraday-follow_redirects
     webmock (3.24.0)
       addressable (>= 2.8.0)
       crack (>= 0.3.2)
@@ -821,6 +869,7 @@ DEPENDENCIES
   omniauth-saml
   omniauth-twitter
   omniauth-weibo-oauth2!
+  omniauth_openid_connect
   openssl
   overcommit
   parallel

+ 5 - 0
LICENSE-ICONS-3RD-PARTY.json

@@ -564,6 +564,11 @@
         "url": "",
         "license": "MIT"
     },
+    "openid-connect-button.svg": {
+        "author": "OpenID",
+        "url": "https://openid.net/",
+        "license": ""
+    },
     "organization.svg": {
         "author": "Felix Niklas",
         "url": "",

+ 5 - 0
app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee

@@ -111,4 +111,9 @@ App.Config.set('auth_provider_all', {
     name:   __('SSO')
     config: 'auth_sso'
     class:  'sso'
+  openid_connect:
+    url:    '/auth/openid_connect'
+    name:   __('OpenID Connect')
+    config: 'auth_openid_connect'
+    class:  'openid-connect'
 })

+ 1 - 0
app/assets/stylesheets/svg-dimensions.css

@@ -112,6 +112,7 @@
 .icon-note { width: 16px; height: 16px; }
 .icon-oauth2-button { width: 29px; height: 24px; }
 .icon-one-ticket { width: 48px; height: 10px; }
+.icon-openid-connect-button { width: 29px; height: 24px; }
 .icon-organization { width: 16px; height: 16px; }
 .icon-outbound-calls { width: 17px; height: 17px; }
 .icon-overflow-button { width: 3px; height: 13px; }

+ 50 - 0
app/controllers/concerns/handles_oidc_authorization.rb

@@ -0,0 +1,50 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+module HandlesOidcAuthorization
+  extend ActiveSupport::Concern
+
+  included do # rubocop:disable Metrics/BlockLength
+    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 'logout_token' is missing.") if params[:logout_token].blank?
+
+      begin
+        oidc = OmniAuth::Strategies::OidcDatabase.new(OmniAuth::Strategies::OidcDatabase.setup)
+        decoded = oidc.decode_logout_token(params[:logout_token])
+      rescue => e
+        Rails.logger.error "OpenID Connect OP-initiated logout failed: #{e.message}"
+        raise Exceptions::UnprocessableEntity, __("The 'logout_token' is invalid.")
+      end
+
+      raise Exceptions::UnprocessableEntity, __("The 'logout_token' does not contain any session information.") if decoded.sid.blank?
+
+      Session.all.detect { |s| s.data['oidc_sid'] == decoded.sid }&.destroy
+    end
+
+    private
+
+    def oidc_session?
+      session[:oidc_id_token].present?
+    end
+
+    def oidc_destroy
+      oidc = OmniAuth::Strategies::OidcDatabase.new(OmniAuth::Strategies::OidcDatabase.setup)
+
+      options = oidc.config
+
+      logout_url = Addressable::URI.parse(options.end_session_endpoint)
+      logout_url.query_values = {
+        id_token_hint:            session[:oidc_id_token],
+        post_logout_redirect_uri: "#{Setting.get('http_type')}://#{Setting.get('fqdn')}"
+      }
+
+      OmniAuth::Strategies::OidcDatabase.destroy_session(request.env, session)
+
+      render json: { url: logout_url.to_s }
+    rescue => e
+      Rails.logger.error "OpenID Connect RP-initiated logout failed: #{e.message}"
+    end
+
+  end
+end

+ 19 - 9
app/controllers/sessions_controller.rb

@@ -1,8 +1,12 @@
 # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
 
 class SessionsController < ApplicationController
+  include HandlesOidcAuthorization
+
   prepend_before_action :authenticate_and_authorize!, only: %i[switch_to_user list delete]
-  skip_before_action :verify_csrf_token, only: %i[show destroy create_omniauth failure_omniauth saml_destroy]
+  skip_before_action :verify_csrf_token, only: %i[show destroy
+                                                  create_omniauth failure_omniauth
+                                                  saml_destroy]
   skip_before_action :user_device_log, only: %i[create_sso create_omniauth]
 
   def show
@@ -64,13 +68,8 @@ class SessionsController < ApplicationController
       ENV['FAKE_SELENIUM_LOGIN_PENDING'] = nil # rubocop:disable Rails/EnvironmentVariableAccess
     end
 
-    if (session['saml_uid'] || session['saml_session_index']) && OmniAuth::Strategies::SamlDatabase.setup.fetch('idp_slo_service_url', nil)
-      begin
-        return saml_destroy
-      rescue => e
-        Rails.logger.error "SAML SLO failed: #{e.message}"
-      end
-    end
+    return saml_destroy if saml_session?
+    return oidc_destroy if oidc_session?
 
     reset_session
 
@@ -130,6 +129,11 @@ class SessionsController < ApplicationController
     # remember omnitauth login
     session[:authentication_type] = 'omniauth'
 
+    if auth['credentials']['id_token'].present?
+      session[:oidc_id_token] = auth['credentials']['id_token']
+      session[:oidc_sid] = auth['extra']['raw_info']['sid']
+    end
+
     # Set needed fingerprint parameter.
     if request.env['omniauth.params']['fingerprint'].present?
       params[:fingerprint] = request.env['omniauth.params']['fingerprint']
@@ -307,6 +311,7 @@ class SessionsController < ApplicationController
     #   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]
 
     # Include the flag for JSON column type support (currently only on PostgreSQL backend).
     config['column_type_json_supported'] =
@@ -331,6 +336,10 @@ class SessionsController < ApplicationController
     config
   end
 
+  def saml_session?
+    (session['saml_uid'] || session['saml_session_index']) && OmniAuth::Strategies::SamlDatabase.setup.fetch('idp_slo_service_url', nil)
+  end
+
   def saml_destroy
     options = OmniAuth::Strategies::SamlDatabase.setup
     settings = OneLogin::RubySaml::Settings.new(options)
@@ -347,6 +356,7 @@ class SessionsController < ApplicationController
     url = logout_request.create(settings)
 
     render json: { url: url }
+  rescue => e
+    Rails.logger.error "SAML SLO failed: #{e.message}"
   end
-
 end

+ 4 - 0
app/frontend/apps/desktop/initializer/3RD-PARTY-ICONS.md

@@ -133,6 +133,10 @@
   - Author: Microsoft
   - URL: https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks
 
+- `assets/openid-connect.svg`
+  - Author: OpenID
+  - URL: https://openid.net
+
 - `assets/saml.svg`
 
   - Author: OASIS

+ 5 - 0
app/frontend/apps/desktop/initializer/assets/openid-connect.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.24015 2.56271V15.5377L9.66306 14.018V1L7.24015 2.56271Z" />
+<path d="M6.24014 13.8355C4.00233 13.2917 2.42292 11.9421 2.42292 10.3621C2.42292 8.79315 4.00213 7.44354 6.24014 6.89593V5.32592C2.64392 5.92733 0 7.95394 0 10.3621C0 12.7702 2.65112 14.7998 6.24014 15.4069V13.8355Z" />
+<path d="M14.423 6.67744C13.4014 6.04133 12.107 5.56833 10.6631 5.32583V6.89244C11.4392 7.08114 12.1373 7.36554 12.7169 7.72394L11.1685 8.68455H16V5.70243L14.4229 6.67734L14.423 6.67744Z" />
+</svg>

+ 4 - 0
app/frontend/apps/mobile/initializer/3RD-PARTY-ICONS.md

@@ -29,6 +29,10 @@
   - Author: Microsoft
   - URL: https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks
 
+- `assets/openid-connect.svg`
+  - Author: OpenID
+  - URL: https://openid.net
+
 - `assets/saml.svg`
   - Author: OASIS
   - URL: https://saml.xml.org/wiki/saml-logos

Some files were not shown because too many files changed in this diff